Urban freight simulation based on the MASS-GT multi-agent model
Project description
urban-dollop
A Python package for urban freight simulation based on the MASS-GT multi-agent model.
Consolidates the original six-container LEAD pipeline into a single installable library —
one pip install replaces six Docker images.
Built as academic research: every step's mathematical model is documented alongside the API so the simulation is fully transparent and citable.
Install
pip install urban-dollop
For performance-critical runs (enables Numba JIT on hot loops):
pip install urban-dollop[fast]
Usage
Each pipeline step is a plain function: it accepts data objects and returns a
pandas.DataFrame. No files are written unless you ask — the library produces results;
you decide what to do with them.
Step 1 — Demand generation
Estimate the daily parcel delivery demand for a city from zonal population and employment data.
from urban_dollop import Zone, Depots, CarrierShares, DemandData, generate_demand
data = DemandData(
zones = Zone("zones.gpkg"),
depots = Depots("depots.gpkg"),
skim_time = "skim_time.mtx",
carrier_shares = CarrierShares("shares.csv"),
study_area = ["Joinville"],
)
parcels = generate_demand(data)
parcels is a pandas.DataFrame — one row per parcel:
| column | type | description |
|---|---|---|
parcel_id |
int |
unique parcel identifier |
origin_zone |
int |
zone ID of the originating depot |
destination_zone |
int |
zone ID of the delivery address |
depot_id |
int |
depot that handles this parcel |
carrier |
str |
carrier name (as supplied in carrier_shares) |
vehicle_type |
int |
vehicle type code (7 = van) |
locker_zone |
int |
zone ID if routed to a parcel locker; 0 otherwise |
segment |
str |
"B2C" (business-to-consumer) or "C2C" (consumer-to-consumer) |
local_to_local |
bool |
parcel originates and delivers within the study area |
crowdship_eligible |
bool |
eligible for crowdshipping (always a subset of local_to_local) |
fulfilment_type |
str |
"Hubspoke" or "Hyperconnected" |
Save as CSV:
parcels.to_csv("parcel_demand.csv", index=False)
Save as a GIS layer (QGIS, ArcGIS):
from urban_dollop import demand_layer
# attach zone geometry and aggregate by destination zone
gdf = demand_layer(parcels, data.zones)
gdf.to_file("parcel_demand.gpkg") # drag directly into QGIS
gdf is a geopandas.GeoDataFrame — one row per zone — with the original zone geometry
plus:
| column | description |
|---|---|
parcel_count |
total parcels delivered to this zone |
hubspoke_count |
via standard depot-to-door fulfilment |
hyperconnected_count |
via local / crowdshipping fulfilment |
b2c_count |
business-to-consumer parcels |
c2c_count |
consumer-to-consumer parcels |
local_to_local_count |
parcels that originate within the study area |
Column mapping — when your file uses different names:
Each input type expects canonical column names by default. If your files use different names, supply a mapping — only the columns that differ:
data = DemandData(
zones = Zone("zones.gpkg", columns={
"zone_id": "id",
"households": "hh_count",
"jobs": "employment",
}),
depots = Depots("depots.gpkg", columns={
"carrier": "operator",
}),
skim_time = "skim_time.mtx",
carrier_shares = CarrierShares("shares.csv", columns={
"share": "market_share",
}),
study_area = ["Joinville"],
)
Canonical column names:
| input | column | description |
|---|---|---|
Zone |
zone_id |
unique integer zone identifier |
Zone |
municipality |
municipality name (used for study_area) |
Zone |
households |
household count |
Zone |
jobs |
job / employee count |
Depots |
depot_id |
unique integer depot identifier |
Depots |
zone_id |
zone the depot is located in |
Depots |
carrier |
carrier / operator name |
CarrierShares |
share |
market share fraction (sums to 1) |
Adjust calibration parameters:
from urban_dollop import DemandConfig
parcels = generate_demand(
data,
DemandConfig(
parcels_per_household = 0.178, # default: 0.2054 (MASS-GT / NL)
parcels_per_employee = 0.029, # default: 0.0
local_to_local_fraction = 0.08, # default: 0.04
random_seed = 42,
),
)
Command-line interface
If your data directory contains the canonical input filenames, you can run the simulation without writing any Python:
urban-dollop generate-demand data/
urban-dollop generate-demand data/ --outdir results/
urban-dollop generate-demand data/ --study-area Joinville --outdir results/
The CLI looks for these filenames in <data-dir>:
| file | description |
|---|---|
zones.gpkg |
zone polygons with socioeconomic attributes |
depots.gpkg |
depot / parcel node locations |
skim_time.mtx |
travel-time matrix (binary, seconds) |
carrier_shares.csv |
carrier market share table |
Output written to <data-dir> (or --outdir):
| file | description |
|---|---|
parcel_demand.csv |
one row per parcel |
parcel_demand.gpkg |
parcel counts joined to zone geometry |
Calibration via config file — place urban-dollop.toml in your project root
to override defaults without touching code:
[demand]
parcels_per_household = 0.178 # NL baseline: 0.2054
parcels_per_employee = 0.029 # NL baseline: 0.0
local_to_local_fraction = 0.08 # NL baseline: 0.04
random_seed = 42
If no config file is present, the MASS-GT original defaults are used.
Mathematical model
Step 1 — Parcel demand generation
Zonal demand
The number of parcels destined for zone $z$ on an average weekday:
$$D_z = \left\lfloor \frac{H_z \cdot r_{HH}}{s_{B2C}(z)} + \frac{E_z \cdot r_E}{s_{B2B}(z)} \right\rceil$$
| symbol | DemandConfig field |
description |
|---|---|---|
| $H_z$ | — | households in zone $z$ (households) |
| $E_z$ | — | employees in zone $z$ (jobs) |
| $r_{HH}$ | parcels_per_household |
parcels per household per day |
| $r_E$ | parcels_per_employee |
parcels per employee per day |
| $s_{B2C}^0$ | delivery_success_b2c |
baseline first-attempt success rate, B2C |
| $s_{B2B}^0$ | delivery_success_b2b |
baseline first-attempt success rate, B2B |
When a parcel locker is present in zone $z$, the effective success rate is raised:
$$s_{B2C}(z) = \alpha_z + (1 - \alpha_z), s_{B2C}^0$$
where $\alpha_z = \ell \cdot \mathbf{1}[z \in \mathcal{L}]$, $\mathcal{L}$ is the set of zones with
lockers (locker_zones), and $\ell$ is the locker adoption rate (locker_adoption_rate).
Carrier split
Total 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$ (column share in carrier_shares).
Depot assignment
Each parcel is assigned to the nearest depot of its carrier by travel time:
$$\delta(z, k) = \underset{n \in \mathcal{N}_k}{\arg\min}; t(n, z)$$
where $\mathcal{N}_k$ is the set of depots operated by carrier $k$ and $t(n, z)$ is the travel time in seconds from depot $n$ to zone $z$, read from the pre-computed skim matrix.
Market segment
Each parcel is independently assigned a segment:
$$\text{segment}i \sim \text{Bernoulli}(p{C2C}), \qquad p_{C2C} = \frac{r_{HH,C2C}}{r_{HH}}$$
where $r_{HH,C2C}$ is parcels_per_household_c2c.
Local-to-local, crowdshipping, and fulfilment type
A fraction $f_{L2L}$ of parcels are flagged local-to-local; of those, a fraction $f_{CS}$ are crowdshipping-eligible:
$$\text{L2L}i \sim \text{Bernoulli}(f{L2L})$$
$$\text{CS}_i \sim \text{L2L}i \cdot \text{Bernoulli}(f{CS})$$
For L2L parcels, the origin zone is resampled from the study-area household distribution (reflecting that senders are residents, not distant warehouses):
$$P(\text{origin}i = z) = \frac{H_z}{\displaystyle\sum{z'} H_{z'}}$$
Fulfilment type is then assigned deterministically:
$$\text{fulfilment}_i = \begin{cases} \text{Hyperconnected} & \text{if } \text{L2L}_i \lor \text{CS}_i \lor (\text{locker}_i \neq 0) \ \text{Hubspoke} & \text{otherwise} \end{cases}$$
History and research context
Forked from MASS-GT, a multi-agent simulation system for urban goods transport originally developed for the Dutch Randstad region.
This refactor is part of ongoing academic research with two primary contributions:
-
Unified package. The original six-container LEAD microservice architecture is dissolved into a single in-memory Python pipeline. Shared data (zone maps, skim matrices) is loaded once and passed through all steps without intermediate files.
-
Grade-aware emission accounting. The original COPERT emission model assumes flat roads. This package introduces road gradient as a third coordinate into the emission step via the Vehicle Specific Power (VSP) formulation, enabling topographically accurate emission estimates in hilly cities. The Joinville (Brazil) case study, with elevation ranging from 0 to 881 m, is the primary validation dataset.
The original LEAD Packaging and Modeler Version source files are preserved in tmp/
during the refactoring process.
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.2.0.tar.gz.
File metadata
- Download URL: urban_dollop-0.2.0.tar.gz
- Upload date:
- Size: 267.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b64434a31fcd2ead86220376bed4cda8f15b3c144153dd9a160efa0e14e85596
|
|
| MD5 |
baddb201709cc391efa32225a18a2f70
|
|
| BLAKE2b-256 |
0011efdf993d4d5d14cb794c10d7a7bd9dc33af4f5a10f630a02aea0eceff4fd
|
Provenance
The following attestation bundles were made for urban_dollop-0.2.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.2.0.tar.gz -
Subject digest:
b64434a31fcd2ead86220376bed4cda8f15b3c144153dd9a160efa0e14e85596 - Sigstore transparency entry: 1553486620
- Sigstore integration time:
-
Permalink:
hcubasd/urban-dollop@1283e47d313a732aa5ad8a69506d3cf1cacd1213 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/hcubasd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
deployment.yaml@1283e47d313a732aa5ad8a69506d3cf1cacd1213 -
Trigger Event:
push
-
Statement type:
File details
Details for the file urban_dollop-0.2.0-py3-none-any.whl.
File metadata
- Download URL: urban_dollop-0.2.0-py3-none-any.whl
- Upload date:
- Size: 21.3 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 |
c5f9d8f3f1df736ec122992ab66f8db4ec5913708a71b4a98e2537bacb10f572
|
|
| MD5 |
678437e9681b254d18837f9c8be8a8d7
|
|
| BLAKE2b-256 |
1f9c132f6a46289e62d01900c67d4cee51e04d6d6526e4a994f58ed266f91abf
|
Provenance
The following attestation bundles were made for urban_dollop-0.2.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.2.0-py3-none-any.whl -
Subject digest:
c5f9d8f3f1df736ec122992ab66f8db4ec5913708a71b4a98e2537bacb10f572 - Sigstore transparency entry: 1553486624
- Sigstore integration time:
-
Permalink:
hcubasd/urban-dollop@1283e47d313a732aa5ad8a69506d3cf1cacd1213 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/hcubasd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
deployment.yaml@1283e47d313a732aa5ad8a69506d3cf1cacd1213 -
Trigger Event:
push
-
Statement type: