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
18 changes: 18 additions & 0 deletions data/solutions/Sol_MPVRP_S_002_s10_d2_p3.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
2: 1 - 2 [2184] - 7 (1280) - 2 [760] - 4 (657) - 3 (1007) - 1 [2183] - 4 (1527) - 1 [1017] - 8 (1673) - 1 [1107] - 10 (1107) - 1 [2184] - 8 (2184) - 2 [658] - 4 (657) - 6 (1) - 2 [2184] - 6 (2184) - 1 [2100] - 6 (2100) - 1
2: 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 2(0.0) - 0(40.0)

3: 1 - 2 [3] - 6 (3) - 1 [1] - 1 (1) - 2 [2402] - 1 (417) - 1 [1042] - 5 (1) - 1 [602] - 5 (3628) - 1
3: 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0)

4: 1 - 1 [1] - 1 (1) - 2 [4144] - 1 (1) - 8 (588) - 1 [1] - 9 (2421) - 1 [1] - 10 (1135) - 1 (1) - 1 [4144] - 10 (2060) - 1 [1021] - 6 (3105) - 1 [4144] - 1 (4144) - 1
4: 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0)

5: 1 - 2 [2505] - 10 (1825) - 7 (680) - 2 [2504] - 9 (2504) - 2 [2326] - 3 (2326) - 1 [2505] - 3 (2505) - 1 [2505] - 2 (2505) - 2 [2505] - 10 (2505) - 1 [2505] - 7 (1529) - 2 (976) - 1 [2505] - 7 (2505) - 1 [2007] - 9 (1852) - 7 (155) - 1
5: 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 0(49.1)

4
2
89.10
3567.09
Intel® Core™ i7-10850H × 12
57.73
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
189 changes: 127 additions & 62 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;

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 @@ -444,6 +467,38 @@ function mapNodeNumber(nodeStr, numGarages, numDepots, numStations) {
}
}

function setTextContentById(id, value) {
const el = document.getElementById(id);
if (!el) return;
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';
Comment on lines +481 to +486
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.

detectProductIndexingBase can mis-detect 1-based product IDs when the solution never uses the highest-numbered product (e.g., numProducts=3 but only products 1 and 2 appear ⇒ maxRaw < numProducts returns 'zero'). That would shift product indices and break per-product delivery/inventory calculations. Consider basing detection primarily on the presence of 0 vs numProducts (and treat the “ambiguous” case where values are within 1..numProducts-1 consistently, e.g., default to 1-based or add an explicit flag in the file format).

Suggested change
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';
// No product IDs found: keep existing behavior and default to zero-based.
if (allRaw.length === 0) return 'zero';
const hasZero = allRaw.includes(0);
const hasMaxId = Number.isInteger(numProducts) && numProducts > 0
? allRaw.includes(numProducts)
: false;
// If 0 is present, we have clear evidence of zero-based indexing.
if (hasZero) return 'zero';
// If the highest valid product ID (numProducts) appears, this is 1-based.
if (hasMaxId) return 'one';
// Ambiguous case: all product IDs lie in 1..numProducts-1 (no 0, no numProducts).
// Default to 1-based indexing to avoid mistakenly shifting valid 1-based IDs.

Copilot uses AI. Check for mistakes.
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 @@ -533,14 +588,14 @@ function initData() {

// Update Stats
const metrics = solution.metrics || {};
document.getElementById('stat-dist').textContent = (metrics.total_cost || solution.objective || 0).toFixed(2);
document.getElementById('stat-routing').textContent = (metrics.routing_cost || 0).toFixed(2);
document.getElementById('stat-exchanges').textContent = `0/${totalExchanges}`;
document.getElementById('stat-trucks').textContent = metrics.vehicles_used || trucks.length;
setTextContentById('stat-dist', (metrics.total_cost || solution.objective || 0).toFixed(2));
setTextContentById('stat-routing', (metrics.routing_cost || 0).toFixed(2));
// setTextContentById('stat-exchanges', `0/${totalExchanges}`);
setTextContentById('stat-trucks', metrics.vehicles_used || trucks.length);

let totalSegs = trucks.reduce((sum, t) => sum + t.segments.length, 0);
document.getElementById('stat-segments').textContent = totalSegs;
document.getElementById('stat-status').textContent = solution.status || 'Loaded';
setTextContentById('stat-segments', totalSegs);
setTextContentById('stat-status', solution.status || 'Loaded');

// Update Fleet Legend
const legendEl = document.getElementById('fleet-legend');
Expand Down Expand Up @@ -1011,7 +1066,7 @@ function updateUI() {
calculateCurrentExchangesAndDeliveries();

// Update exchanges display
document.getElementById('stat-exchanges').textContent = `${currentExchanges}/${totalExchanges}`;
// document.getElementById('stat-exchanges').textContent = `${currentExchanges}/${totalExchanges}`;

// Update depot inventory panel
updateDepotInventoryPanel();
Expand All @@ -1027,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
? explicitDelivery
Comment on lines +1119 to +1121
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 as “missing” and falls back to the demand/visits estimate, which makes routes that intentionally deliver 0 appear to deliver a positive amount (see existing sample solutions with (0) quantities). Consider preserving the distinction between “no delivery specified” vs “delivery specified as 0” (e.g., have parseDatSolution store deliveryQty: null when no parentheses are present, and here check segMeta.deliveryQty !== null to decide whether to use it).

Suggested change
const explicitDelivery = Number(segMeta.deliveryQty || 0);
const deliveryQty = explicitDelivery > 0
? explicitDelivery
const hasExplicitDelivery =
segMeta.deliveryQty !== null && segMeta.deliveryQty !== undefined;
const deliveryQty = hasExplicitDelivery
? (Number(segMeta.deliveryQty) || 0)

Copilot uses AI. Check for mistakes.
: (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 @@ -1259,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 @@ -1286,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 @@ -1303,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
8 changes: 6 additions & 2 deletions pages/visualisation.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ <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>
</div>
<div class="metric">
<!--<div class="metric">
<span class="metric-value" id="stat-exchanges">0</span>
<span class="metric-label">Exchanges</span>
</div>-->
<div class="metric">
<span class="metric-value" id="stat-trucks">0</span>
<span class="metric-label">Trucks</span>
</div>
<div class="metric">
<span class="metric-value" id="stat-segments">0</span>
Expand Down
Loading