Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions assignment/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.2 on 2025-04-23 12:51
# Generated by Django 5.2 on 2025-05-08 10:38

import django.db.models.deletion
from django.db import migrations, models
Expand All @@ -9,7 +9,8 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
('fleet', '0001_initial'),
('fleet', '0003_remove_fuelrecord_vehicle_and_more'),
('shipments', '0002_remove_shipment_destination_warehouse_id_and_more'),
]

operations = [
Expand All @@ -18,9 +19,26 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('delivery_locations', models.JSONField()),
('started_at', models.DateTimeField(blank=True, null=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('status', models.CharField(choices=[('created', 'Created'), ('dispatched', 'Dispatched'), ('partially_completed', 'Partially Completed'), ('completed', 'Completed'), ('failed', 'Failed'), ('reassigned', 'Reassigned')], default='created', max_length=32)),
('total_load', models.PositiveIntegerField()),
('vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.vehicle')),
],
),
migrations.CreateModel(
name='AssignmentItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('delivery_sequence', models.PositiveIntegerField()),
('delivery_location', models.JSONField()),
('is_delivered', models.BooleanField(default=False)),
('delivered_at', models.DateTimeField(blank=True, null=True)),
('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='assignment.assignment')),
('shipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shipments.shipment')),
],
options={
'ordering': ['delivery_sequence'],
},
),
]
11 changes: 0 additions & 11 deletions assignment/models.py

This file was deleted.

Empty file added assignment/models/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions assignment/models/assignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.db import models
from fleet.models import Vehicle

class Assignment(models.Model):
vehicle = models.ForeignKey(Vehicle, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)

status = models.CharField(
max_length=32,
choices=[
('created', 'Created'),
('dispatched', 'Dispatched'),
('partially_completed', 'Partially Completed'),
('completed', 'Completed'),
('failed', 'Failed'),
('reassigned', 'Reassigned'),
],
default='created'
)

total_load = models.PositiveIntegerField()

def __str__(self):
return f"Assignment #{self.id} to Vehicle {self.vehicle.vehicle_id}"
20 changes: 20 additions & 0 deletions assignment/models/assignment_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import models

from assignment.models.assignment import Assignment
from shipments.models import Shipment


class AssignmentItem(models.Model):
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE, related_name='items')
shipment = models.ForeignKey(Shipment, on_delete=models.CASCADE)
delivery_sequence = models.PositiveIntegerField() # 1st, 2nd, 3rd drop, etc.
delivery_location = models.JSONField() # { "lat": ..., "lng": ... }

is_delivered = models.BooleanField(default=False)
delivered_at = models.DateTimeField(null=True, blank=True)

class Meta:
ordering = ['delivery_sequence']

def __str__(self):
return f"Shipment {self.shipment.id} in Assignment {self.assignment.id}"
4 changes: 3 additions & 1 deletion assignment/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from rest_framework import serializers
from .models import Assignment

from assignment.models.assignment import Assignment


class AssignmentSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
Empty file added assignment/services/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions assignment/services/assignment_planner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import logging
from typing import List
from datetime import datetime

from assignment.models.assignment import Assignment
from assignment.models.assignment_item import AssignmentItem
from assignment.services.mappers import map_vehicle_model
from fleet.models import Vehicle
from route_optimizer.services.vrp_solver import solve_cvrp
from shipments.models import Shipment
from route_optimizer.models.vrp_input import VRPInputBuilder, VRPCompiler, Location, DeliveryTask

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

class AssignmentPlanner:
def __init__(self, vehicles: List[Vehicle], shipments: List[Shipment]):
self.vehicles = vehicles
self.shipments = shipments

def plan_assignments(self) -> List[Assignment]:
logger.info("Planning assignments started.")
builder = VRPInputBuilder()

vehicle_map = {}
for v in self.vehicles:
logger.debug(f"Mapping vehicle: {v.vehicle_id}")
mapped_vehicle = map_vehicle_model(v)
builder.add_vehicle(mapped_vehicle)
vehicle_map[mapped_vehicle.id] = v
logger.info(f"{len(vehicle_map)} vehicles added to VRP input.")

shipment_map = {}
for s in self.shipments:
logger.debug(f"Adding shipment: {s.shipment_id} (demand={s.demand})")
builder.add_delivery_task(
DeliveryTask(
id=str(s.id),
pickup=Location(lat=s.origin["lat"], lon=s.origin["lng"]),
delivery=Location(lat=s.destination["lat"], lon=s.destination["lng"]),
demand=s.demand,
)
)
shipment_map[str(s.id)] = s
logger.info(f"{len(shipment_map)} shipments added to VRP input.")

vrp_input = VRPCompiler.compile(builder)
logger.debug(f"Compiled VRP input with {len(vrp_input.location_ids)} locations.")

result = solve_cvrp(vrp_input)
logger.info("Optimizer finished solving.")

if result["status"] != "success":
logger.error("Optimizer failed to find a solution.")
raise Exception("Optimization failed")

assignments = []
for i, route in enumerate(result["routes"]):
vehicle = self.vehicles[i]
logger.debug(f"Creating assignment for vehicle {vehicle.vehicle_id}, route: {route}")
assignment = Assignment.objects.create(
vehicle=vehicle,
total_load=sum(
vrp_input.demands[node] for node in route if vrp_input.demands[node] > 0
Comment on lines +58 to +64

Copilot AI May 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Assuming that the order of routes from the VRP solver always matches the order of self.vehicles might be fragile. Consider adding a mapping mechanism to associate each route with the correct vehicle to improve robustness.

Suggested change
for i, route in enumerate(result["routes"]):
vehicle = self.vehicles[i]
logger.debug(f"Creating assignment for vehicle {vehicle.vehicle_id}, route: {route}")
assignment = Assignment.objects.create(
vehicle=vehicle,
total_load=sum(
vrp_input.demands[node] for node in route if vrp_input.demands[node] > 0
for route in result["routes"]:
vehicle_id = route["vehicle_id"] # Assuming each route includes a vehicle_id field
vehicle = vehicle_map.get(vehicle_id)
if not vehicle:
logger.error(f"Vehicle with ID {vehicle_id} not found in vehicle_map.")
raise Exception(f"Vehicle with ID {vehicle_id} not found.")
logger.debug(f"Creating assignment for vehicle {vehicle.vehicle_id}, route: {route['nodes']}")
assignment = Assignment.objects.create(
vehicle=vehicle,
total_load=sum(
vrp_input.demands[node] for node in route["nodes"] if vrp_input.demands[node] > 0

Copilot uses AI. Check for mistakes.
),
status='created'
)

seq = 1
for node in route:
if node in vrp_input.task_index_map:
task_id, role = vrp_input.task_index_map[node]
shipment = shipment_map[task_id]
loc = shipment.destination if role == "delivery" else shipment.origin

logger.debug(f"Adding {role} for shipment {shipment.shipment_id} at sequence {seq}")
AssignmentItem.objects.create(
assignment=assignment,
shipment=shipment,
delivery_sequence=seq,
delivery_location={
"lat": loc["lat"],
"lng": loc["lng"],
}
)
seq += 1

assignments.append(assignment)

logger.info(f"{len(assignments)} assignments successfully created.")
return assignments
14 changes: 14 additions & 0 deletions assignment/services/mappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from route_optimizer.models.vrp_input import Vehicle as VRPVehicle, Location

def map_vehicle_model(vehicle_model):
if vehicle_model.depot_latitude is None or vehicle_model.depot_longitude is None:
raise ValueError(f"Vehicle {vehicle_model.vehicle_id} missing depot coordinates")

return VRPVehicle(
id=vehicle_model.vehicle_id,
capacity=vehicle_model.capacity,
depot=Location(
lat=float(vehicle_model.depot_latitude),
lon=float(vehicle_model.depot_longitude)
)
)
69 changes: 0 additions & 69 deletions assignment/tests.py

This file was deleted.

Empty file added assignment/tests/__init__.py
Empty file.
Loading