![]() |
CloudTwin
ROS2 Humble
Digital twin for path and trajectory optimisation
|
This page explains the **==logic==** behind the delivery optimizer — not the web application itself (for the app, its dependencies and how to run it, see the delivery_optimization section of the main README).
The optimizer answers one operational question: when the pharmacy has several deliveries waiting, in what order should the robot deliver them so that critical medication arrives first and total travel time is minimised? It does this with a learned travel-time model and a greedy sequencing algorithm, and it keeps improving because every real robot movement is recorded and fed back into the model.
There are two feedback loops in DRYVbot. The navigation feedback loop (robot ↔ digital twin, replanning around crowds) is described in architecture.md. This page describes the second one: the model feedback loop, where recorded deliveries make the travel-time predictor more accurate over time.
The model is a gradient-boosted regression model (LightGBM) that estimates how long the robot needs to travel between two rooms:
| Inputs (features) | departure room, arrival room, departure time |
| Target | travel duration in**seconds** |
The departure time matters because the hospital is not equally busy at all hours: during a crowd or emergency scenario (see navigation_==logic==.md) corridors fill with people and Nav2 has to detour, so the same A→B trip takes longer. By learning from time-stamped trips, the model captures these congestion patterns instead of assuming a fixed distance/speed.
The trained model runs as a separate Python microservice, not inside theLaravel application. Laravel talks to it over HTTP:
services.prediction.url (env PREDICTION_SERVICE_URL, default http://172.30.0.20:8001).POST /predictThe model service and its training code/notebooks are kept outside this Laravel repository (the
delivery_optimization/AI/folder is the intended home for them). This page documents the contract and the data flow; the training scripts themselves are maintained alongside the model service.
app/Services/PredictionService.php wraps the call:
predicted_duration_s to minutes (rounded to 2 decimals).This means the optimizer degrades gracefully: without the model it still produces a sequence (just a less accurate one).
The model is trained — and retrained — on data produced by the robot actually moving. Two complementary sources feed the training set.
The simulation_logger.py node in the digital_twin package records one JSON record per room command, into simulation_logs/simulation_<timestamp>.json. Each record contains the departure position + timestamp, the target room, thearrival position + timestamp, the measured duration_s, and the position_error_m. The full schema is documented in the README ("Simulation log format").
These logs are the ground-truth travel times the model learns from. Because they are produced under the controlled normal / crowd / emergency scenarios, they cover a realistic spread of congestion conditions.
When the digital twin completes a delivery, it posts the real timings back to the web app via POST to DtWebhookController (/api/...):
This updates the order's dt_* columns and is also the basis of the DtData model (salle_code_depart, salle_code_arrivee, date_depart, date_arrivee, duree, order_id) — a clean, per-trip record of where the robot went and how long it took, tied to a real delivery.
The raw recordings are relatively few, so they are augmented / synthesised into a larger, balanced training set (for example, interpolating across departure times and room pairs that share corridors) before fitting the LightGBM regressor. Training produces the model weights (les poids) that the prediction service loads to answer /predict.
As more deliveries are completed, new movements accumulate and the model is retrained on the larger set, closing the model feedback loop.
All of the sequencing ==logic== lives in app/Services/DeliveryPlannerService.php. There are two ideas: criticality first, then shortest predicted time within each criticality batch.
Orders carry an is_critical flag (set by the pharmacy manager). When a planning cycle starts (initializeBatchPlan):
active plan that runs first.queued plan that becomes active only once the critical batch is finished.So a life-critical medication is never delayed behind routine deliveries, even if the routine one is physically closer.
Within a batch, the order of stops is chosen by a greedy nearest-neighbour search that uses predicted travel time as the distance metric (computeSequence):
This is a classic heuristic for the travelling-salesman-style problem: at each step go to the stop that is predicted to be reached fastest from where the robot currently is. It is fast (no exhaustive search) and good enough for the handful of stops in a typical batch. The per-order estimates (predicted_minutes, sequence position, departure/arrival rooms) are stored in the plan's estimated_times and shown in the UI's "Séquence optimisée".
Because the metric is learned predicted time, not straight-line distance, the sequence automatically accounts for things like a corridor that is slow at 2pm
The plan is not computed once and frozen. As the robot reports progress through the digital-twin socket (navigating / arrived events handled by DtStatusService), the planner recomputes the remaining sequence from the robot's actual current room (recalculateRemainingSequence / plan):
activateNextQueuedPlan)This keeps the sequence optimal even if a delivery is added, cancelled, or takes longer than predicted.
| Concern | File |
|---|---|
| Sequencing & batching==logic== | app/Services/DeliveryPlannerService.php |
| Travel-time model client | app/Services/PredictionService.php |
| Model service URL | config/services.php → prediction.url |
| Room→robot command queue | app/Services/DtTaskQueueService.php |
| Status handling / re-plan trigger | app/Services/DtStatusService.php |
| Recording delivery timings | app/Http/Controllers/Api/DtWebhookController.php, app/Models/DtData.php |
| Ground-truth trip logs (ROS) | digital_twin/digital_twin/simulation_logger.py |
salle_pharmacie, salle_101, …) are the same registry entries used by the room interpreter — see navigation_==logic==.md and room_registry.yaml.