Urban freight simulation based on the MASS-GT multi-agent model
Project description
urban-dollop
urban-dollop is a Python library that extracts and packages the mathematical models from MASS-GT — a multi-agent urban freight simulation system originally developed at TU Delft for the Dutch Randstad region. The models are reimplemented as a transparent, installable Python pipeline intended for academic research and reproducible urban logistics studies.
The primary novel contribution is the introduction of road grade as a dimension in emission accounting, enabling topographically accurate estimates in hilly cities.
Usage
Parcel demand generation
Estimate the daily parcel delivery demand for a study area from zonal population and employment data.
from urban_dollop import Zone, Depot, Carrier, SkimMatrix, generate_parcel_demand
zones = Zone.from_file("zones.gpkg")
depots = Depot.from_file("depots.gpkg")
carriers = Carrier.from_file("carrier_shares.csv")
skim = SkimMatrix.from_file("skim_time.mtx", zones)
demands = generate_parcel_demand(zones, depots, carriers, skim)
demands is a list[ParcelDemand] — one record per unique
(destination zone, depot) combination:
| field | type | description |
|---|---|---|
destination_zone_id |
int |
zone ID of the delivery address |
depot_id |
int |
depot that handles this flow |
n_parcels |
int |
number of parcels in this flow |
The origin zone of each flow is implicit — it is always the zone of the depot.
Save to CSV (join to your zones layer in QGIS or ArcGIS on destination_zone_id
to map parcels delivered per zone):
ParcelDemand.to_file(demands, "parcel_demand.csv")
Calibration — via urban-dollop.toml:
Place an urban-dollop.toml in your current working directory to set calibration
parameters without touching code:
[parcel_demand]
parcels_per_household = 0.2054 # B2C daily deliveries per household
parcels_per_employee = 0.0 # B2B daily deliveries per employee
delivery_success_b2c = 0.75 # first-attempt success rate, residential
delivery_success_b2b = 0.95 # first-attempt success rate, commercial
Calibration — programmatic override:
Pass a ParcelDemandConfig to override TOML values in code:
from urban_dollop import ParcelDemandConfig
demands = generate_parcel_demand(
zones, depots, carriers, skim,
config=ParcelDemandConfig(
parcels_per_household = 0.178,
parcels_per_employee = 0.029,
delivery_success_b2c = 0.80,
delivery_success_b2b = 0.95,
),
)
Programmatic values take precedence over TOML. Missing fields fall back to TOML. Pydantic raises if a required field is absent from both sources.
Column mapping — when your files use different column names:
zones = Zone.from_file("zones.gpkg", columns={
"zone_id": "id",
"households": "hh_count",
"employment": "jobs",
})
carriers = Carrier.from_file("shares.csv", columns={
"name": "courier",
"share": "market_share",
})
Canonical column names:
| model | field | description |
|---|---|---|
Zone |
zone_id |
unique integer zone identifier |
Zone |
households |
household count |
Zone |
employment |
employee count |
Depot |
depot_id |
unique integer depot identifier |
Depot |
zone_id |
zone the depot is located in |
Depot |
carrier |
carrier name (must match Carrier.name) |
Carrier |
name |
carrier name |
Carrier |
share |
market share fraction (all carriers must sum to 1) |
CLI
Run the parcel demand module from canonical files:
urban-dollop generate-demand data/
This command:
- reads
urban-dollop.tomlfrom the current working directory - reads
zones.gpkg,depots.gpkg,carrier_shares.csv, andskim_time.mtx(orskim_time.mtx.gz) fromdata/ - writes
parcel_demand.csvto the current working directory by default
To write the CSV somewhere else, pass --outdir with either an existing directory
or a full .csv path:
urban-dollop generate-demand --outdir results/ data/
urban-dollop generate-demand --outdir results/joinville_parcel_demand.csv data/
Example output
[!NOTE] Example choropleth of simulated parcel deliveries aggregated by
destination_zone_idfor the Netherlands fixture scenario (5 925 zones, Randstad region). Zone geometry, household counts, and employment are from the MASS-GT LEADVersion dataset (SEGS2016, CBS). Travel times are derived from the MASS-GT Netherlands skim matrix. Depot locations and carrier shares are sourced from the MASS-GT LEADVersion scenario.
Parcel delivery scheduling
Schedule the demand produced by generate_parcel_demand into vehicle tours.
from urban_dollop import (
Depot, ParcelDemand, SkimMatrix, Vehicle,
schedule_parcel_deliveries,
)
depots = Depot.from_file("depots.gpkg")
vehicles = Vehicle.from_file("vehicles.csv")
skim = SkimMatrix.from_file("skim_time.mtx", zones)
demands = ParcelDemand.from_file("parcel_demand.csv")
trips = schedule_parcel_deliveries(demands, depots, vehicles, skim)
trips is a list[DeliveryTrip] — one record per trip leg within a tour:
| field | type | description |
|---|---|---|
tour_id |
int |
unique tour identifier |
trip_id |
int |
sequential leg position within the tour |
depot_id |
int |
depot the tour departs from and returns to |
carrier |
str |
carrier operating the tour |
origin_zone_id |
int |
zone at the start of this leg |
destination_zone_id |
int |
zone at the end of this leg |
n_parcels |
int |
parcels delivered at this stop (0 for the return leg) |
vehicle_id |
int |
vehicle assigned to this tour |
Each tour begins at the depot, visits a sequence of delivery stops, and ends with a
return leg back to the depot (n_parcels = 0).
Save to CSV:
DeliveryTrip.to_file(trips, "delivery_trips.csv")
Calibration — via urban-dollop.toml:
[parcel_scheduling]
seed = 42 # random seed for reproducible tour construction
Calibration — programmatic override:
from urban_dollop import ParcelSchedulingConfig
trips = schedule_parcel_deliveries(
demands, depots, vehicles, skim,
config=ParcelSchedulingConfig(seed=42),
)
Vehicle types — vehicles.csv:
vehicles = Vehicle.from_file("vehicles.csv")
| field | type | description |
|---|---|---|
vehicle_id |
int |
unique vehicle type identifier |
name |
str |
vehicle type label |
max_parcels |
int |
maximum parcel capacity |
The scheduler assigns the smallest vehicle whose capacity fits the tour load. If demand exceeds all vehicle capacities the largest vehicle is used.
CLI
Run the scheduling module from canonical files:
urban-dollop schedule-deliveries data/
This command:
- reads
urban-dollop.tomlfrom the current working directory - reads
zones.gpkg,depots.gpkg,carrier_shares.csv,vehicles.csv,skim_time.mtx(orskim_time.mtx.gz), andparcel_demand.csvfromdata/ - writes
delivery_trips.csvto the current working directory by default
urban-dollop schedule-deliveries --outdir results/ data/
urban-dollop schedule-deliveries --outdir results/netherlands_trips.csv data/
Example output
[!NOTE] Example choropleth of delivery vehicle stops aggregated by
destination_zone_idfor the Netherlands fixture scenario. Each stop represents one vehicle visit to a zone (one trip leg withn_parcels > 0). A zone receiving 6 stops has 6 separate vehicles — one per active carrier — stopping there on their respective tours. Same fixture as the parcel demand example above.
Mathematical models
The following models are extracted from MASS-GT's parcel_dmnd module and implemented
in generate_parcel_demand().
Parcel demand generation
Zonal demand
The number of parcel flows destined for zone $z$ on an average weekday:
$$D_z = \left\lfloor \frac{H_z \cdot r_{HH}}{s_{B2C}} + \frac{E_z \cdot r_E}{s_{B2B}} \right\rceil$$
| symbol | ParcelDemandConfig field |
description |
|---|---|---|
| $H_z$ | — | households in zone $z$ |
| $E_z$ | — | employees in zone $z$ |
| $r_{HH}$ | parcels_per_household |
B2C parcels per household per day |
| $r_E$ | parcels_per_employee |
B2B parcels per employee per day |
| $s_{B2C}$ | delivery_success_b2c |
first-attempt delivery success rate, residential |
| $s_{B2B}$ | delivery_success_b2b |
first-attempt delivery success rate, commercial |
The success rates correct for failed first-attempt deliveries: the generated volume reflects shipments sent, not deliveries completed.
Carrier split
Total zonal demand is distributed across carriers by market share:
$$D_{z,k} = \left\lfloor \sigma_k \cdot D_z \right\rceil$$
where $\sigma_k$ is the market share of carrier $k$ (Carrier.share), and
$\sum_k \sigma_k = 1$. This normalization is required by the implementation and
validated before demand is allocated across carriers.
Depot assignment
Each (zone, carrier) flow is assigned to the nearest depot of that carrier by travel time from the depot zone to the destination zone:
$$\delta(z, k) = \underset{n \in \mathcal{N}k}{\arg\min}\ t(n{\text{zone}}, z)$$
where:
- $\mathcal{N}_k$ is the set of depots operated by carrier $k$
- $t(n_{\text{zone}}, z)$ is the travel time from the depot's zone to zone $z$, read from the pre-computed skim matrix
Aggregation
Flows that share the same destination zone and depot are summed:
$$F_{z,n} = \sum_{k,:,\delta(z,k) = n} D_{z,k}$$
Each resulting $(z, n)$ pair with $F_{z,n} > 0$ becomes one ParcelDemand record.
Parcel delivery scheduling
The following models are implemented in schedule_parcel_deliveries().
Tour construction
Demand records are grouped by depot. Within each depot, records are packed into tours using a greedy bin-packer that respects the maximum vehicle capacity $C$:
$$\sum_{i \in \text{tour}} p_i \leq C$$
where $p_i$ is the parcel count of demand record $i$ and $C$ is the
max_parcels of the largest available vehicle.
Nearest-neighbour routing
Each tour's stop sequence is initialised with a nearest-neighbour heuristic. Starting from the depot zone $d$, the algorithm repeatedly appends the unvisited stop closest in travel time to the current position:
$$\text{next} = \underset{j \in \mathcal{U}}{\arg\min}\ t(\text{current}, j)$$
where $\mathcal{U}$ is the set of unvisited stops and $t(\cdot, \cdot)$ is the travel-time skim.
2-opt improvement
The nearest-neighbour tour is refined with 2-opt. For each pair of edges $(a \to b)$ and $(c \to d)$ in the tour, the swap is accepted if:
$$t(a, c) + t(b, d) < t(a, b) + t(c, d)$$
Passes continue until no improving swap is found or a maximum of 10 passes is reached.
Vehicle assignment
After tour construction, the smallest vehicle whose capacity covers the tour load is selected:
$$v^* = \underset{v,:,C_v \geq \sum_i p_i}{\arg\min}\ C_v$$
Trip generation
Each tour becomes a sequence of DeliveryTrip records. For a tour visiting
stops $s_1, s_2, \ldots, s_m$ departing from depot zone $d$:
$$d \to s_1 \to s_2 \to \cdots \to s_m \to d$$
The final leg $(s_m \to d)$ is the return trip with $p = 0$.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file urban_dollop-0.5.0.tar.gz.
File metadata
- Download URL: urban_dollop-0.5.0.tar.gz
- Upload date:
- Size: 94.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c210dab82862aca385e429b56a49ba2615c734001d75dcc0732dd0c78038cbb
|
|
| MD5 |
f09f930fef11085ebf0ad4b87a894d59
|
|
| BLAKE2b-256 |
4d756aa207fc5a8b394d3280b527859375a326ed35680ce7b4f55603cd6ad312
|
Provenance
The following attestation bundles were made for urban_dollop-0.5.0.tar.gz:
Publisher:
deployment.yaml on hcubasd/urban-dollop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
urban_dollop-0.5.0.tar.gz -
Subject digest:
3c210dab82862aca385e429b56a49ba2615c734001d75dcc0732dd0c78038cbb - Sigstore transparency entry: 1761735883
- Sigstore integration time:
-
Permalink:
hcubasd/urban-dollop@b0de511d3d2967c5051c65e353244c9862c19ca6 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/hcubasd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
deployment.yaml@b0de511d3d2967c5051c65e353244c9862c19ca6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file urban_dollop-0.5.0-py3-none-any.whl.
File metadata
- Download URL: urban_dollop-0.5.0-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4979b17227264bb5aaa8006df593eecf1984058e44459325d0690ca11f15ca31
|
|
| MD5 |
1405f0396c689ddaf0d9fc40fbc98406
|
|
| BLAKE2b-256 |
40db463a2aa3a9ee330b92efcc0df0cf43b1230afb280930f144b6dcbbb640d9
|
Provenance
The following attestation bundles were made for urban_dollop-0.5.0-py3-none-any.whl:
Publisher:
deployment.yaml on hcubasd/urban-dollop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
urban_dollop-0.5.0-py3-none-any.whl -
Subject digest:
4979b17227264bb5aaa8006df593eecf1984058e44459325d0690ca11f15ca31 - Sigstore transparency entry: 1761735986
- Sigstore integration time:
-
Permalink:
hcubasd/urban-dollop@b0de511d3d2967c5051c65e353244c9862c19ca6 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/hcubasd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
deployment.yaml@b0de511d3d2967c5051c65e353244c9862c19ca6 -
Trigger Event:
push
-
Statement type: