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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,5 @@ __marimo__/

# vscode
.vscode/

backup/core/model/modelisation.py
9 changes: 9 additions & 0 deletions data/solutions/Sol_MPVRP_S_005_s7_d1_p3.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
2: 1 - 1 [3088] - 1 (3088) - 1 [3847] - 3 (3847) - 1 [4467] - 6 (4467) - 1 [3635] - 5 (2341) - 4 (1294) - 1 [4038] - 7 (4038) - 1 [3708] - 4 (3708) - 1 [3333] - 1 (911) - 2 (2422) - 1
2: 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 0(16.1) - 0(16.1) - 0(16.1) - 0(16.1) - 0(16.1) - 2(62.4) - 2(62.4) - 2(62.4) - 2(62.4) - 2(62.4) - 2(62.4)

1
2
62.40
531.68
x86_64
593.913
169 changes: 114 additions & 55 deletions pages/static/js/visualisation.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ function parseDatSolution(text) {
const solution = {
routes: {},
depotLoads: {}, // Track loading quantities at depots
productLines: {},
segmentMeta: {},
metrics: {}
};

Expand All @@ -315,10 +317,19 @@ function parseDatSolution(text) {
productsLineRaw = productsLineRaw.replace(/^\s*\d+\s*:\s*/, '');
lineIdx++;

const parseProductState = (token) => {
const m = String(token).trim().match(/^(-?\d+)\s*(?:\(([-+]?\d*\.?\d+)\))?$/);
if (!m) return null;
const idx = parseInt(m[1], 10);
return Number.isNaN(idx) ? null : idx;
};

// Parse the route (split by " - ") and build segments
const routeParts = routeLine.split(' - ').map(p => p.trim());
const productStates = productsLineRaw.split(' - ').map(p => parseProductState(p));
const segments = [];
const vehicleLoads = []; // Track loads for this vehicle
const vehicleSegmentMeta = [];

const extractNodeInfo = (token, position, lastPosition) => {
// Token may be: "12", "12 [qty]", "12 (qty)", or typed "G2"/"D1"/"S5".
Expand All @@ -328,17 +339,21 @@ function parseDatSolution(text) {
// Extract quantity from brackets [qty] (depot load)
const bracketMatch = raw.match(/\[(\d+(?:\.\d+)?)\]/);
const loadQty = bracketMatch ? parseFloat(bracketMatch[1]) : 0;
// Extract quantity from parentheses (station delivery in route line)
const parenMatch = raw.match(/\(([-+]?\d*\.?\d+)\)/);
const deliveryQty = parenMatch ? parseFloat(parenMatch[1]) : 0;
Comment on lines 339 to +344
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

deliveryQty is defaulted to 0 when no parentheses are present, which makes it impossible to distinguish “no explicit delivery quantity provided” from an explicit “(0)” in the route. This later causes station deliveries to fall back to demand/visits even when the solution explicitly encodes a 0 delivery. Consider storing deliveryQty as null/undefined when no parentheses are present (or keep a separate hasDeliveryQty flag) so downstream logic can respect explicit zeros.

Copilot uses AI. Check for mistakes.

const typed = base.match(/^([GDS])(\d+)$/i);
if (typed) {
return {
id: `${typed[1].toUpperCase()}${parseInt(typed[2], 10)}`,
loadQty
loadQty,
deliveryQty
};
}

const numeric = base.match(/^(?:N)?(\d+)$/);
if (!numeric) return { id: null, loadQty: 0 };
if (!numeric) return { id: null, loadQty: 0, deliveryQty: 0 };
const n = parseInt(numeric[1], 10);

// New convention (no prefixes): infer by markers/position.
Expand All @@ -348,7 +363,7 @@ function parseDatSolution(text) {
else if (position === 0 || position === lastPosition) nodeId = `G#${n}`;
else nodeId = `G#${n}`;

return { id: nodeId, loadQty };
return { id: nodeId, loadQty, deliveryQty };
};

const lastPos = routeParts.length - 1;
Expand All @@ -361,6 +376,12 @@ function parseDatSolution(text) {

if (fromInfo.id && toInfo.id) {
segments.push([fromInfo.id, toInfo.id]);
vehicleSegmentMeta.push({
deliveryQty: toInfo.deliveryQty || 0,
loadQty: toInfo.loadQty || 0,
productRaw: productStates[i] ?? null,
nextProductRaw: productStates[i + 1] ?? null
});
// Track depot load if going to a depot with a load qty
if (toInfo.loadQty > 0) {
vehicleLoads.push({
Expand All @@ -374,6 +395,8 @@ function parseDatSolution(text) {

solution.routes[`V${vehicleId}`] = segments;
solution.depotLoads[`V${vehicleId}`] = vehicleLoads;
solution.productLines[`V${vehicleId}`] = productStates;
solution.segmentMeta[`V${vehicleId}`] = vehicleSegmentMeta;

// Skip empty line separators
while (lineIdx < lines.length && lines[lineIdx].trim() === '') {
Expand Down Expand Up @@ -450,6 +473,32 @@ function setTextContentById(id, value) {
el.textContent = value;
}

function detectProductIndexingBase(productLines, numProducts) {
const allRaw = Object.values(productLines || {})
.flat()
.filter(v => Number.isInteger(v) && v >= 0);

if (allRaw.length === 0) return 'zero';
if (allRaw.includes(0)) return 'zero';

const maxRaw = Math.max(...allRaw);
if (maxRaw === numProducts) return 'one';
if (maxRaw < numProducts) return 'zero';
return 'one';
}

function normalizeProductIndex(rawProduct, base, numProducts) {
if (!Number.isInteger(rawProduct) || numProducts <= 0) return null;

if (base === 'one') {
if (rawProduct < 1 || rawProduct > numProducts) return null;
return rawProduct - 1;
}

if (rawProduct < 0 || rawProduct >= numProducts) return null;
return rawProduct;
}

function updateFileStatus(type, filename) {
const statusEl = document.getElementById(type + 'Status');
const zoneEl = document.getElementById(type + 'Zone');
Expand Down Expand Up @@ -1033,79 +1082,74 @@ function calculateCurrentExchangesAndDeliveries() {
});

const numProducts = instance.num_products || 1;
const numGarages = instance.num_garages || 1;
const numDepots = instance.num_depots || 2;
const numStations = instance.num_stations || 5;

// Parse product lines from solution if available
const productLines = solution.productLines || {};
const segmentMeta = solution.segmentMeta || {};
const productBase = detectProductIndexingBase(productLines, numProducts);

trucks.forEach((t, truckIdx) => {
const vehicleKey = t.id;
const completedSegs = Math.floor(Math.min(progress, t.segments.length));
const vehicleProducts = productLines[vehicleKey] || [];
const vehicleMeta = segmentMeta[vehicleKey] || [];

let lastProduct = null;

for (let i = 0; i < completedSegs; i++) {
const toNode = t.segments[i][1];

// Check for product changes (exchanges)
// In absence of detailed product tracking, estimate based on segment transitions
// A product change typically happens when visiting different types of stations

// Track deliveries to stations
const segMeta = vehicleMeta[i] || {};
const fromProduct = normalizeProductIndex(
vehicleProducts[i] ?? segMeta.productRaw,
productBase,
numProducts
);
const toProduct = normalizeProductIndex(
vehicleProducts[i + 1] ?? segMeta.nextProductRaw,
productBase,
numProducts
);
const activeProduct = fromProduct ?? toProduct;

// Track station deliveries for the single active product of that segment.
if (toNode && toNode.startsWith('S')) {
const stationId = toNode;
// Simulate delivery - distribute evenly across products for now
// In a real implementation, this would use the solution's product line
if (stationDeliveriesPerProduct[stationId]) {
if (stationDeliveriesPerProduct[stationId] && activeProduct !== null) {
const stationDemand = stationDemandsPerProduct[stationId] || [];
stationDemand.forEach((demand, pIdx) => {
if (demand > 0) {
// Calculate how much should be delivered per visit
const totalVisits = stationVisits[stationId] || 1;
const deliveryPerVisit = demand / totalVisits;
stationDeliveriesPerProduct[stationId][pIdx] += deliveryPerVisit;
}
});
const demandForProduct = stationDemand[activeProduct] || 0;
if (demandForProduct > 0) {
const explicitDelivery = Number(segMeta.deliveryQty || 0);
const deliveryQty = explicitDelivery > 0
Comment on lines +1119 to +1120
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

explicitDelivery > 0 treats an explicit “(0)” delivery the same as “no explicit delivery provided”, which will over-report deliveries by falling back to demand/visits. Use a sentinel (e.g., null) or an explicit flag from parsing so that explicit zeros result in a 0 delivery rather than triggering the fallback.

Suggested change
const explicitDelivery = Number(segMeta.deliveryQty || 0);
const deliveryQty = explicitDelivery > 0
let explicitDelivery = null;
if (segMeta.deliveryQty !== undefined && segMeta.deliveryQty !== null && segMeta.deliveryQty !== '') {
const parsedDelivery = Number(segMeta.deliveryQty);
if (!Number.isNaN(parsedDelivery)) {
explicitDelivery = parsedDelivery;
}
}
const deliveryQty = explicitDelivery !== null

Copilot uses AI. Check for mistakes.
? explicitDelivery
: (demandForProduct / Math.max(1, stationVisits[stationId] || 1));
stationDeliveriesPerProduct[stationId][activeProduct] += deliveryQty;
}
}
}
}

// Estimate exchanges: count transitions between depot visits
// (simplified - actual exchanges depend on product line in solution)
let depotVisitCount = 0;
let lastDepotProduct = lastProductByTruck[vehicleKey] || 1;

for (let i = 0; i < completedSegs; i++) {
const toNode = t.segments[i][1];
// Count exchanges only when product actually changes across a depot segment.
if (toNode && toNode.startsWith('D')) {
depotVisitCount++;
if (depotVisitCount > 1) {
// Potential product change
if (fromProduct !== null && toProduct !== null && fromProduct !== toProduct) {
currentExchanges++;

// Calculate a simulated product change (cycling through products)
const newProduct = ((lastDepotProduct % numProducts) + 1);
const swapKey = `${vehicleKey}-${i}`;

// Show notification if this swap hasn't been shown yet
if (!shownSwapNotifications[swapKey]) {
shownSwapNotifications[swapKey] = true;
showSwapNotification(vehicleKey, lastDepotProduct, newProduct, t.color);
showSwapNotification(vehicleKey, fromProduct + 1, toProduct + 1, t.color);
}

lastDepotProduct = newProduct;
}
}

if (activeProduct !== null) {
lastProduct = activeProduct;
}
}

// Update last product tracking for this truck
lastProductByTruck[vehicleKey] = lastDepotProduct;
lastProductByTruck[vehicleKey] = lastProduct;
});

// Cap exchanges at total (estimation may overshoot)
currentExchanges = Math.min(currentExchanges, totalExchanges);
if (Number.isFinite(totalExchanges) && totalExchanges > 0) {
currentExchanges = Math.min(currentExchanges, totalExchanges);
}
}

// ═══════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -1265,6 +1309,9 @@ function updateDepotInventoryPanel() {

const numProducts = instance.num_products || 0;
const depotSupplies = instance.depotSupplies || {};
const productLines = solution.productLines || {};
const segmentMeta = solution.segmentMeta || {};
const productBase = detectProductIndexingBase(productLines, numProducts);

if (Object.keys(depotSupplies).length === 0) {
panel.innerHTML = '<div class="depot-placeholder">Load an instance</div>';
Expand Down Expand Up @@ -1292,6 +1339,8 @@ function updateDepotInventoryPanel() {
trucks.forEach((t, truckIdx) => {
const vehicleKey = t.id;
const vehicleLoads = solution.depotLoads?.[vehicleKey] || [];
const vehicleProducts = productLines[vehicleKey] || [];
const vehicleMeta = segmentMeta[vehicleKey] || [];
const completedSegs = Math.floor(Math.min(progress, t.segments.length));

for (let i = 0; i < completedSegs; i++) {
Expand All @@ -1309,15 +1358,25 @@ function updateDepotInventoryPanel() {
const loadInfo = vehicleLoads.find(l => l.segmentIdx === i);

if (loadInfo && loadInfo.quantity > 0) {
// We have actual load data - subtract from ALL products proportionally
// (simplified: in real scenario we'd track which product)
// For now, distribute withdrawal across products based on their ratios
const totalSupply = depotSupplies[depotId].reduce((a, b) => a + b, 0);
for (let p = 0; p < currentInventory[depotId].length; p++) {
const ratio = totalSupply > 0 ? depotSupplies[depotId][p] / totalSupply : 1 / numProducts;
const withdrawal = loadInfo.quantity * ratio;
currentInventory[depotId][p] -= withdrawal;
depotWithdrawalsTotal[depotId][p] += withdrawal;
const segMetaInfo = vehicleMeta[i] || {};
const loadedProduct = normalizeProductIndex(
vehicleProducts[i + 1] ?? segMetaInfo.nextProductRaw,
productBase,
numProducts
);

if (loadedProduct !== null && currentInventory[depotId][loadedProduct] !== undefined) {
currentInventory[depotId][loadedProduct] -= loadInfo.quantity;
depotWithdrawalsTotal[depotId][loadedProduct] += loadInfo.quantity;
} else {
// Backward-compatible fallback for malformed/missing product lines.
const totalSupply = depotSupplies[depotId].reduce((a, b) => a + b, 0);
for (let p = 0; p < currentInventory[depotId].length; p++) {
const ratio = totalSupply > 0 ? depotSupplies[depotId][p] / totalSupply : 1 / Math.max(1, numProducts);
const withdrawal = loadInfo.quantity * ratio;
currentInventory[depotId][p] -= withdrawal;
depotWithdrawalsTotal[depotId][p] += withdrawal;
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion pages/visualisation.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ <h2><a href="../index.html" style="text-decoration: none; color: inherit;">MPVRP
</div>
<div class="metric">
<span class="metric-value" id="stat-routing">0.00</span>
<span class="metric-label">Routing Cost</span>
<span class="metric-label">Changes Cost</span>
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The label was changed to “Changes Cost”, but stat-routing is still populated from metrics.routing_cost in visualisation.js. Please align the label/element id with the metric being displayed (either restore “Routing Cost” or update the JS + metric naming so this field truly shows the changes cost).

Suggested change
<span class="metric-label">Changes Cost</span>
<span class="metric-label">Routing Cost</span>

Copilot uses AI. Check for mistakes.
</div>
<!--<div class="metric">
<span class="metric-value" id="stat-exchanges">0</span>
Expand Down
Loading