diff --git a/.gitignore b/.gitignore index d3e3039..1e3b226 100644 --- a/.gitignore +++ b/.gitignore @@ -211,3 +211,5 @@ __marimo__/ # vscode .vscode/ + +backup/core/model/modelisation.py diff --git a/data/solutions/Sol_MPVRP_S_005_s7_d1_p3.dat b/data/solutions/Sol_MPVRP_S_005_s7_d1_p3.dat new file mode 100644 index 0000000..4b5d5bf --- /dev/null +++ b/data/solutions/Sol_MPVRP_S_005_s7_d1_p3.dat @@ -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 \ No newline at end of file diff --git a/pages/static/js/visualisation.js b/pages/static/js/visualisation.js index cefda5b..b1184e2 100644 --- a/pages/static/js/visualisation.js +++ b/pages/static/js/visualisation.js @@ -289,6 +289,8 @@ function parseDatSolution(text) { const solution = { routes: {}, depotLoads: {}, // Track loading quantities at depots + productLines: {}, + segmentMeta: {}, metrics: {} }; @@ -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". @@ -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. @@ -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; @@ -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({ @@ -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() === '') { @@ -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'); @@ -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 + ? 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); + } } // ═══════════════════════════════════════════════════════════════ @@ -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 = '