diff --git a/README.md b/README.md index 507f921..152bb5d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ Research and competition platform for the **Multi-Product Vehicle Routing Problem with Split Deliveries and Changeover Costs**. +In this benchmark, a changeover cost represents the operational preparation +associated with loading a product. It may apply before the first trip as well +as between successive trips; it is not limited to the act of switching from +one product to another. + The repository deliberately separates two deployable surfaces: - the static GitHub Pages frontend at the repository root and in `pages/`; @@ -20,8 +25,14 @@ The interactive route visualizer remains a standalone canvas application in demands, but replaces every transition cost by zero. The official score is the sum of `distance_total + total_switch_cost` across the -150 original-cost instances. A missing or infeasible solution receives a penalty -of `100000`. +150 original-cost instances. A ZIP submission may contain any subset of those +solutions. Every recognized file is evaluated independently; missing, unresolved, +unreadable and infeasible solutions all receive the same penalty of `100000`. + +A vehicle may visit a station at most once for a given product across its entire +schedule. It may return to the same station on another trip only when serving a +different product. Split deliveries for one station-product pair must therefore +be shared between distinct vehicles. See [`docs/problem.md`](docs/problem.md), [`docs/instance_format.md`](docs/instance_format.md), and @@ -36,7 +47,7 @@ backend/ core/model/ instance/solution parsing and strict feasibility checks core/scoring/ secure ZIP ingestion and official evaluation core/experiments/ paired scenarios and ex-post changeover repricing - database/ Notion persistence adapter + database/ participant and scoreboard persistence data/instances/ paired benchmark datasets docs/ Markdown sources used by the static documentation pages pages/ GitHub Pages UI and JavaScript clients @@ -61,14 +72,8 @@ Configure deployments with: FRONTEND_DEV_URL=http://127.0.0.1:5500 FRONTEND_PROD_URL=https://your-org.github.io FRONTEND_PROD_URL_2=https://your-custom-domain.example -NOTION_TOKEN=secret_... -NOTION_DATABASE_ID=... -NOTION_DATA_SOURCE_ID=... ``` -Notion remains the source of truth for participant score, feasible-solution -count, submission date and rank. - ## Static frontend The frontend has no build requirement. Serve the repository root with any static @@ -96,7 +101,7 @@ uv run pytest ``` The suite validates the API, strict solution checks, generator, ZIP safety, -Notion adapter and all 150 paired benchmark files. +scoreboard persistence and all 150 paired benchmark files. ## Docker diff --git a/backend/README.md b/backend/README.md index cb0ec64..5a4321e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -7,7 +7,7 @@ This directory is the complete server-side boundary of MPVRP-CC. - `core/model/` owns canonical parsers and solution feasibility. - `core/scoring/` evaluates submitted archives against official instances. - `core/experiments/` creates and reprices paired cost scenarios. -- `database/` is the Notion persistence adapter. +- `database/` stores participant and scoreboard information. -Domain code does not depend on the static frontend. The frontend communicates -with it only through the routes declared in `app/main.py`. +The server and the static website remain independent. They communicate through +the public HTTP API. diff --git a/backend/app/routes/model.py b/backend/app/routes/model.py index 08bfbb7..314a8dd 100644 --- a/backend/app/routes/model.py +++ b/backend/app/routes/model.py @@ -25,7 +25,7 @@ async def verify_solution_endpoint( - Weight maintenance (quantity loaded = quantity delivered) - Meeting the demand of all stations - Compliance with depot stock levels - - Metric validation + - Automatic recomputation of distance and transition metrics Returns: diff --git a/backend/core/model/feasibility.py b/backend/core/model/feasibility.py index 3546836..380d4d1 100644 --- a/backend/core/model/feasibility.py +++ b/backend/core/model/feasibility.py @@ -12,7 +12,18 @@ METRIC_TOLERANCE = 0.2 -def verify_solution(instance: Instance, solution: ParsedSolutionDat) -> tuple[list[str], dict[str, Any]]: +def verify_solution( + instance: Instance, + solution: ParsedSolutionDat, + *, + check_reported_metrics: bool = False, +) -> tuple[list[str], dict[str, Any]]: + """Check route feasibility and recompute all performance metrics. + + Values reported in the solution file are informative by default: rounding + or stale summary values must not invalidate an otherwise feasible route. + They can still be audited explicitly with ``check_reported_metrics=True``. + """ errors: list[str] = [] vehicles = {int(key[1:]): value for key, value in instance.camions.items()} depots = {int(key[1:]): value for key, value in instance.depots.items()} @@ -44,7 +55,9 @@ def verify_solution(instance: Instance, solution: ParsedSolutionDat) -> tuple[li ) continue keys = [solution_node_key(node["kind"], node["id"]) for node in route.nodes] - expected_garage = vehicle.garage_id + expected_garage = str(vehicle.garage_id) + if not expected_garage.startswith("G"): + expected_garage = f"G{expected_garage}" if not keys: errors.append(f"Vehicle {vehicle_id}: empty route") continue @@ -145,7 +158,7 @@ def verify_solution(instance: Instance, solution: ParsedSolutionDat) -> tuple[li trip_has_station = True expected_cumulative = float(cumulative_costs[index]) - if abs(expected_cumulative - cumulative) > METRIC_TOLERANCE: + if check_reported_metrics and abs(expected_cumulative - cumulative) > METRIC_TOLERANCE: errors.append( f"Vehicle {vehicle_id}: cumulative changeover cost at step {index + 1} " f"is {expected_cumulative}, expected {cumulative:.2f}" @@ -177,10 +190,11 @@ def verify_solution(instance: Instance, solution: ParsedSolutionDat) -> tuple[li "total_switch_cost": total_switch_cost, "distance_total": total_distance, } - _check_metric(errors, solution.metrics, computed, "used_vehicles", 0) - _check_metric(errors, solution.metrics, computed, "total_changes", 0) - _check_metric(errors, solution.metrics, computed, "total_switch_cost", METRIC_TOLERANCE) - _check_metric(errors, solution.metrics, computed, "distance_total", METRIC_TOLERANCE) + if check_reported_metrics: + _check_metric(errors, solution.metrics, computed, "used_vehicles", 0) + _check_metric(errors, solution.metrics, computed, "total_changes", 0) + _check_metric(errors, solution.metrics, computed, "total_switch_cost", METRIC_TOLERANCE) + _check_metric(errors, solution.metrics, computed, "distance_total", METRIC_TOLERANCE) return errors, computed diff --git a/backend/core/model/modelisation.py b/backend/core/model/modelisation.py index d61659e..eaad73d 100644 --- a/backend/core/model/modelisation.py +++ b/backend/core/model/modelisation.py @@ -33,7 +33,8 @@ def __init__(self, instance: Instance, max_positions: int = None, time_limit: in self.C = {c.id : c.capacity for c in instance.camions.values()} self.g_k = {c.id : c.garage_id for c in instance.camions.values()} - self.p_initial = {c.id : c.initial_product for c in instance.camions.values()} + # The optimization model uses products 1..P while domain objects use 0..P-1. + self.p_initial = {c.id: c.initial_product + 1 for c in instance.camions.values()} # IMPORTANT: dans `utils.parse_instance`, les clés produits des demandes/stocks sont 0..(P-1) # alors que le modèle utilise 1..P. On décale donc de +1 ici pour rester cohérent. @@ -502,11 +503,11 @@ def _val(var) -> float: # garage de départ line1_parts.append(f"{mapping.get(garage_str, 0)}") - # On aligne la ligne produit sur le produit de la 1ère mini-tournée (format README) - first_tour_product_export = (k_tours[0]["product"] - 1) if k_tours else self.p_initial[k] - line2_parts.append(f"{first_tour_product_export}({current_cumul_cost:.1f})") + # The departure garage records the vehicle's initial configuration. + initial_product_export = self.p_initial[k] - 1 + line2_parts.append(f"{initial_product_export}({current_cumul_cost:.1f})") - last_product_export = first_tour_product_export + last_product_export = initial_product_export last_station = None last_t = None @@ -590,4 +591,4 @@ def _val(var) -> float: solver = Solver(instance) solver.solve() print(solver.solution) - solver.export_solution(solution_path) \ No newline at end of file + solver.export_solution(solution_path) diff --git a/backend/core/model/utils.py b/backend/core/model/utils.py index 939e136..eacf609 100644 --- a/backend/core/model/utils.py +++ b/backend/core/model/utils.py @@ -161,14 +161,14 @@ def _parse_solution_route_token(token: str) -> Dict[str, Any]: # Dépôt : format id[qty] left, right = token.split("[", 1) node_id = int(left.strip()) - qty = int(right.split("]", 1)[0].strip()) + qty = float(right.split("]", 1)[0].strip()) return {"kind": "depot", "id": node_id, "qty": qty} if "(" in token and ")" in token: # Station : format id(qty) left, right = token.split("(", 1) node_id = int(left.strip()) - qty = int(right.split(")", 1)[0].strip()) + qty = float(right.split(")", 1)[0].strip()) return {"kind": "station", "id": node_id, "qty": qty} # Garage : format id (pas de quantité) @@ -187,6 +187,11 @@ def _parse_solution_product_token(token: str) -> Tuple[int, float]: token = token.strip() if not token: raise ValueError("Empty token") + if "(" not in token and ")" not in token: + try: + return int(token), 0.0 + except ValueError as exc: + raise ValueError(f"Invalid product token: {token}") from exc if "(" not in token or ")" not in token: raise ValueError(f"Invalid product token: {token}") p_str, rest = token.split("(", 1) @@ -275,6 +280,18 @@ def _is_vehicle_line(line: str) -> bool: prod_tokens = [t for t in prod_tokens if t] products = [_parse_solution_product_token(t) for t in prod_tokens] + # Historical benchmark solutions omit the product entry associated + # with the final return to the garage. The vehicle keeps the product + # configuration and cumulative cost of the preceding step there, so + # expand that compact representation to the canonical node-aligned + # form used by the feasibility checker. + if ( + len(products) == len(nodes) - 1 + and products + and nodes[-1]["kind"] == "garage" + ): + products.append(products[-1]) + vehicles.append(ParsedSolutionVehicle(vehicle_id=vehicle_id, nodes=nodes, products=products)) i += 2 @@ -284,16 +301,22 @@ def _is_vehicle_line(line: str) -> bool: # Les lignes non vides restantes contiennent les métriques (6 lignes) metrics_lines = [l.strip() for l in raw_lines[i:] if l.strip()] - if len(metrics_lines) != 6: - raise ValueError(f"Expected 6 metric lines, got {len(metrics_lines)}") - - metrics = { - "used_vehicles": int(metrics_lines[0]), - "total_changes": int(metrics_lines[1]), - "total_switch_cost": float(metrics_lines[2]), - "distance_total": float(metrics_lines[3]), - "processor": metrics_lines[4], - "time": float(metrics_lines[5]), - } + if len(metrics_lines) not in {0, 4, 5, 6}: + raise ValueError( + "The optional summary must be omitted or contain at least its four numeric metrics" + ) + + metrics: dict[str, Any] = {} + if metrics_lines: + metrics.update({ + "used_vehicles": int(metrics_lines[0]), + "total_changes": int(metrics_lines[1]), + "total_switch_cost": float(metrics_lines[2]), + "distance_total": float(metrics_lines[3]), + }) + if len(metrics_lines) >= 5: + metrics["processor"] = metrics_lines[4] + if len(metrics_lines) == 6: + metrics["time"] = float(metrics_lines[5]) return ParsedSolutionDat(vehicles=vehicles, metrics=metrics) diff --git a/docs/instance_format.md b/docs/instance_format.md index cd5c577..b713c05 100644 --- a/docs/instance_format.md +++ b/docs/instance_format.md @@ -1,8 +1,8 @@ -# Instance Format Specification +# Instance File Format -## 1. Filename +## 1. File name -Each paired scenario uses the same filename: +Every benchmark instance follows this naming pattern: ```text MPVRP_A_sB_dC_pD.dat @@ -10,82 +10,60 @@ MPVRP_A_sB_dC_pD.dat | Field | Meaning | | --- | --- | -| `A` | Instance number, from `001` to `150` for the benchmark | +| `A` | Instance number, from `001` to `150` | | `B` | Number of service stations | | `C` | Number of depots | | `D` | Number of products | -Example: +For example: ```text MPVRP_001_s48_d1_p1.dat ``` -The repository contains two directories with a one-to-one filename mapping: +Each file has a matching counterpart in both benchmark scenarios. The version with changeover costs is used for the official evaluation. The zero-cost version contains the same fleet, locations, stocks, demands, and identifier, but all values in its transition matrix are zero. -- `with_changeover_costs`: official instances used by the evaluator; -- `without_changeover_costs`: comparative twins whose transition matrices contain only zeroes. +## 2. General structure -All other data, including the UUID, fleet, coordinates, stocks and demands, is identical inside a pair. - -## 2. Parser Rules - -The LP parser tokenizes the complete file. To stay compatible: - -- The first non-empty line must be the UUID comment line. -- Do not add any other comment line anywhere in the file. -- Blank lines should be avoided. -- Values may be separated by spaces or tabs. -- Entity IDs are one-based and contiguous: `1, ..., n`. -- Product IDs are one-based: `1, ..., NbProducts`. - -## 3. File Blocks - -The block order is fixed: +An instance is a plain-text file. Its sections always appear in this order: ```text # NbProducts NbDepots NbGarages NbStations NbVehicles - - - - - + + + + + ``` -The expected number of data lines after the UUID line is: +Values may be separated by spaces or tabs. Identifiers begin at `1` and must remain consecutive within each category. Apart from the identifier on the first line, the file should not contain comments or blank lines. -```text -1 + NbProducts + NbVehicles + NbDepots + NbGarages + NbStations -``` - -## 4. UUID +## 3. Instance identifier -Line 1 contains a UUID comment: +The first line contains the unique identifier shared by the two versions of an instance: ```text # c01ab718-9a2c-4a7d-bb95-f37e2a389409 ``` -This line is mandatory for compatibility with `MPVRPInstance.read()`. - -## 5. Global Parameters +## 4. Main dimensions -Line 2 contains five positive integers: +The second line gives the number of products, depots, garages, stations, and vehicles: ```text NbProducts NbDepots NbGarages NbStations NbVehicles ``` -Example: +For example, the following line describes an instance with 3 products, 2 depots, 1 garage, 20 stations, and 5 vehicles: ```text 3 2 1 20 5 ``` -## 6. Transition Cost Matrix +## 5. Transition cost matrix -Next come `NbProducts` rows, each with `NbProducts` numeric values: +The next `NbProducts` lines form a square matrix: ```text Cost_P1_to_P1 Cost_P1_to_P2 ... @@ -93,73 +71,58 @@ Cost_P2_to_P1 Cost_P2_to_P2 ... ... ``` -Requirements: +The value on row `p` and column `q` is the operational cost of preparing the vehicle to load product `q` when its current configuration is product `p`. This cost may include the loading setup itself; it is not limited to cleaning or changing the product. -- Costs must be finite and non-negative. -- The diagonal must be zero. -- The matrix may be asymmetric. The solver uses - `cost[previous_product - 1][next_product - 1]`. +All values must be finite and non-negative. The matrix may be asymmetric because the preparation required from `p` to `q` can differ from the preparation required from `q` to `p`. In the benchmark instances, the diagonal is zero, so loading the same product again adds no transition cost. -## 7. Vehicles +## 6. Vehicles -Next come `NbVehicles` rows: +Each vehicle is described on one line: ```text ID Capacity HomeGarage InitialProduct ``` -Requirements: +- `ID` identifies the vehicle. +- `Capacity` is the maximum quantity it can carry and must be positive. +- `HomeGarage` identifies the garage where its schedule starts and ends. +- `InitialProduct` gives its product configuration before the first loading. -- `ID` must be unique and contiguous in `[1, NbVehicles]`. -- `Capacity` must be strictly positive. -- `HomeGarage` must reference an existing garage ID. -- `InitialProduct` must be in `[1, NbProducts]`. +The initial product is important because the first loading cost is read from the transition matrix using this configuration as the starting point. -## 8. Depots +## 7. Depots -Next come `NbDepots` rows: +Each depot is described as follows: ```text ID X Y Stock_P1 Stock_P2 ... Stock_Pn ``` -Requirements: +`X` and `Y` are its coordinates. The remaining values give the available stock of each product. Stocks must be finite and non-negative, and the total stock of every product across all depots must cover total demand. -- `ID` must be unique and contiguous in `[1, NbDepots]`. -- Coordinates must be finite. -- Stocks must be finite and non-negative. -- For each product, total depot stock must be at least total station demand. +## 8. Garages -## 9. Garages - -Next come `NbGarages` rows: +Each garage has an identifier and a position: ```text ID X Y ``` -Requirements: - -- `ID` must be unique and contiguous in `[1, NbGarages]`. -- Coordinates must be finite. +Coordinates must be finite. -## 10. Service Stations +## 9. Service stations -Next come `NbStations` rows: +Each station is described by: ```text ID X Y Demand_P1 Demand_P2 ... Demand_Pn ``` -Requirements: +Demand values must be finite and non-negative, and every station must request at least one product. Deliveries may be split between vehicles, but each station-product demand must be fully satisfied. -- `ID` must be unique and contiguous in `[1, NbStations]`. -- Coordinates must be finite. -- Demands must be finite and non-negative. -- Each station must have at least one positive demand. -- For LP compatibility, each station/product demand must not exceed the sum of all vehicle capacities. The LP allows split delivery across vehicles, but it limits a vehicle to at most one visit for the same station/product pair. +A vehicle may serve a station only once for the same product over its complete schedule. It may return to that station on another trip to deliver a different product. Consequently, when a station-product demand is split, each contributing share must be assigned to a different vehicle. -## 11. Complete Example +## 10. Complete example ```text # c01ab718-9a2c-4a7d-bb95-f37e2a389409 @@ -176,4 +139,4 @@ Requirements: 3 56.7 31.3 0 2319 ``` -This instance has 2 products, 1 depot, 2 garages, 3 stations, and 2 vehicles. +This instance contains 2 products, 1 depot, 2 garages, 3 stations, and 2 vehicles. Vehicle 1 initially carries product 1, while vehicle 2 initially carries product 2. diff --git a/docs/instance_format.pdf b/docs/instance_format.pdf index cc19ee8..f58e1ae 100644 Binary files a/docs/instance_format.pdf and b/docs/instance_format.pdf differ diff --git a/docs/lp_model.tex b/docs/lp_model.tex index c992d84..6239664 100644 --- a/docs/lp_model.tex +++ b/docs/lp_model.tex @@ -59,23 +59,21 @@ \maketitle \begin{abstract} -This document states the mixed-integer linear programming model implemented in -\texttt{mpvrp\_cc/optimization/milp\_solver.py} for the Multi-Product Vehicle Routing Problem with Split -Deliveries and Changeover Costs (MPVRP-CC). Each vehicle may perform a bounded +This document presents a mixed-integer linear programming formulation for the +Multi-Product Vehicle Routing Problem with Split Deliveries and Changeover +Costs (MPVRP-CC). Each vehicle may perform a bounded number of mini-routes. A mini-route starts at one depot, loads one product, visits one or more stations, and ends at one depot. The model minimizes travel distance, empty depot transfers, garage access and return distance, and product -changeover costs. The formulation also enforces depot stock limits and prevents +preparation and loading-transition costs. The formulation also enforces depot stock limits and prevents the same vehicle from visiting the same station more than once for the same product. \end{abstract} \section{Problem Setting} -Let \(R\) be the maximum number of mini-routes allowed for each vehicle. In the -implementation, this value is the argument -\texttt{max\_trips\_per\_vehicle}. If it is not provided, the code computes the -lower bound +Let \(R\) be the maximum number of mini-routes allowed for each vehicle. A +natural lower bound is \[ R = \left\lceil @@ -87,9 +85,9 @@ \section{Problem Setting} is the capacity of vehicle \(k\). The model allows split deliveries. Therefore, the demand of a station for a -product may be served by several vehicles or by several trips, subject to the -additional rule that one vehicle cannot visit the same station twice for the -same product. +product may be shared by several vehicles. A given vehicle can visit a station +at most once for that product over its complete schedule. The same vehicle may +return to the station on another trip when it is carrying a different product. \section{Sets and Indices} @@ -126,7 +124,7 @@ \section{Parameters} \(B_{dp}\) & Stock of product \(p\) available at depot \(d\). \\ \(g(k)\) & Home garage of vehicle \(k\). \\ \(p^0_k\) & Initial product configuration of vehicle \(k\). \\ -\(C_{pp'}\) & Changeover cost from product \(p\) to product \(p'\). \\ +\(C_{pp'}\) & Preparation and loading-transition cost from configuration \(p\) to loaded product \(p'\). \\ \(\Delta_{ij}\) & Distance between nodes \(i\) and \(j\). \\ \(M=|S|\) & Big-\(M\) value used for station ordering. \\ \bottomrule @@ -178,10 +176,10 @@ \section{Objective Function} && \text{empty transfer between depots} \nonumber\\ & + \sum_{k \in K}\sum_{p \in P} C_{p^0_k p} y_{k1p} - && \text{initial product setup} \nonumber\\ + && \text{initial loading setup} \nonumber\\ & + \sum_{k \in K}\sum_{t=1}^{R-1}\sum_{p \in P}\sum_{p' \in P} C_{pp'} z_{ktpp'} - && \text{inter-trip changeover}. + && \text{preparation for a subsequent loading}. \label{eq:objective} \end{align} @@ -262,7 +260,7 @@ \subsection{Station-Product Visit Logic} \end{align} The same vehicle cannot visit the same station more than once for the same -product: +product. Visits by that vehicle for different products remain possible: \begin{equation} \sum_{t \in T} w_{ktsp} \leq 1 \qquad \forall k \in K,\ s \in S,\ p \in P. @@ -330,7 +328,7 @@ \subsection{Depot Stock Accounting} \subsection{Subtour Elimination} -The implementation uses Miller--Tucker--Zemlin station order variables: +Miller--Tucker--Zemlin order variables describe the position of each station: \begin{align} u_{kts} &\leq M v_{kts} && \forall k \in K,\ t \in T,\ s \in S, \label{eq:order-upper}\\ @@ -400,12 +398,9 @@ \section{Domains} 0 \leq u_{kts} &\leq M. \end{align} -\section{Implementation Notes} +\section{Model Scope} -The notation above follows the implemented model in -\texttt{mpvrp\_cc/optimization/milp\_solver.py}. The -Python code uses zero-based product indices internally, while product -identifiers in instance files may be one-based. The allowed route arcs contain +The allowed route arcs contain depot-to-station, station-to-station, and station-to-depot arcs, but not depot-to-depot arcs. Empty depot-to-depot movements are priced only between consecutive trips. diff --git a/docs/problem.md b/docs/problem.md index bbfe605..31a79fa 100644 --- a/docs/problem.md +++ b/docs/problem.md @@ -1,108 +1,124 @@ # Multi-Product Vehicle Routing Problem with Split Deliveries and Changeover Costs -## 1. Context and motivation +## 1. Overview -Efficient supply-chain management relies on coordinated transportation strategies that ensure timely product distribution while minimizing operational costs. In industries such as petroleum distribution, chemical manufacturing, food distribution, agriculture, pharmaceuticals, and waste collection, a shared fleet may transport several product types from multiple depots to geographically dispersed customers. +The **Multi-Product Vehicle Routing Problem with Split Deliveries and Changeover Costs (MPVRP-CC)** studies how a shared fleet can distribute several products from a set of depots to geographically dispersed customers. -Using the same vehicle for successive products can improve fleet utilization, but it may also require product-specific preparation before the next trip. These operations consume money, labor, equipment, and time. Route planning must therefore account for both geographical efficiency and the operational consequences of changing the product assigned to a vehicle. +The aim is to decide which vehicles to use, where they should load, which customers they should visit, how much they should deliver, and in which order the products should be transported. A good solution must satisfy every demand while balancing travel distance with the operational costs associated with preparing and loading vehicles. -The **Multi-Product Vehicle Routing Problem with Split Deliveries and Changeover Costs (MPVRP-CC)** determines vehicle routes, delivered quantities, depot assignments, and product sequences that satisfy all customer demands at minimum total cost. - -The problem is industry-independent. Petroleum distribution is one relevant application, but it is only one example of the broader planning setting. +The problem applies to many sectors, including petroleum distribution, chemicals, food, agriculture, pharmaceuticals, and waste collection. These examples differ in practice, but they share the same planning challenge: a vehicle may perform several trips and carry different products over the course of its schedule. ## 2. Logistics network -The problem is defined by the following sets: +The network contains: -- **K**: heterogeneous vehicles, each with a capacity, a home garage, and an initial product configuration; -- **P**: products to distribute; -- **G**: garages from which vehicles depart and to which they return; -- **D**: depots where products are stocked and loaded; -- **S**: customer locations with a demand for one or more products. +- **vehicles**, each with a capacity, a home garage, and an initial product configuration; +- **products** to be distributed; +- **garages**, where vehicles begin and end their schedules; +- **depots**, where products are stored and loaded; +- **service stations**, or customers, with demand for one or more products. -Every depot, garage, and customer has a geographical position. Transportation costs are based on the distance between locations. Each depot holds a finite stock of every product, and each vehicle can carry at most its stated capacity. +Every depot, garage, and station has a geographical position. Travel cost is measured using the distance between these locations. Depot stocks are limited, and no vehicle may carry more than its capacity. -## 3. Vehicle operations +## 3. How a vehicle operates -A vehicle route starts at its assigned garage, contains one or more delivery trips, and ends at the same garage: +A vehicle leaves its home garage, performs one or more delivery trips, and returns to the same garage: ```text -Garage → [Depot → Customers → Depot] ... → Garage +Garage → Depot → Customers → Depot → ... → Garage ``` -Each depot-to-customer cycle is called a **mini-route** or **trip**. During one trip, a vehicle: - -1. travels to a depot; -2. is prepared and loaded with exactly one product; -3. visits one or more customers requiring that product; -4. returns to a depot, either to begin another trip or to return to its garage. - -A vehicle carries only one product during a trip. It may nevertheless carry different products on successive trips. +Each trip begins with a loading operation at a depot. The vehicle then visits one or more stations and delivers a single product before returning to a depot or ending its schedule at the garage. A vehicle carries only one product during a trip, but it may carry another product on a later trip. -## 4. Changeover costs +## 4. Changeover and loading costs -A **changeover** occurs when the product assigned to a vehicle for its next trip differs from its current product configuration. The changeover cost is an aggregate operational cost, not merely a tank-cleaning cost. +The **changeover cost** is an operational transition cost associated with preparing a vehicle for the product loaded on its next trip. It should not be understood only as a penalty for switching from one product to another. The loading operation itself may require preparation, handling, inspection, or equipment setup, including for the first trip of the day. -Depending on the application, it may represent: +Depending on the application, this cost may include: - cleaning, purging, washing, drying, or decontamination; -- loading-related preparation and product-handling operations; -- equipment, tank, compartment, hose, or temperature reconfiguration; -- quality-control, safety, inspection, and certification procedures; -- labor and consumable materials; -- setup delays, vehicle downtime, and the associated loss of availability; -- administrative or coordination activities required before the next trip. +- preparation and handling during loading; +- reconfiguration of tanks, compartments, hoses, pumps, or temperature settings; +- quality, safety, inspection, or certification procedures; +- labor, consumables, and equipment use; +- waiting time, vehicle downtime, and loss of availability; +- administrative and coordination work before departure. -These costs are represented by a directed product-to-product matrix. A transition from product `p` to product `q` may have a different cost from the reverse transition. The diagonal is zero because continuing with the same product does not trigger an additional changeover in the current model. +The cost is described by a directed matrix. Its value depends on the vehicle's current product configuration and on the product that will be loaded. A transition from product `p` to product `q` may therefore cost more or less than the reverse transition. -The initial configuration of each vehicle is also considered: if its first trip uses another product, the corresponding initial changeover cost is incurred. +The initial configuration of each vehicle is part of the instance. The first loading is evaluated from that initial configuration, so an initial preparation cost may be incurred before any delivery takes place. Later costs are evaluated at each new loading. In the current benchmark, the diagonal of the matrix is zero: loading the same product again does not add a new transition cost, even though a loading operation still takes place. ## 5. Split deliveries -A customer’s demand for a product may exceed one vehicle’s capacity or may be more efficiently distributed among several vehicles. The model therefore permits **split deliveries**: the demand of a customer-product pair can be divided among multiple vehicles. +A station's demand for one product may be larger than a vehicle's capacity or may be more efficiently shared among several vehicles. The problem therefore allows **split deliveries**: several vehicles may contribute to the same station-product demand. -The complete demand must still be delivered exactly. In the implemented formulation, a given vehicle can serve the same customer-product pair at most once over its trips, so a split is performed across distinct vehicles. +The full requested quantity must still be delivered. A vehicle may visit the same station several times only when those visits concern different products. For any given product, that vehicle may serve the station at most once during its complete schedule. + +For example, a vehicle may visit station 4 once with product 1 and return later with product 2. It may not return to station 4 a second time with product 1. If the demand for product 1 must be split, another vehicle has to deliver the remaining quantity. ## 6. Objective -The objective is to minimize the sum of: +The objective is to minimize the total of: + +- the distance traveled by the fleet; +- the operational transition costs incurred during initial and subsequent loading operations. -- travel distance within delivery trips; -- initial and inter-trip changeover costs. +Mathematically, the objective can be written as: -This objective captures the trade-off at the center of the problem. A geographically shorter plan may require expensive product changes, while a longer route may preserve a vehicle’s current configuration and reduce preparation costs. +$$ +\min Z = +\underbrace{\sum_{k \in K}\sum_{(i,j) \in A} d_{ij}\,n_{ijk}}_{\text{total travel distance}} ++ +\underbrace{\sum_{k \in K^{+}}\left( +C_{p_k^{0},p_{k1}} ++ +\sum_{t=2}^{|T_k|} C_{p_{k,t-1},p_{kt}} +\right)}_{\text{initial and subsequent loading-transition costs}} +$$ -## 7. Main constraints +where: -A feasible solution must satisfy the following requirements: +- $K$ is the set of vehicles and $K^{+}$ is the set of vehicles that perform at least one trip; +- $A$ is the set of possible travel arcs; +- $d_{ij}$ is the distance between locations $i$ and $j$; +- $n_{ijk}$ is the number of times vehicle $k$ travels from $i$ to $j$; +- $T_k$ is the ordered set of trips performed by vehicle $k$; +- $p_k^{0}$ is the initial product configuration of vehicle $k$; +- $p_{kt}$ is the product loaded by vehicle $k$ for trip $t$; +- $C_{pq}$ is the preparation and loading-transition cost from configuration $p$ to loaded product $q$. -- every customer demand is delivered exactly; -- every vehicle load respects its capacity; -- the quantity loaded from a depot does not exceed its available stock; -- each active trip selects exactly one product and begins and ends at a depot; -- a station is visited during a trip only when it demands the product carried; -- every used vehicle starts and ends at its assigned garage; -- active trips are consecutive and form connected routes without isolated subtours. +This creates the central trade-off of the problem. The shortest routes are not always the least expensive: a slightly longer plan may reduce costly preparations, while a compact route may require more product transitions or loading setups. -The current problem does not include delivery time windows, explicit service durations, or depot replenishment. Distances are Euclidean, and every location is assumed to be accessible. +## 7. Conditions for a feasible solution -## 8. Comparative experiment +A solution is feasible when: -The repository provides two paired benchmark scenarios: +- every station receives exactly the quantity requested for each product; +- vehicle capacities are respected; +- quantities loaded at each depot remain within available stocks; +- every trip carries exactly one product; +- a station is visited only for a product it requires; +- each vehicle visits a station at most once for any given product, although it may return with a different product; +- each used vehicle starts and ends at its home garage; +- consecutive trips form a complete, connected schedule. -- **with changeover costs**: the original product-transition matrices are retained; -- **without changeover costs**: the same instances are used, but every transition cost is set to zero. +The benchmark does not include time windows, explicit service times, or depot replenishment. Distances are Euclidean, and all locations are considered accessible. -Within each pair, the UUID, fleet, locations, stocks, demands, capacities, and initial vehicle products are identical. Comparing the resulting solutions isolates the influence of changeover costs on vehicle utilization, product sequences, depot choices, route geometry, and total distance. +## 8. Benchmark scenarios -Only solutions for the **with changeover costs** scenario enter the official -scoreboard. The zero-cost scenario is provided so competitors can run and report -their own controlled comparison. +The benchmark contains two versions of each instance: + +- **with changeover costs**, using the original transition matrix; +- **without changeover costs**, using the same data with every transition cost set to zero. + +The paired instances have the same fleet, locations, stocks, demands, capacities, vehicle configurations, and identifier. Comparing their solutions shows how loading and transition costs influence vehicle use, product sequences, depot choices, routes, and total distance. + +Only the instances with changeover costs are included in the official ranking. The zero-cost versions are provided for comparative experiments. ## 9. Official score -For every feasible official instance, the evaluator adds the recomputed travel -distance and changeover cost. A missing, malformed, or infeasible solution receives -a penalty of `100000`. The submission score is the sum across all 150 instances; -therefore, lower is better. The scoreboard keeps the current result associated with -each participant email in Notion. +For each feasible official instance, the score is the sum of the total travel distance and the total transition cost. Participants may submit any subset of the 150 solutions in a ZIP archive; submitting the complete set at once is not required. + +The platform identifies the solutions present in the archive and evaluates them independently. A solution that is absent, unresolved, unreadable, or infeasible receives the same penalty of `100000` for its instance. + +The final score is the sum obtained across all 150 instances. Lower scores are better. diff --git a/docs/problem.pdf b/docs/problem.pdf index fa72915..c74dc8b 100644 Binary files a/docs/problem.pdf and b/docs/problem.pdf differ diff --git a/docs/solution_format.md b/docs/solution_format.md index 12f87e4..3361afe 100644 --- a/docs/solution_format.md +++ b/docs/solution_format.md @@ -1,58 +1,58 @@ -# Solution Format Specification +# Solution File Format -> **Note:** This document details the file format used for MPVRP-CC solutions. To be validated, a solution must strictly follow the structure described below. +## 1. Naming the files ---- +Solutions are plain-text files with the `.dat` extension. For the instance: -## 1. File Format +```text +MPVRP_001_s48_d1_p1.dat +``` + +the preferred solution name is: -Solutions are stored in text files with the `.dat` extension. For official instance -`MPVRP_001_s48_d1_p1.dat`, the canonical solution name is -`Sol_MPVRP_001_s48_d1_p1.dat`. The submission service also accepts the short name -`Sol_001.dat`. A ZIP may contain files at any directory depth, but it must contain -one solution for every ID from `001` through `150`. +```text +Sol_MPVRP_001_s48_d1_p1.dat +``` ---- +The shorter name `Sol_001.dat` is also accepted. A submission archive may organize files in folders and may contain any subset of the instances from `001` to `150`. The platform identifies every recognized solution and evaluates it independently. -## 2. File Structure +Submitting all 150 solutions at once is not required. For the final score, an absent solution, an unresolved solution, and an invalid solution are treated in the same way: the corresponding instance receives a penalty of `100000`. -The file describes the routes vehicle by vehicle. For each vehicle used, the solution contains a block of **2 lines**, separated by an empty line. +## 2. Describing a vehicle schedule -### 2.1 Line 1: Visit Sequence +Every used vehicle is represented by two matching lines. Leave an empty line before the next vehicle. -``` -ID: Garage - Depot [Load] - Station (Deliver) - ... - Garage +### Route line + +```text +ID: Garage - Depot [Load] - Station (Delivery) - ... - Garage ``` -This line starts with the vehicle ID and describes the path: +This line follows the vehicle from departure to return: -- **Garage**: Start and end point (Node ID only). -- **Depot**: Identified by square brackets `[Qty]` indicating quantity loaded. -- **Station**: Identified by parentheses `(Qty)` indicating quantity delivered. +- a **garage** is written with its identifier; +- a **depot** is followed by the quantity loaded in square brackets; +- a **station** is followed by the quantity delivered in parentheses. -Node IDs refer to their 1-based index in the instance file (e.g., loaded at Depot 1, delivered to Station 2) and are not cumulative across types. +Identifiers are local to their category. Depot 1, garage 1, and station 1 are therefore three different locations. -### 2.2 Line 2: Product Sequence and Costs +### Product and cost line -``` -ID: Prod(Cost) - Prod(Cost) - ... +```text +ID: Product(CumulativeCost) - Product(CumulativeCost) - ... ``` -This line indicates which product is associated with every route step and the cumulative changeover cost. -Products are zero-based in solutions: valid IDs are `0, ..., NbProducts - 1`. +This second line gives the vehicle's product configuration and cumulative transition cost at every step of the route. Product identifiers start at `0` in solution files and range from `0` to `NbProducts - 1`. -The first token, at the departure garage, must be the vehicle's initial product from -the instance converted to zero-based indexing. A product may change only on a depot -step. The cumulative cost increases by the directed matrix value whenever the depot -product differs from the preceding configuration, including before the first trip. +The cumulative cost annotation is optional. A simpler sequence such as `0 - 0 - 1 - 1` is accepted because the platform recalculates transition costs from the instance matrix. -> **Important:** The two lines must be perfectly aligned in terms of the number of steps. Each element in the visit sequence corresponds to exactly one element in the product sequence. +The first value is the vehicle's initial product configuration. At every depot, the next value is the product being loaded. This is also where any preparation and loading-related transition cost is added. The amount comes from the directed matrix using the previous configuration and the newly loaded product. It therefore applies before the first delivery trip as well as between later trips. In the current benchmark, loading the same product again adds no transition cost because the matrix diagonal is zero. ---- +The route line and the product line normally have the same number of elements: every visited location has one corresponding product and cumulative cost. The compact convention used by the historical benchmark solutions may omit the final garage entry from the product line. In that case, the last product configuration and cumulative cost are implicitly carried through to the return garage. -## 3. Valid Solution Example +## 3. Example -``` +```text 1: 1 - 1 [1344] - 2 (1344) - 1 1: 0(0.0) - 0(0.0) - 0(0.0) - 0(0.0) @@ -60,18 +60,13 @@ product differs from the preceding configuration, including before the first tri 2: 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) - 1(0.0) ``` -In this example: - -- **Vehicle 1** starts at garage 1, loads 1344 units at depot 1, delivers 1344 units to station 2, and returns to garage 1. It carries product 0 (cost 0.0). -- **Vehicle 2** starts at garage 1, loads 8947 units at depot 1, delivers to stations 1, 2, and 3, and returns to garage 1. It carries product 1 (cost 0.0). +Here, vehicle 1 leaves garage 1, loads 1344 units of product 0 at depot 1, delivers them to station 2, and returns home. Vehicle 2 loads 8947 units of product 1, serves stations 1, 2, and 3, then returns to garage 1. Neither vehicle changes configuration, so their cumulative transition costs remain zero. ---- +## 4. Summary metrics -## 4. Solution Metrics +After the last vehicle block, the file may end with six summary lines: -After all vehicle routes, the file ends with **6 lines** of performance metrics, in the following order: - -``` +```text 2 7 55.66 @@ -80,36 +75,27 @@ Intel Core i7-10700K 0.245 ``` -### 4.1 Line 1 — Number of Vehicles Used -The count of vehicles with at least one delivery (e.g., `2`). - -### 4.2 Line 2 — Number of Product Changes -The total number of product changes across the entire solution (e.g., `7`). - -### 4.3 Line 3 — Total Transition Cost -The sum of all product changeover costs for all vehicles (e.g., `55.66`). - -### 4.4 Line 4 — Total Distance -The total distance traveled by the fleet, expressed as the sum of Euclidean distances (e.g., `1385.07`). - -### 4.5 Line 5 — Processor -The model of the processor on which the solution was generated (e.g., `Intel Core i7-10700K`). - -### 4.6 Line 6 — Resolution Time -The time elapsed to generate the solution, in seconds (e.g., `0.245`). +They contain, in this order: ---- +1. **Vehicles used** — the number of vehicles that perform at least one delivery. +2. **Product transitions** — the total number of charged product transitions. +3. **Total transition cost** — the sum of all preparation and loading-related transition costs. +4. **Total distance** — the Euclidean distance traveled by the complete fleet. +5. **Processor** — the processor used to produce the solution. +6. **Resolution time** — the computation time in seconds. -> A valid solution must satisfy all the constraints. +This summary is optional. The processor and resolution time may also be omitted, leaving only the first four numeric lines. The platform always recalculates the number of vehicles, product transitions, transition cost, and distance from the routes. Differences caused by rounding or outdated summary values do not make an otherwise feasible route invalid. -## 5. Feasibility rules enforced by the platform +## 5. Feasibility requirements -- Every route block uses a distinct vehicle from the instance. -- A route starts and ends at that vehicle's home garage; garages cannot occur in the middle. -- Every mini-route starts with one positive depot load, contains at least one station delivery, and ends at a depot or the final garage. -- The product remains constant throughout a mini-route and may change only at a depot. -- A mini-route's loaded quantity equals its delivered quantity and never exceeds vehicle capacity. -- Quantities are positive, depot stocks are respected, and every station-product demand is met exactly. -- One vehicle may serve a station-product pair at most once across its complete route. -- Cumulative changeover costs and the six final metrics must agree with values recomputed by the verifier. +A valid solution must respect all of the following conditions: +- each vehicle appears at most once; +- every route starts and ends at that vehicle's home garage; +- each trip begins with a positive depot load and includes at least one delivery; +- a vehicle carries one product throughout a trip, with a new product selected only when loading at a depot; +- the quantity loaded for a trip equals the quantity delivered and does not exceed vehicle capacity; +- depot stocks remain non-negative; +- every station-product demand is met exactly; +- one vehicle serves a given station-product pair at most once across all its trips; it may revisit the same station only to deliver another product; +- the route itself respects all structural and operational constraints; cumulative costs and final metrics are recalculated by the platform. diff --git a/docs/solution_format.pdf b/docs/solution_format.pdf index 50e085b..3ff07f2 100644 Binary files a/docs/solution_format.pdf and b/docs/solution_format.pdf differ diff --git a/index.html b/index.html index bf7d3af..a493b17 100644 --- a/index.html +++ b/index.html @@ -1,59 +1,86 @@ - + + MPVRP-CC — Route beyond distance - + + - - + + - -
- - -
+ +
-
-
+
+
-

Route beyond
distance.

-

Design multi-product vehicle routes that balance geography, capacity and the operational cost of changing products. Compare the same network with and without changeover costs.

- -
-
-
Objectiveminimize
-
Travel distance
+ changeover cost
-
150official
150zero-cost
1:1paired
+ +

Route beyond
distance.

+

Plan multi-product deliveries by balancing route length, vehicle capacity and the operational cost of preparing each loading.

+ +
+ +
+ Illustrated logistics network with roads, depots and service facilities +
+ Objective + Distance + loading transitions +
+
-
-

The challenge

One fleet.
Several products.
Real trade-offs.

01 / Route

Connected delivery trips

Every used vehicle leaves its home garage, loads at depots, serves customers and returns home.

02 / Product

One product per trip

A vehicle may switch products between trips, with a directed cost determined by the transition matrix.

03 / Demand

Split deliveries

A station-product demand may be shared across vehicles, while every quantity must be delivered exactly.

04 / Evidence

Paired experiments

Only the transition matrix changes between paired scenarios, isolating its effect on route design.

-
+
+
+
+
+

The challenge

+

One fleet.
Several products.
Real trade-offs.

+
+
+
ROUTES

Connected trips from the home garage through depots and stations.

+
PRODUCTS

One product per trip, with preparation costs at loading.

+
DEMAND

Split deliveries with one visit per vehicle, station and product.

+
+
-
-

Benchmark datasets

Same instances. Two cost regimes.

Submit solutions for the original-cost dataset. Use the zero-cost twin to measure how changeover economics reshape your decisions.

-
+ -

Ready to compete?

Validate locally. Submit once. Learn from every instance.

+
+

Validate locally, submit any subset of solutions, and improve progressively across the benchmark.

+ +
+
+
-
- - + +
+ + + diff --git a/pages/about.html b/pages/about.html new file mode 100644 index 0000000..9ac68e1 --- /dev/null +++ b/pages/about.html @@ -0,0 +1,102 @@ + + + + + + + About & Partnerships — MPVRP-CC + + + + + + + + +
+ +
+
+
+
+

The research behind the benchmark

+

From research questions to industrial decisions.

+
+
+

We are an academic research team working on planning and scheduling problems arising in industry. Our work brings together operations research, mathematical modelling and algorithm design to turn complex operational constraints into practical decision-support tools.

+

MPVRP-CC is one step in that direction: an open platform for studying multi-product distribution when routing decisions and loading transitions must be optimized together.

+
+
+
+ +
+
+
+
+

Project outlook

+

What comes
next.

+

The project is in an active academic research phase, with the ambition of evolving into a concrete industrial application.

+
+ +
    +
  1. + 01 +

    Scale up the solution methods

    +

    Design and implement new models based on constraint programming and metaheuristics or any relevant method to solve large-scale instances efficiently.

    +
  2. +
  3. + 02 +

    Open the challenge

    +

    Launch an open optimization competition on this platform and invite the global Operations Research community to propose novel algorithms.

    +
  4. +
  5. + 03 +

    Move into the field

    +

    Identify a specific business context and build a partnership with a logistics company to test the model in real operational scenarios.

    +
  6. +
+
+
+
+ +
+
+
+
+
+

Work with us

+

Let’s turn a research problem into a shared project.

+

We welcome discussions with logistics companies, industrial partners, research teams and sponsors. Contact us to explore a real-world case study, support the open competition, collaborate on research, or simply ask a question about the platform.

+
+ Industrial partnership + Sponsorship + Research collaboration +
+
+ + +
+
+
+
+
+ +
+ + + diff --git a/pages/documentation.html b/pages/documentation.html index 711566a..0db43e6 100644 --- a/pages/documentation.html +++ b/pages/documentation.html @@ -1,5 +1,5 @@ Documentation — MPVRP-CC - -
-

Reference guide

-
+ +
+

Reference guide

↓ Download PDF
+
diff --git a/pages/scoreboard.html b/pages/scoreboard.html index 2346213..3341621 100644 --- a/pages/scoreboard.html +++ b/pages/scoreboard.html @@ -1,2 +1,40 @@ -Scoreboard — MPVRP-CC
-

Official benchmark

Current recorded submission per participant. Lower scores are better.

Loading scoreboard…
+ + + + + + Scoreboard — MPVRP-CC + + + + + + + + +
+
+
+
+

Official benchmark

+

Current recorded submission per participant. Lower scores are better.

+

+
+ +
+
+
Loading scoreboard…
+
+ + + + +
+
+
+
+ + + + + diff --git a/pages/static/css/app.css b/pages/static/css/app.css index a53f060..e4fd0fc 100644 --- a/pages/static/css/app.css +++ b/pages/static/css/app.css @@ -1,6 +1,29 @@ -:root { color-scheme: light; } +:root { + color-scheme: light; + --accent: #F4320B; + --accent-dark: #cf2808; + --accent-soft: #fff; +} html { scroll-behavior: smooth; } body { font-family: Inter, sans-serif; } +.has-floating-nav { padding-top: 4.5rem; background-color: #fff !important; } +.has-floating-nav > footer { background-color: #fff !important; } +.floating-site-header { pointer-events: none; } +.floating-site-header nav { pointer-events: auto; } +.floating-site-header .brand-icon { height: 2.4rem; width: 2.4rem; border-radius: 999px; background: var(--accent); } +.floating-site-header .brand-icon svg { height: 1.2rem; width: 1.2rem; } +.text-blue-600, .text-blue-700 { color: var(--accent) !important; } +.text-blue-100, .text-blue-200, .text-blue-300 { color: #ffb3a3 !important; } +.text-blue-950 { color: #431208 !important; } +.text-blue-400 { color: #ff7357 !important; } +.bg-blue-600 { background-color: var(--accent) !important; } +.bg-blue-700, .hover\:bg-blue-700:hover { background-color: var(--accent-dark) !important; } +.bg-blue-50, .hover\:bg-blue-50:hover { background-color: var(--accent-soft) !important; } +.bg-blue-50\/50 { background-color: rgba(255, 240, 235, .65) !important; } +.border-blue-300, .border-blue-400, .border-blue-600 { border-color: var(--accent) !important; } +.hover\:bg-blue-600:hover, .hover\:bg-blue-500:hover { background-color: var(--accent) !important; } +.hover\:text-blue-600:hover { color: var(--accent) !important; } +.hover\:border-blue-600:hover, .focus\:border-blue-600:focus { border-color: var(--accent) !important; } h1, h2, h3, h4, .font-display { font-family: "Bricolage Grotesque", sans-serif; } .grid-noise { background-image: linear-gradient(rgba(15, 23, 42, .045) 1px, transparent 1px), @@ -9,9 +32,9 @@ h1, h2, h3, h4, .font-display { font-family: "Bricolage Grotesque", sans-serif; } .tab-content { display: none; } .tab-content.active { display: block; } -.tab-btn.active { background: #2563eb; color: white; } +.tab-btn.active { background: var(--accent); color: white; } .status-message { margin-top: 1rem; border-radius: .75rem; padding: .9rem 1rem; font-size: .9rem; } -.status-message.info { background: #eff6ff; color: #1d4ed8; } +.status-message.info { background: var(--accent-soft); color: var(--accent-dark); } .status-message.success { background: #f0fdf4; color: #166534; } .status-message.error { background: #fef2f2; color: #b91c1c; } .verification-results { margin-top: 1.25rem; border: 1px solid #e2e8f0; border-radius: 1rem; padding: 1.25rem; } @@ -35,30 +58,32 @@ h1, h2, h3, h4, .font-display { font-family: "Bricolage Grotesque", sans-serif; .markdown-body { color: #334155; font-size: 1rem; line-height: 1.8; } .markdown-body h1 { color: #0f172a; font-size: 2.25rem; line-height: 1.12; margin: 0 0 2rem; } .markdown-body h2 { color: #0f172a; font-size: 1.55rem; line-height: 1.25; margin: 2.5rem 0 1rem; } -.markdown-body h3 { color: #172554; font-size: 1.15rem; margin: 2rem 0 .75rem; } +.markdown-body h3 { color: #431208; font-size: 1.15rem; margin: 2rem 0 .75rem; } .markdown-body p, .markdown-body ul, .markdown-body ol { margin: 0 0 1rem; } .markdown-body ul, .markdown-body ol { padding-left: 1.4rem; } .markdown-body ul { list-style: disc; } .markdown-body ol { list-style: decimal; } -.markdown-body code { background: #eff6ff; color: #1d4ed8; border-radius: .35rem; padding: .12rem .35rem; font-size: .88em; } -.markdown-body pre { overflow-x: auto; border: 1px solid #dbeafe; background: #0f172a; color: #e2e8f0; border-radius: 1rem; padding: 1.25rem; margin: 1.25rem 0; } +.markdown-body code { background: var(--accent-soft); color: var(--accent-dark); border-radius: .35rem; padding: .12rem .35rem; font-size: .88em; } +.markdown-body pre { overflow-x: auto; border: 1px solid #ffd2c8; background: #171411; color: #e7e5e4; border-radius: 1rem; padding: 1.25rem; margin: 1.25rem 0; } .markdown-body pre code { background: transparent; color: inherit; padding: 0; } -.markdown-body blockquote { border-left: 3px solid #2563eb; background: #eff6ff; padding: 1rem 1.25rem; margin: 1.5rem 0; border-radius: 0 .75rem .75rem 0; } +.markdown-body blockquote { border-left: 3px solid var(--accent); background: var(--accent-soft); padding: 1rem 1.25rem; margin: 1.5rem 0; border-radius: 0 .75rem .75rem 0; } .markdown-body table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; font-size: .92rem; } .markdown-body th, .markdown-body td { border: 1px solid #e2e8f0; padding: .7rem .8rem; text-align: left; } .markdown-body th { background: #f8fafc; color: #0f172a; } -.markdown-body a { color: #2563eb; text-decoration: underline; text-underline-offset: 3px; } +.markdown-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; } +.markdown-body .katex-display { margin: 1.75rem 0; overflow-x: auto; overflow-y: hidden; padding: .5rem 0; } +.markdown-body .katex { color: #0f172a; font-size: 1.05em; } .brand-icon { display: inline-flex; height: 2.6rem; width: 2.6rem; align-items: center; justify-content: center; border-radius: .85rem; background: #0f172a; color: white; transition: background-color .2s, transform .2s; } -.brand-icon:hover { background: #2563eb; transform: translateY(-1px); } +.brand-icon:hover { background: var(--accent); transform: translateY(-1px); } .brand-icon svg { height: 1.45rem; width: 1.45rem; } .toc-link { display: block; border-left: 2px solid #e2e8f0; padding: .42rem .8rem; color: #64748b; font-size: .78rem; line-height: 1.25; transition: color .2s, border-color .2s; } -.toc-link:hover { border-color: #2563eb; color: #2563eb; } +.toc-link:hover { border-color: var(--accent); color: var(--accent); } .toc-link--nested { padding-left: 1.35rem; } .docs-layout { display: grid; gap: 1.5rem; align-items: start; } .docs-content { min-width: 0; } .docs-toc { max-height: 20rem; overflow-y: auto; } .site-footer-link { display: inline-flex; align-items: center; gap: .4rem; color: #475569; transition: color .2s; } -.site-footer-link:hover { color: #2563eb; } +.site-footer-link:hover { color: var(--accent); } .site-footer-link svg { height: 1rem; width: 1rem; flex: none; } .contributor-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 1.5rem; min-width: 18rem; } .contributor-actions { display: flex; align-items: center; gap: .45rem; } @@ -67,8 +92,8 @@ h1, h2, h3, h4, .font-display { font-family: "Bricolage Grotesque", sans-serif; .contributor-icon svg { height: 1.05rem; width: 1.05rem; } .contributor-icon--github { background: #0f172a; color: #fff; } .contributor-icon--github:hover { background: #334155; } -.contributor-icon--email { background: #dbeafe; color: #2563eb; } -.contributor-icon--email:hover { background: #2563eb; color: #fff; } +.contributor-icon--email { background: #ffe0d8; color: var(--accent); } +.contributor-icon--email:hover { background: var(--accent); color: #fff; } @media (min-width: 1024px) { .docs-layout { grid-template-columns: 15rem minmax(0, 1fr); gap: 2rem; } .docs-toc { position: sticky; top: 6.5rem; max-height: calc(100vh - 8rem); } diff --git a/pages/static/css/scoreboard.css b/pages/static/css/scoreboard.css index d168cd4..f73d946 100644 --- a/pages/static/css/scoreboard.css +++ b/pages/static/css/scoreboard.css @@ -5,7 +5,7 @@ } .leaderboard-meta span { - color: #3C27F5; + color: #F4320B; font-weight: bold; } @@ -34,8 +34,8 @@ tr.rank-3 td:first-child { /* Bouton refresh */ .btn-refresh { background: none; - border: 1px solid #3C27F5; - color: #3C27F5; + border: 1px solid #F4320B; + color: #F4320B; cursor: pointer; padding: 5px 14px; border-radius: 4px; @@ -46,5 +46,5 @@ tr.rank-3 td:first-child { vertical-align: middle; } -.btn-refresh:hover:not(:disabled) { background: #3C27F5; color: #fff; } -.btn-refresh:disabled { opacity: 0.5; cursor: not-allowed; } \ No newline at end of file +.btn-refresh:hover:not(:disabled) { background: #F4320B; color: #fff; } +.btn-refresh:disabled { opacity: 0.5; cursor: not-allowed; } diff --git a/pages/static/css/style.css b/pages/static/css/style.css index 3e0864b..a881494 100644 --- a/pages/static/css/style.css +++ b/pages/static/css/style.css @@ -28,7 +28,7 @@ header { h1 { font-size: 32px; font-weight: 700; - color: #3C27F5; + color: #F4320B; margin-bottom: 5px; } @@ -57,7 +57,7 @@ nav { } .nav-links a { - color: #3C27F5; + color: #F4320B; text-decoration: none; font-size: 16px; } @@ -78,7 +78,7 @@ nav { .menu-toggle .bar { width: 25px; height: 3px; - background-color: #3C27F5; + background-color: #F4320B; transition: all 0.3s ease; } @@ -90,7 +90,7 @@ section { h2 { font-size: 24px; font-weight: 700; - color: #3C27F5; + color: #F4320B; border-bottom: 1px solid #ddd; padding-bottom: 8px; margin-bottom: 15px; @@ -103,7 +103,7 @@ p { /* Links */ a { - color: #3C27F5; + color: #F4320B; } a:hover { @@ -254,4 +254,4 @@ footer { th, td { padding: 8px 6px; } -} \ No newline at end of file +} diff --git a/pages/static/css/submission.css b/pages/static/css/submission.css index 6ddf65f..08df316 100644 --- a/pages/static/css/submission.css +++ b/pages/static/css/submission.css @@ -49,7 +49,7 @@ .form-group input:focus { outline: none; - border-color: #3c27f5; + border-color: #F4320B; box-shadow: 0 0 0 3px rgba(60, 39, 245, 0.12); } @@ -120,7 +120,7 @@ .btn-upload { width: 100%; margin-top: 4px; - background: #3c27f5; + background: #F4320B; color: #fff; border: none; border-radius: 6px; @@ -152,9 +152,9 @@ .msg-banner--info-static { margin-top: 12px; margin-bottom: 14px; - background: #eff6ff; - border: 1px solid #3c27f5; - color: #3c27f5; + background: #fff0eb; + border: 1px solid #F4320B; + color: #cf2808; } .result-section { @@ -195,7 +195,7 @@ .result-details summary { cursor: pointer; - color: #3c27f5; + color: #F4320B; font-weight: 600; padding: 12px 0; user-select: none; diff --git a/pages/static/css/tools.css b/pages/static/css/tools.css index 5b3ab66..b0fcf16 100644 --- a/pages/static/css/tools.css +++ b/pages/static/css/tools.css @@ -34,12 +34,12 @@ } .tab-btn:hover { - color: #3c27f5; + color: #F4320B; } .tab-btn.active { - color: #3c27f5; - border-bottom-color: #3c27f5; + color: #F4320B; + border-bottom-color: #F4320B; } /* Tab Content */ @@ -131,7 +131,7 @@ .form-group input[type="number"]:focus, .form-group input[type="file"]:focus { outline: none; - border-color: #3c27f5; + border-color: #F4320B; box-shadow: 0 0 0 3px rgba(60, 39, 245, 0.12); } @@ -151,7 +151,7 @@ .btn-submit { display: inline-block; padding: 12px 32px; - background-color: #3c27f5; + background-color: #F4320B; color: white; border: none; border-radius: 6px; @@ -314,7 +314,7 @@ width: 14px; height: 14px; border: 2px solid #f3f3f3; - border-top: 2px solid #3c27f5; + border-top: 2px solid #F4320B; border-radius: 50%; animation: spin 1s linear infinite; } @@ -334,7 +334,7 @@ margin-top: 20px; padding: 10px 16px; background-color: #e8eaf6; - color: #3c27f5; + color: #F4320B; text-decoration: none; border-radius: 6px; font-size: 14px; @@ -375,4 +375,3 @@ padding: 8px 10px; } } - diff --git a/pages/static/css/visualisation.css b/pages/static/css/visualisation.css index 653a209..636d8d3 100644 --- a/pages/static/css/visualisation.css +++ b/pages/static/css/visualisation.css @@ -2,8 +2,8 @@ Static page layout and presentation are expressed with Tailwind in visualisation.html. */ :root { - --primary: #2563eb; - --primary-light: rgba(37, 99, 235, .1); + --primary: #F4320B; + --primary-light: rgba(244, 50, 11, .1); --success: #10b981; --text: #1e293b; --text-muted: #64748b; @@ -25,7 +25,7 @@ canvas:active { cursor: grabbing; } body:has(.sidebar.collapsed) .sidebar-toggle { left: 0; } body:has(.sidebar.collapsed) .sidebar-toggle .toggle-icon { transform: rotate(180deg); } -.upload-compact.dragover { border-color: #2563eb; background: #eff6ff; } +.upload-compact.dragover { border-color: #F4320B; background: #fff0eb; } .upload-compact.loaded { border-color: #10b981; background: #f0fdf4; } .file-error[hidden] { display: none; } @@ -34,13 +34,13 @@ body:has(.sidebar.collapsed) .sidebar-toggle .toggle-icon { transform: rotate(18 .fleet-toolbar { display: flex; justify-content: flex-end; } .fleet-item { display: flex; align-items: center; gap: 6px; border-radius: 6px; background: #f1f5f9; padding: 5px 6px; color: #64748b; font-size: 11px; font-weight: 500; } .fleet-item.is-hidden { opacity: .55; } -.fleet-item.is-focused { box-shadow: inset 0 0 0 1px #2563eb; background: #eff6ff; } +.fleet-item.is-focused { box-shadow: inset 0 0 0 1px #F4320B; background: #fff0eb; } .fleet-visibility, .fleet-only, .fleet-toolbar button { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; } .fleet-visibility { display: inline-flex; align-items: center; gap: 5px; padding: 3px; } .fleet-color { width: 10px; height: 10px; border-radius: 3px; } .fleet-name { flex: 1; } -.fleet-only, .fleet-toolbar button { border-radius: 5px; padding: 4px 6px; color: #2563eb; font-size: 10px; font-weight: 700; } -.fleet-only:hover, .fleet-toolbar button:hover { background: #dbeafe; } +.fleet-only, .fleet-toolbar button { border-radius: 5px; padding: 4px 6px; color: #F4320B; font-size: 10px; font-weight: 700; } +.fleet-only:hover, .fleet-toolbar button:hover { background: #fff0eb; } .ctrl-btn { transition: background-color .2s, color .2s; } .ctrl-btn.primary.playing { background: #dc2626; } @@ -48,8 +48,8 @@ body:has(.sidebar.collapsed) .sidebar-toggle .toggle-icon { transform: rotate(18 .depot-inventory-panel.collapsed { max-height: 40px; } input[type="range"] { appearance: none; height: 4px; border-radius: 999px; background: #e2e8f0; outline: none; } -input[type="range"]::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #2563eb; cursor: pointer; } -input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #2563eb; cursor: pointer; } +input[type="range"]::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #F4320B; cursor: pointer; } +input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #F4320B; cursor: pointer; } .tooltip { position: fixed; z-index: 1000; min-width: 180px; max-width: 260px; border: 1px solid #e2e8f0; border-radius: 10px; background: #fff; padding: 0; box-shadow: 0 10px 40px rgba(15, 23, 42, .14); font-size: 12px; pointer-events: none; opacity: 0; transition: opacity .15s; } .tooltip-header { border-bottom: 1px solid #e2e8f0; border-radius: 10px 10px 0 0; background: #f8fafc; padding: 10px 14px; font-size: 13px; font-weight: 600; } @@ -87,7 +87,7 @@ input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; bo .depot-card.depot-warning .depot-name { color: #dc2626; } .notification-container { position: absolute; z-index: 100; top: 16px; right: 16px; display: flex; max-width: 280px; flex-direction: column; gap: 8px; pointer-events: none; } -.notification { display: flex; align-items: center; gap: 10px; border: 1px solid #e2e8f0; border-left: 4px solid #2563eb; border-radius: 8px; background: #fff; padding: 12px 16px; box-shadow: 0 4px 20px rgba(15, 23, 42, .1); animation: slideIn .3s ease-out, fadeOut .3s ease-in 2.7s; } +.notification { display: flex; align-items: center; gap: 10px; border: 1px solid #e2e8f0; border-left: 4px solid #F4320B; border-radius: 8px; background: #fff; padding: 12px 16px; box-shadow: 0 4px 20px rgba(15, 23, 42, .1); animation: slideIn .3s ease-out, fadeOut .3s ease-in 2.7s; } .notification.fade-out { animation: fadeOut .3s ease-in forwards; } .notification-icon { font-size: 18px; } .notification-content { flex: 1; } diff --git a/pages/static/imgs/tableau.jpg b/pages/static/imgs/tableau.jpg new file mode 100644 index 0000000..4b0df2b Binary files /dev/null and b/pages/static/imgs/tableau.jpg differ diff --git a/pages/static/imgs/tableau_.jpg b/pages/static/imgs/tableau_.jpg new file mode 100644 index 0000000..98cbc09 Binary files /dev/null and b/pages/static/imgs/tableau_.jpg differ diff --git a/pages/static/js/auth.js b/pages/static/js/auth.js index eb02164..835915b 100644 --- a/pages/static/js/auth.js +++ b/pages/static/js/auth.js @@ -8,7 +8,7 @@ function showMessage(message, type = 'error') { const colors = { error: { bg: '#fdf2f2', border: '#e74c3c', text: '#c0392b' }, success: { bg: '#f0fdf4', border: '#27ae60', text: '#1e8449' }, - info: { bg: '#eff6ff', border: '#3C27F5', text: '#3C27F5' }, + info: { bg: '#fff0eb', border: '#F4320B', text: '#cf2808' }, }; const c = colors[type] || colors.error; diff --git a/pages/static/js/docs.js b/pages/static/js/docs.js index 751d75e..d622253 100644 --- a/pages/static/js/docs.js +++ b/pages/static/js/docs.js @@ -1,6 +1,7 @@ const target = document.querySelector('[data-markdown-target]'); const buttons = [...document.querySelectorAll('[data-markdown-source]')]; const toc = document.querySelector('[data-toc-target]'); +const pdfDownload = document.querySelector('[data-pdf-download]'); function slugify(value) { return value @@ -31,6 +32,40 @@ function buildTableOfContents(documentId) { document.querySelector('[data-toc-shell]')?.classList.toggle('hidden', headings.length === 0); } +function renderMarkdownWithMath(markdown) { + const expressions = []; + + function placeholder(expression, displayMode) { + const index = expressions.push({ expression: expression.trim(), displayMode }) - 1; + const tag = displayMode ? 'div' : 'span'; + return `<${tag} data-math-index="${index}">`; + } + + // Protect LaTeX before Markdown parsing so backslashes, braces and underscores + // reach KaTeX exactly as they appear in the source document. + let protectedMarkdown = markdown.replace(/\$\$([\s\S]*?)\$\$/g, (_, expression) => ( + `\n${placeholder(expression, true)}\n` + )); + protectedMarkdown = protectedMarkdown.replace(/\$([^$\n]+?)\$/g, (_, expression) => ( + placeholder(expression, false) + )); + + target.innerHTML = marked.parse(protectedMarkdown, { gfm: true }); + target.querySelectorAll('[data-math-index]').forEach((element) => { + const math = expressions[Number(element.dataset.mathIndex)]; + if (!math) return; + if (typeof katex === 'undefined') { + element.textContent = math.displayMode ? `$$${math.expression}$$` : `$${math.expression}$`; + return; + } + katex.render(math.expression, element, { + displayMode: math.displayMode, + throwOnError: false, + strict: false, + }); + }); +} + async function renderMarkdown(button) { if (!target || !button) return; buttons.forEach((item) => { @@ -43,10 +78,17 @@ async function renderMarkdown(button) { }); target.innerHTML = '

Loading documentation…

'; try { - const response = await fetch(button.dataset.markdownSource); + const sourceUrl = new URL(button.dataset.markdownSource, document.baseURI); + sourceUrl.searchParams.set('v', '20260815-2'); + const response = await fetch(sourceUrl, { cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const markdown = await response.text(); - target.innerHTML = marked.parse(markdown, { gfm: true }); + renderMarkdownWithMath(markdown); + if (pdfDownload) { + pdfDownload.href = button.dataset.pdfSource; + pdfDownload.download = button.dataset.pdfSource.split('/').pop(); + pdfDownload.setAttribute('aria-label', `Download ${button.textContent.trim()} as PDF`); + } buildTableOfContents(button.dataset.documentId); document.title = `${button.textContent.trim()} — MPVRP-CC`; history.replaceState(null, '', `#${button.dataset.documentId}`); diff --git a/pages/static/js/scoreboard.js b/pages/static/js/scoreboard.js index f06550a..d58eb3b 100644 --- a/pages/static/js/scoreboard.js +++ b/pages/static/js/scoreboard.js @@ -35,7 +35,7 @@ async function loadLeaderboard() { tbody.replaceChildren(...data.map(row => { const tr = document.createElement('tr'); - tr.className = row.rank <= 3 ? 'bg-blue-50/40' : 'hover:bg-slate-50'; + tr.className = row.rank <= 3 ? 'bg-[#fff0eb]' : 'hover:bg-stone-50'; [medals[row.rank] ?? row.rank, row.team, Number(row.score).toFixed(2), row.instances_validated, formatDate(row.last_submission)] .forEach((value, index) => { const td = document.createElement('td'); diff --git a/pages/static/js/site.js b/pages/static/js/site.js index 771b8c3..be8633f 100644 --- a/pages/static/js/site.js +++ b/pages/static/js/site.js @@ -1,3 +1,90 @@ +const pageName = window.location.pathname.split('/').pop() || 'index.html'; +const isNestedPage = pageName !== 'index.html' || window.location.pathname.includes('/pages/'); +const siteBase = isNestedPage ? '../' : ''; +const activePage = { + 'documentation.html': 'documentation', + 'tools.html': 'tools', + 'visualisation.html': 'visualisation', + 'scoreboard.html': 'scoreboard', + 'submission.html': 'submission', + 'about.html': 'about', +}[pageName] || 'home'; + +const navigationItems = [ + ['documentation', 'Documentation', `${siteBase}pages/documentation.html`], + ['tools', 'Tools', `${siteBase}pages/tools.html`], + ['visualisation', 'Visualizer', `${siteBase}pages/visualisation.html`], + ['scoreboard', 'Leaderboard', `${siteBase}pages/scoreboard.html`], + ['about', 'About', `${siteBase}pages/about.html`], +]; + +const existingHeader = document.querySelector('body > header'); +if (existingHeader) { + const keepOutOfPrint = existingHeader.classList.contains('no-print'); + existingHeader.className = `floating-site-header fixed inset-x-0 top-3 z-50 px-3 ${keepOutOfPrint ? 'no-print' : ''}`; + existingHeader.style.pointerEvents = 'none'; + existingHeader.style.transition = 'transform .35s ease, opacity .35s ease'; + existingHeader.innerHTML = ` + `; + existingHeader.querySelector('nav').style.pointerEvents = 'auto'; + document.body.classList.add('has-floating-nav'); + document.body.style.paddingTop = '4.5rem'; +} + +const scrollTopButton = document.createElement('button'); +scrollTopButton.type = 'button'; +scrollTopButton.setAttribute('aria-label', 'Back to top'); +scrollTopButton.title = 'Back to top'; +scrollTopButton.className = 'fixed bottom-5 right-5 z-40 flex h-11 w-11 translate-y-4 items-center justify-center rounded-full bg-[#F4320B] text-lg font-bold text-white opacity-0 shadow-xl shadow-orange-950/20 transition duration-300 hover:-translate-y-0.5 hover:bg-[#cf2808] pointer-events-none'; +scrollTopButton.innerHTML = '↑'; +scrollTopButton.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); +document.body.appendChild(scrollTopButton); + +let previousScrollY = window.scrollY; +let scrollFramePending = false; + +function updateScrollControls() { + const currentScrollY = Math.max(window.scrollY, 0); + const scrollingDown = currentScrollY > previousScrollY && currentScrollY > 90; + + if (existingHeader) { + existingHeader.style.transform = scrollingDown ? 'translateY(calc(-100% - 1rem))' : 'translateY(0)'; + existingHeader.style.opacity = scrollingDown ? '0' : '1'; + } + + const showScrollTop = currentScrollY > 360; + scrollTopButton.classList.toggle('opacity-0', !showScrollTop); + scrollTopButton.classList.toggle('translate-y-4', !showScrollTop); + scrollTopButton.classList.toggle('pointer-events-none', !showScrollTop); + scrollTopButton.classList.toggle('opacity-100', showScrollTop); + scrollTopButton.classList.toggle('translate-y-0', showScrollTop); + + previousScrollY = currentScrollY; + scrollFramePending = false; +} + +window.addEventListener('scroll', () => { + if (!scrollFramePending) { + window.requestAnimationFrame(updateScrollControls); + scrollFramePending = true; + } +}, { passive: true }); + document.querySelectorAll('[data-menu-button]').forEach((button) => { button.addEventListener('click', () => { const menu = document.querySelector('[data-mobile-menu]'); @@ -18,7 +105,13 @@ const truckIcon = ` `; document.querySelectorAll('[data-site-brand]').forEach((brand) => { - brand.innerHTML = truckIcon; + brand.innerHTML = `${truckIcon}MPVRP-CC`; + if (brand.closest('.floating-site-header')) { + const icon = brand.querySelector('.brand-icon'); + const svg = brand.querySelector('svg'); + Object.assign(icon.style, { height: '2.4rem', width: '2.4rem', borderRadius: '999px' }); + Object.assign(svg.style, { height: '1.2rem', width: '1.2rem' }); + } }); const githubIcon = ``; diff --git a/pages/static/js/visualisation.js b/pages/static/js/visualisation.js index b0c3985..30f7d61 100644 --- a/pages/static/js/visualisation.js +++ b/pages/static/js/visualisation.js @@ -47,7 +47,7 @@ let hoveredNode = null; let focusedTruckId = null; const TRUCK_COLORS = [ - '#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2', + '#F4320B', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2', '#db2777', '#4f46e5', '#65a30d', '#c026d3', '#0f766e', '#b45309', '#03050a', '#ee99ae', '#15803d', '#7e22ce', '#c2410c', '#0e7490', '#a21caf', '#cac538', '#4d7c0f', '#9d174d', '#0369a1', '#a16207' diff --git a/pages/submission.html b/pages/submission.html index 16678d5..9f01c82 100644 --- a/pages/submission.html +++ b/pages/submission.html @@ -1,3 +1,49 @@ -Submission — MPVRP-CC
-
-

Submission package

First submission: enter name and email. Later submissions only require the same email.

+ + + + + + Submission — MPVRP-CC + + + + + + + + +
+
+
+ +
+
+

Submission package

+

First submission: enter name and email. Later submissions only require the same email.

+
+ + + + +
+
+
+
+
+
+ + + + + diff --git a/pages/tools.html b/pages/tools.html index 5003ef1..b091d84 100644 --- a/pages/tools.html +++ b/pages/tools.html @@ -1,11 +1,11 @@ -Tools — MPVRP-CC -
+Tools — MPVRP-CC +

Developer workspace

-

Interactive visualizer

Inspect every route, trip and product change.

Load an instance and its solution to animate vehicle movements, review depot inventories and understand the distance and changeover costs.

Open visualizer →
+

Interactive visualizer

Inspect every route, trip and product loading.

Load an instance and its solution to animate vehicle movements, review depot inventories, and understand travel and loading-transition costs.

Open visualizer →

Structured instance generator

Difficulty levels control realistic value ranges; the seed makes generation reproducible.

-

Strict solution verifier

Checks route structure, products, mass conservation, demand, stocks and submitted metrics.

-
+

Solution verifier

Checks the routes, capacities, quantities, demands, stocks and product rules. Distances, transition costs and summary metrics are recalculated automatically.

+
diff --git a/pages/visualisation.html b/pages/visualisation.html index eadbd3b..96055cd 100644 --- a/pages/visualisation.html +++ b/pages/visualisation.html @@ -8,10 +8,11 @@ - + + - +