Spaces:
Sleeping
Sleeping
import pandas as pd | |
from itertools import permutations, combinations | |
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request, Response | |
from fastapi.middleware.cors import CORSMiddleware | |
from pydantic import BaseModel | |
from typing import List, Dict, Any | |
import uuid | |
import requests | |
import json | |
app = FastAPI() | |
stored_combinations = {} | |
processed_data_store: Dict[str, Dict[str, Any]] = {} # Store processed data with session_id | |
app.add_middleware( | |
CORSMiddleware, | |
allow_origins=["*"], # Adjust as needed for security | |
allow_credentials=True, | |
allow_methods=["*"], | |
allow_headers=["*"], | |
) | |
async def preflight_handler(request: Request, rest_of_path: str) -> Response: | |
return Response(headers={ | |
"Access-Control-Allow-Origin": "*", # Adjust as needed for security | |
"Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE", | |
"Access-Control-Allow-Headers": "*", | |
}) | |
# Box and Truck classes | |
class Box: | |
def __init__(self, length, width, height, quantity, box_type): | |
self.length = length | |
self.width = width | |
self.height = height | |
self.quantity = quantity if quantity > 0 else 1 | |
self.type = box_type | |
def rotated_dimensions(self): | |
possible_rotations = [ | |
(self.length, self.width, self.height), | |
(self.length, self.height, self.width), | |
(self.width, self.length, self.height), | |
(self.width, self.height, self.length), | |
(self.height, self.length, self.width), | |
(self.height, self.width, self.length) | |
] | |
return sorted(possible_rotations, key=lambda x: (x[0] * x[1], x[2]), reverse=True) | |
def volume(self): | |
return self.length * self.width * self.height | |
class Truck: | |
def __init__(self, length, width, height): | |
self.length = length | |
self.width = width | |
self.height = height | |
self.volume = length * width * height | |
self.placed_boxes = [] | |
self.available_space = [(0, 0, 0, length, width, height)] | |
def is_space_supported(self, position, box): | |
x, y, z = position | |
bl, bw, bh = box | |
if z == 0: | |
return True | |
for placed_box, placed_position in self.placed_boxes: | |
px, py, pz = placed_position | |
pl, pw, ph = placed_box | |
if (x >= px and x + bl <= px + pl) and (y >= py and y + bw <= py + pw) and (z == pz + ph): | |
return True | |
return False | |
def add_box_ffd(self, box): | |
best_fit = None | |
best_fit_space_index = -1 | |
best_fit_volume_waste = float('inf') | |
for rotation in box.rotated_dimensions(): | |
for i, space in enumerate(self.available_space): | |
x, y, z, l, w, h = space | |
if rotation[0] <= l and rotation[1] <= w and rotation[2] <= h: | |
if not self.is_space_supported((x, y, z), rotation): | |
continue | |
leftover_volume = (l * w * h) - (rotation[0] * rotation[1] * rotation[2]) | |
if leftover_volume < best_fit_volume_waste: | |
best_fit = rotation | |
best_fit_space_index = i | |
best_fit_volume_waste = leftover_volume | |
if best_fit: | |
x, y, z, l, w, h = self.available_space[best_fit_space_index] | |
box_position = (x, y, z) | |
self.placed_boxes.append((best_fit, box_position)) | |
self.available_space.pop(best_fit_space_index) | |
self.update_space(best_fit, box_position, l, w, h) | |
return box_position | |
else: | |
return None | |
def update_space(self, box, position, l, w, h): | |
x, y, z = position | |
bl, bw, bh = box | |
new_spaces = [ | |
(x + bl, y, z, l - bl, w, h), | |
(x, y + bw, z, bl, w - bw, h), | |
(x, y, z + bh, bl, bw, h - bh), | |
] | |
new_spaces = [space for space in new_spaces if space[3] > 0 and space[4] > 0 and space[5] > 0] | |
self.available_space.extend(new_spaces) | |
self.available_space.sort(key=lambda space: (space[2], space[1], space[0])) | |
# Helper functions for box packing | |
def pack_boxes_ffd(truck, consignments): | |
packed_positions = [] | |
consignments = sorted(consignments, key=lambda c: (not c[1], -sum(box.volume() * box.quantity for box in c[0]))) | |
for consignment in consignments: | |
consignment_packed = [] | |
for box in consignment[0]: | |
for _ in range(box.quantity): | |
position = truck.add_box_ffd(box) | |
if position is None: | |
break | |
consignment_packed.append((box, position)) | |
if len(consignment_packed) == sum(box.quantity for box in consignment[0]): | |
packed_positions.extend(consignment_packed) | |
else: | |
break | |
return packed_positions | |
def find_max_consignments_combinations(truck, consignments, combination_limit=100): | |
combinations_that_fit = [] | |
consignments_sorted = sorted(consignments, key=lambda c: (not c[1], -sum(box.volume() * box.quantity for box in c[0]))) | |
combo_count = 0 | |
max_consignments_fitted = 0 | |
for r in range(1, len(consignments_sorted) + 1): | |
for combo in combinations(consignments_sorted, r): | |
combo_count += 1 | |
print(f"Checking combination {combo_count}: {[f'Consignment {boxes[0].type}' for boxes, _ in combo]}") | |
if combo_count > combination_limit: | |
print(f"Reached combination limit of {combination_limit}. Stopping further checks.") | |
break | |
temp_truck = Truck(truck.length, truck.width, truck.height) | |
total_volume = sum(sum(box.volume() * box.quantity for box in consignment[0]) for consignment in combo) | |
if total_volume > temp_truck.volume: | |
continue | |
try: | |
packed_positions = pack_boxes_ffd(temp_truck, combo) | |
if packed_positions: | |
print(f"Combination {combo_count} fits with {len(combo)} consignments") | |
combinations_that_fit.append(combo) | |
max_consignments_fitted = max(max_consignments_fitted, len(combo)) | |
if len(combinations_that_fit) >= 5: | |
break | |
except HTTPException: | |
print(f"Combination {combo_count} does not fit") | |
continue | |
if combo_count > combination_limit: | |
break | |
return combinations_that_fit[-5:] | |
def suggest_truck(consignments): | |
trucks = { | |
"TATA ACE": {"length": 7, "width": 4.8, "height": 4.8}, | |
"ASHOK LEYLAND DOST": {"length": 7, "width": 4.8, "height": 4.8}, | |
"MAHINDRA BOLERO PICK UP": {"length": 8, "width": 5, "height": 4.8}, | |
"ASHOK LEYLAND BADA DOST": {"length": 9.5, "width": 5.5, "height": 5}, | |
"TATA 407": {"length": 9, "width": 5.5, "height": 5}, | |
"EICHER 14 FEET": {"length": 14, "width": 6, "height": 6.5}, | |
"EICHER 17 FEET": {"length": 17, "width": 6, "height": 7}, | |
"EICHER 19 FEET": {"length": 19, "width": 7, "height": 7}, | |
"TATA 22 FEET": {"length": 22, "width": 7.5, "height": 7}, | |
"TATA TRUCK (6 TYRE)": {"length": 17.5, "width": 7, "height": 7}, | |
"TAURUS 16 T (10 TYRE)": {"length": 21, "width": 7.2, "height": 7}, | |
"TAURUS 21 T (12 TYRE)": {"length": 24, "width": 7.3, "height": 7}, | |
"TAURUS 25 T (14 TYRE)": {"length": 28, "width": 7.8, "height": 7}, | |
"CONTAINER 20 FT": {"length": 20, "width": 8, "height": 8}, | |
"CONTAINER 32 FT SXL": {"length": 32, "width": 8, "height": 8}, | |
"CONTAINER 32 FT MXL": {"length": 32, "width": 8, "height": 8}, | |
"CONTAINER 32 FT SXL / MXL HQ": {"length": 32, "width": 8, "height": 10}, | |
"20 FEET OPEN ALL SIDE (ODC)": {"length": 20, "width": 8, "height": 8}, | |
"28-32 FEET OPEN-TRAILOR JCB ODC": {"length": 28, "width": 8, "height": 8}, | |
"32 FEET OPEN-TRAILOR ODC": {"length": 32, "width": 8, "height": 8}, | |
"40 FEET OPEN-TRAILOR ODC": {"length": 40, "width": 8, "height": 8}, | |
"SCV": {"length": 5, "width": 12, "height": 6}, | |
"LCV": {"length": 11, "width": 5, "height": 5}, | |
"ICV": {"length": 16, "width": 6.5, "height": 6.5}, | |
"MCV": {"length": 19, "width": 7, "height": 7} | |
} | |
sorted_trucks = sorted(trucks.items(), key=lambda t: t[1]['length'] * t[1]['width'] * t[1]['height']) | |
for truck_name, dimensions in sorted_trucks: | |
truck = Truck(dimensions['length'] * 12, dimensions['width'] * 12, dimensions['height'] * 12) | |
packed_positions = pack_boxes_ffd(truck, consignments) | |
total_boxes = sum(box.quantity for consignment in consignments for box in consignment[0]) | |
if len(packed_positions) == total_boxes: | |
return {"name": truck_name, "dimensions": dimensions} | |
return None | |
# Models | |
class BoxData(BaseModel): | |
PieceLength: float | |
PieceBreadth: float | |
PieceHeight: float | |
Priority: int | |
class ConsignmentData(BaseModel): | |
ConsignmentNo: str | |
Priority: int | |
boxes: List[BoxData] | |
class SubmitDataRequest(BaseModel): | |
truck_name: str | |
autoSuggest: bool | |
consignments_data: List[ConsignmentData] | |
from fastapi.responses import RedirectResponse | |
async def submit_data(request: SubmitDataRequest): | |
""" | |
Endpoint to receive data from the client via API. | |
""" | |
truck_name = request.truck_name | |
autoSuggest = request.autoSuggest | |
consignments_data = request.consignments_data # This is already parsed from JSON | |
global stored_combinations, processed_data_store | |
# Parse consignments and calculate total quantity based on box dimensions | |
consignments = [] | |
for consignment_data in consignments_data: | |
consignment_no = consignment_data.ConsignmentNo # ConsignmentNo is now accessible as a string | |
is_priority = consignment_data.Priority | |
# Aggregate the boxes with same dimensions | |
box_aggregation = {} | |
for box_data in consignment_data.boxes: | |
dimensions = (box_data.PieceLength, box_data.PieceBreadth, box_data.PieceHeight) | |
if dimensions in box_aggregation: | |
box_aggregation[dimensions]['Quantity'] += 1 # Increment the count if dimensions are the same | |
else: | |
box_aggregation[dimensions] = { | |
'PieceLength': box_data.PieceLength, | |
'PieceBreadth': box_data.PieceBreadth, | |
'PieceHeight': box_data.PieceHeight, | |
'Quantity': 1 # Initialize count to 1 | |
} | |
# Convert aggregated boxes back to the expected list format | |
consignment_boxes = [ | |
Box( | |
box['PieceLength'] / 2.54, # Convert cm to inches | |
box['PieceBreadth'] / 2.54, | |
box['PieceHeight'] / 2.54, | |
box['Quantity'], | |
f"Consignment {consignment_no} ({'Priority' if is_priority else 'Non-Priority'})" | |
) | |
for box in box_aggregation.values() | |
] | |
consignments.append((consignment_boxes, is_priority)) | |
# Handle autoSuggest logic and other functionality here | |
if autoSuggest: | |
suggested_truck = suggest_truck(consignments) | |
if suggested_truck: | |
truck_name = suggested_truck["name"] | |
length = suggested_truck['dimensions']['length'] | |
width = suggested_truck['dimensions']['width'] | |
height = suggested_truck['dimensions']['height'] | |
else: | |
raise HTTPException(status_code=400, detail="No suitable truck found") | |
else: | |
# Assuming truck dimensions are known based on truck_name | |
trucks = { | |
"TATA ACE": {"length": 7, "width": 4.8, "height": 4.8}, | |
"ASHOK LEYLAND DOST": {"length": 7, "width": 4.8, "height": 4.8}, | |
"MAHINDRA BOLERO PICK UP": {"length": 8, "width": 5, "height": 4.8}, | |
"ASHOK LEYLAND BADA DOST": {"length": 9.5, "width": 5.5, "height": 5}, | |
"TATA 407": {"length": 9, "width": 5.5, "height": 5}, | |
"EICHER 14 FEET": {"length": 14, "width": 6, "height": 6.5}, | |
"EICHER 17 FEET": {"length": 17, "width": 6, "height": 7}, | |
"EICHER 19 FEET": {"length": 19, "width": 7, "height": 7}, | |
"TATA 22 FEET": {"length": 22, "width": 7.5, "height": 7}, | |
"TATA TRUCK (6 TYRE)": {"length": 17.5, "width": 7, "height": 7}, | |
"TAURUS 16 T (10 TYRE)": {"length": 21, "width": 7.2, "height": 7}, | |
"TAURUS 21 T (12 TYRE)": {"length": 24, "width": 7.3, "height": 7}, | |
"TAURUS 25 T (14 TYRE)": {"length": 28, "width": 7.8, "height": 7}, | |
"CONTAINER 20 FT": {"length": 20, "width": 8, "height": 8}, | |
"CONTAINER 32 FT SXL": {"length": 32, "width": 8, "height": 8}, | |
"CONTAINER 32 FT MXL": {"length": 32, "width": 8, "height": 8}, | |
"CONTAINER 32 FT SXL / MXL HQ": {"length": 32, "width": 8, "height": 10}, | |
"20 FEET OPEN ALL SIDE (ODC)": {"length": 20, "width": 8, "height": 8}, | |
"28-32 FEET OPEN-TRAILOR JCB ODC": {"length": 28, "width": 8, "height": 8}, | |
"32 FEET OPEN-TRAILOR ODC": {"length": 32, "width": 8, "height": 8}, | |
"40 FEET OPEN-TRAILOR ODC": {"length": 40, "width": 8, "height": 8}, | |
"SCV": {"length": 5, "width": 12, "height": 6}, | |
"LCV": {"length": 11, "width": 5, "height": 5}, | |
"ICV": {"length": 16, "width": 6.5, "height": 6.5}, | |
"MCV": {"length": 19, "width": 7, "height": 7} | |
} | |
if truck_name not in trucks: | |
raise HTTPException(status_code=400, detail="Invalid truck name provided") | |
truck_dimensions = trucks[truck_name] | |
length = truck_dimensions['length'] | |
width = truck_dimensions['width'] | |
height = truck_dimensions['height'] | |
# Initialize the Truck object and pack boxes | |
truck = Truck(length * 12, width * 12, height * 12) # Convert feet to inches | |
packed_positions = pack_boxes_ffd(truck, consignments) | |
total_boxes = sum(box.quantity for consignment in consignments for box in consignment[0]) | |
if len(packed_positions) < total_boxes: | |
stored_combinations = find_max_consignments_combinations(truck, consignments) | |
if stored_combinations: | |
combinations_info = [] | |
for index, combo in enumerate(stored_combinations): | |
total_quantity = sum(box.quantity for boxes, _ in combo for box in boxes) | |
combination_info = { | |
"combination_number": index + 1, | |
"consignments": [ | |
{ | |
"consignment_number": f"{boxes[0].type.split(' ')[1]}", | |
"priority_status": boxes[0].type.split('(')[1].strip(')') | |
} | |
for boxes, _ in combo | |
], | |
"total_quantity": total_quantity # Add total quantity for this combination | |
} | |
combinations_info.append(combination_info) | |
# Generate a unique session_id | |
session_id = str(uuid.uuid4()) | |
processed_data_store[session_id] = { | |
"error": "Not all consignments fit.", | |
"combinations_that_fit": combinations_info, | |
"truck": { | |
"name": truck_name, | |
"dimensions": { | |
"length": length, | |
"width": width, | |
"height": height | |
} | |
} | |
} | |
# Redirect to the visualization page | |
return RedirectResponse(url=f"https://66e6a59e6eaa79674a3c7c47--stellar-jelly-eac527.netlify.app?session_id={session_id}", status_code=302) | |
# Prepare box data for visualization | |
box_data = [ | |
{ | |
'length': box.length, | |
'width': box.width, | |
'height': box.height, | |
'type': box.type, | |
'quantity': box.quantity, | |
'position': {'x': pos[0], 'y': pos[1], 'z': pos[2]} | |
} | |
for box, pos in packed_positions | |
] | |
# Generate a unique session_id | |
session_id = str(uuid.uuid4()) | |
processed_data_store[session_id] = { | |
"boxes": box_data, | |
"truck": { | |
"name": truck_name, | |
"dimensions": { | |
"length": length, | |
"width": width, | |
"height": height | |
} | |
} | |
} | |
# Redirect to the visualization page | |
return RedirectResponse(url=f"https://66e6a59e6eaa79674a3c7c47--stellar-jelly-eac527.netlify.app?session_id={session_id}", status_code=302) | |
async def load_combination( | |
index: int = Form(...), | |
truck_length: float = Form(...), | |
truck_width: float = Form(...), | |
truck_height: float = Form(...), | |
truck_name: str = Form(...) | |
): | |
""" | |
Endpoint to visualize the selected combination. | |
""" | |
print(f"Received index: {index}") | |
print(f"Received truck details: {truck_length}L x {truck_width}W x {truck_height}H, Name: {truck_name}") | |
# Ensure the index is within the valid range | |
if index >= len(stored_combinations): | |
raise HTTPException(status_code=400, detail="Invalid combination index") | |
selected_combination = stored_combinations[index] | |
# Create a Truck object using the received truck dimensions | |
truck = Truck(truck_length * 12, truck_width * 12, truck_height * 12) # Convert feet to inches | |
# Unpack and structure the combination data for the pack_boxes_ffd function | |
consignments = [ | |
(boxes, priority) for boxes, priority in selected_combination # Unpack the combination as expected | |
] | |
# Generate the box data for this combination | |
packed_positions = pack_boxes_ffd(truck, consignments) | |
# Prepare the box data | |
box_data = [ | |
{ | |
'length': box.length, | |
'width': box.width, | |
'height': box.height, | |
'type': box.type, | |
'quantity': box.quantity, | |
'position': {'x': pos[0], 'y': pos[1], 'z': pos[2]} | |
} | |
for box, pos in packed_positions | |
] | |
# Return the box data and truck information to the frontend | |
return { | |
"boxes": box_data, | |
"truck": { | |
"name": truck_name, | |
"dimensions": { | |
"length": truck_length, | |
"width": truck_width, | |
"height": truck_height | |
} | |
} | |
} | |
async def get_visualization(session_id: str): | |
""" | |
Endpoint for the frontend to fetch processed data using session_id. | |
""" | |
if session_id not in processed_data_store: | |
raise HTTPException(status_code=404, detail="Session ID not found") | |
return processed_data_store[session_id] | |