Skip to main content

urban-dollop

flowchart LR
    zones[zones] --> agents
    demand_effects[demand-effects] --> demand[demand]
    demand_thresholds[demand-thresholds] --> demand
    supply_effects[supply-effects] --> supply[supply]
    supply_thresholds[supply-thresholds] --> supply
    capacity_effects[capacity-effects] --> capacities[capacities]
    capacity_thresholds[capacity-thresholds] --> capacities
    need_effects[need-effects] --> needs[needs]
    need_thresholds[need-thresholds] --> needs
    demand --> agents[agents]
    supply --> agents
    capacities --> agents
    needs --> agents
    agents --> desire_lines[desire-lines]
    desire_lines --> network_loads
    departures[departures] --> network_loads
    time_intervals[time-intervals] --> network_loads
    dwell_times[dwell-times] --> network_loads
    vehicle_velocities[vehicle-velocities] --> network_loads
    vehicle_capacities[vehicle-capacities] --> network_loads
    consolidation_radii[consolidation-radii] --> network_loads
    road_capacities[road-capacities] --> network_loads
    alternative_specific_constants[alternative-specific-constants] --> network_loads
    network[network] --> network_loads[network-loads]
    vehicles[vehicles] --> network_loads
    network --> network_emissions
    vehicles --> network_emissions
    network_loads[network-loads] --> network_emissions[network-emissions]
    copert_v_coefficients[copert-v-coefficients] --> network_emissions
    emission_factors[emission-factors] --> network_emissions

synth zones

A GeoDataFrame of Voronoi-tessellated zone polygons over the unit square, one row per zone_id, written to zones.gpkg. zone_id values are plain integers (1, 2, ...), matching the convention effects.csv uses for its own zone_id stratum.

zone_id geometry
1 POLYGON ((0.41 0.09, 0.63 0.22, ...))
2 POLYGON ((0.12 0.55, 0.41 0.09, ...))

Synthesis

Fully self-contained, same leaf contract as every other command. Run it with nothing on disk yet and it invents both the zone count (ceil(lognormal(0, sigma)), the only place --sigma acts) and the labels, then tessellates geometry for them.

Alternatively, hand it a zones.gpkg that already has a zone_id column (real IDs are welcome, and can be integers or strings) with geometry left entirely empty, and it synthesizes geometry for exactly those zones, in that order -- the count comes from the shape you gave it, not from --sigma. A file with geometry already populated anywhere is left alone and the command is a no-op: it doesn't matter whether every zone has geometry or only some do, any real geometry at all means the file is complete (partial geometry would mean regenerating some zones' shapes but not others, which makes no sense for a tessellation -- unlike effects.csv, there's no legitimate reason for geometry to be partially populated on purpose). Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has a zone_id list has nothing left for it to control.

synth supply-effects / synth demand-effects / synth capacity-effects / synth need-effects

Coefficients for a textbook additive (main-effects, no interactions among stratum dimensions) ordered-logit linear predictor -- the cumulative logit / proportional odds model of McCullagh (1980). Output is wide: one row per (stratum, stratum value) pair, one column per resource.

stratum stratum_value resource_1 resource_2
zone_id 1 0.734 -0.310
zone_id 2 -0.051 0.884
stratum_1 value_1 0.512 -0.204

A stratum value can repeat across different strata (value_1 could belong to both stratum_1 and stratum_2) -- values are only unique within their own stratum, not across the file. Each resource column is its own independent linear predictor ($\beta$), the sum of whichever rows apply to a given stratum combination for that resource -- a resource column indexes which independent model a cell belongs to, not a covariate on a shared one. A resource cell can be empty even in an otherwise-complete file: a stratum genuinely may not participate in every resource's market (a zone that supplies grain may not supply parcels at all), so an empty cell there is a real, permanent "not applicable," not a placeholder waiting to be filled. The stratum shape (which dimensions exist, how many values each has) stays shared across resources, but which rows actually carry a given resource's column at all is independent per resource -- it's participation, not shape, that varies.

supply/demand describe a stratum's aggregate resource totals; capacity/need describe the size distribution of individual agents drawn from that stratum later -- different questions, same statistical machinery, four independent files.

Synthesis

Every effect value is drawn from a standard normal, Normal(0, 1), fixed regardless of --sigma -- it is the value being synthesized, not a count of how much to synthesize. Mean zero is not an arbitrary default: with no reference category dropped and no separate intercept anywhere in this design, each effect is a random effect (in the mixed-model sense), and mean zero is exactly what makes that identifiable rather than an unmeasurable duplicate of an intercept that doesn't exist here.

Each of these four commands is fully self-contained -- none of them look at any other file. Run one with nothing on disk yet and it synthesizes shape and values together: dimension count, each dimension's value count, and resource count are all ceil(lognormal(0, sigma)), uncapped, zone_id always included. zone_id values are plain integers (1, 2, ...); every other dimension gets string labels (value_1, value_2, ...) -- zone_id is the one stratum guaranteed to exist and the one with a natural real-world numeric identity, so it's the one place that distinction is made. --sigma 0 collapses this to the smallest possible draw: one dimension (zone_id), one value, one resource column.

Each resource independently applies to a uniformly-random non-empty subset of the invented rows (size drawn uniform over [1, n], membership drawn without replacement) -- never every row automatically, since that would silently assume every stratum participates in every resource's market. This is deliberately not controlled by --sigma: choosing how many of an already-fixed set of rows to include is a bounded selection problem, not an unbounded count to invent, so it doesn't fit the ceil(lognormal(0, sigma)) pattern used everywhere else -- capping a lognormal draw at n would just pile up mass at the cap.

Alternatively, hand it a file that already has stratum/stratum_value filled in plus one or more resource columns (real names and values are welcome here). If every resource cell in the file is empty, it's shape-only -- the command fills every one of them, leaving the shape untouched. If any resource cell already has a value, the file is treated as complete and the command does nothing at all: it does not fill the remaining empty cells, because by that point an empty cell no longer means "not yet decided" -- it means "this stratum doesn't participate in this resource," and that's not a leaf command's call to make or overwrite once real data exists. stratum must contain strings; stratum_value must be a string for every row except zone_id's, which may be int or string -- resource columns are identified by header name, not by dtype, so this isn't needed for disambiguation, it's just that only zone_id has a real-world numeric identity worth allowing. Filled resource cells must contain floats. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls shape invention, and a file that already has shape has nothing left for it to control.

synth supply-thresholds / synth demand-thresholds / synth capacity-thresholds / synth need-thresholds

Cutpoints ($\mu$) for the same cumulative-logit model: a resource with $n$ ordered levels gets n - 1 thresholds, partitioning a latent continuous scale into the n discrete outcomes.

resource resource_level threshold
resource_1 0 0.432
resource_1 2 1.738
resource_1 3 (empty)
resource_2 4 -0.220
resource_2 9 (empty)

resource must contain strings; resource_level must contain integers only -- a resource is always counted in whole units (finer granularity means switching to a smaller unit, e.g. tonnes to kilograms, not a fractional level). resource_level is a genuine numeric quantity, not a category label -- it is the actual amount of the resource at that outcome, used directly in a weighted sum downstream, even though it plays the same shape-defining role stratum_value plays for effects. Levels are always distinct within a resource (a repeated level would mean two ordinal categories mapping to the identical real-world quantity, which has no meaningful interpretation) and always ascending; the largest level in a resource has no upper threshold, hence empty.

Synthesis

Every threshold is Normal(0, 1), sorted ascending per resource, fixed regardless of --sigma -- a value, not a count, same reasoning as effect above. Resource count and level count per resource are both ceil(lognormal(0, sigma)), uncapped -- genuine counts, never zero. Level values are different: drawn from the same --sigma as the count that determined how many are needed, repeated until the required number of levels is distinct within that resource, but shifted down by one (ceil(lognormal(0, sigma)) - 1) so 0 is reachable -- unlike a count of things to synthesize, a resource level legitimately starts at zero (the "none of this resource" outcome), and needs a real threshold between zero and whatever level comes next. resource_level plays the same shape-defining role stratum_value plays for effects, even though it's numeric.

Same self-contained contract as the effects commands: nothing on disk synthesizes shape and values together; a file with resource/resource_level filled in and threshold entirely empty gets just its thresholds filled; anything with threshold already set anywhere is left alone and the command throws. Passing --sigma against a file that already exists throws too, same reasoning as effects.

synth supply / synth demand / synth capacities / synth needs

Combines a pair of upstream files (supply_effects.csv + supply_thresholds.csv, and so on for the other three) into the actual ordered-logit output. Unlike every other command, this one invents nothing -- it's a pure function of already-computed data, so --sigma has no meaning here at all: passing it throws unconditionally, not just when the output file already exists.

Only resources present in both the effects file (as a column) and the thresholds file (as a resource value) get combined -- a resource missing from either side has nothing to evaluate it against. For each such resource, a full stratum combination (the cartesian product of every dimension in effects.csv, e.g. every zone_id × every stratum_1 value) needs every constituent dimension-value's effect to compute a beta; if even one is missing, that whole combination is omitted for that resource -- never given a zero-contribution stand-in. resource_level = 0 is itself a real, reachable outcome, so it can never double as an "undefined" marker; omission and an explicit computed 0 stay distinguishable all the way through.

supply.csv/demand.csv are wide: one column per stratum dimension plus one column per combined resource, holding the rounded expected value of that resource's distribution -- resource is always counted in whole units, so a fractional expected value isn't a deliverable quantity. A combination with no computable value for a resource simply doesn't get that column set for that row.

capacities.csv/needs.csv are wide on stratum dimensions but long on resource: explicit resource, resource_level, probability columns, multiple rows per stratum combination. Nothing gets rounded or collapsed here -- this is the actual distribution individual agents get sampled from later, so it has to stay a distribution.

supply.csv / demand.csv:

zone_id stratum_1 resource_1 resource_2
1 value_1 12 5
1 value_2 8 (empty)
2 value_1 3 1

capacities.csv / needs.csv:

zone_id stratum_1 resource resource_level probability
1 value_1 resource_1 0 0.412
1 value_1 resource_1 2 0.588
1 value_1 resource_2 4 1.000

Synthesis

Deterministic, not random: the same effects + thresholds always produce the same output. Because of that, once the output file exists, the command is a no-op rather than overwriting it -- same protective posture as everywhere else in this arc (a user may have hand-calibrated the output against real totals after generation), even though a fresh recompute against unchanged inputs would give an identical file anyway. Delete the output file to force a regenerate. If either upstream file is still absent or shape-only, the command throws with a pointer to which leaf command to run first.

synth agents

Combines supply.csv, demand.csv, capacities.csv, needs.csv, and zones.gpkg -- five independently-synthesized files -- into agents.gpkg: one row per synthesized agent, with agent_id, geometry, the stratum dimension columns, and {resource}_capacity / {resource}_need for every usable resource.

A stratum combination is only usable if its dimension set matches exactly across all four data files -- not just overlaps. A dimension present in one file but not another means their notion of "stratum" isn't even the same shape, so under pure independent synthesis (no aligned shape-only input across the four upstream commands) this intersection can easily be sparse or empty; that's expected, not a bug -- --sigma 0 collapses every file to the same single minimal stratum, so it always matches there, but larger, independent --sigma draws make agreement increasingly unlikely by construction. Zone identity is matched separately against zones.gpkg, normalized on both sides to guard against a numeric zone_id silently stringifying on a CSV round-trip in a file that also has string-labeled sibling dimensions.

A resource is usable for a stratum combination only if it has a real value in all four files for that exact combination: supply and demand each need a defined aggregate (not an omitted cell), and capacities and needs each need an actual distribution. An agent is a single coherent record needing a valid draw for every usable resource at once, so a resource missing from even one of the four isn't included for that stratum at all -- there'd be no way to give an agent a well-defined value for it.

agent_id zone_id stratum_1 resource_1_capacity resource_1_need geometry
1 1 value_1 6 0 POINT (0.42 0.71)
2 1 value_1 0 4 POINT (0.38 0.65)
3 2 value_2 3 0 POINT (0.81 0.10)

Synthesis

Agents are drawn one at a time per stratum, depleting that stratum's supply/demand budget as they go. Each draw independently truncates the capacity distribution to the remaining supply and the need distribution to the remaining demand, renormalizes, and draws one value from each via uniform(0, 1) -- capacity and need for the same resource are independent draws, not correlated. Generation for a stratum halts the moment any resource can't produce a feasible draw (its distribution's smallest level exceeds what's left) -- a single agent is one coherent record, so if even one resource can't be given a valid value, no valid agent can be produced, and every later attempt would only face equal-or-worse depletion. It also halts after committing an agent that made zero progress on every tracked resource simultaneously (e.g. a resource whose only synthesized level in that stratum is 0, which is always feasible and never depletes anything) -- otherwise that state would repeat forever by the same idempotency the truncation itself relies on. A depleting-toward-zero-need agent is a completely ordinary, expected outcome, not a stopping condition by itself.

Agent placement: uniform random (x, y) in the zone polygon's bounding box, rejected and redrawn until the point actually falls inside the polygon.

Like synth supply and friends, this invents nothing new (agent count is emergent from the depletion loop, not a synthesized shape), so --sigma throws unconditionally, and the command is a no-op once agents.gpkg already exists -- even more important here than for the pure combiners, since agent synthesis has genuine randomness: an accidental re-run wouldn't just redundantly recompute the same file, it would silently replace the whole agent population with a different random draw.

synth desire-lines

Combines pairs of agents from agents.gpkg into desire_lines.gpkg: one row per resource transaction between two agents -- resource, quantity, origin_agent_id, and a 2-point LineString directed from the provider (capacity side) to the consumer (need side). The same two agents can produce more than one line, one per resource they trade, or even more than one line for the same resource across separate draws -- each transaction is its own row regardless of who's involved.

origin_agent_id identifies the supplying depot for network-loads. The consumer's location is the line endpoint; downstream consolidation groups nearby endpoints around a selected consumer, so neither a destination zone nor consumer ID is carried.

agents.gpkg alone has everything needed: capacity, need, zone, and location, for every resource that survived Layer 3 (found the same way Layer 3 finds them -- matching {resource}_capacity/{resource}_need column pairs, no other file read). There is no batch_sizes.csv anymore -- see Synthesis below for why.

resource quantity origin_agent_id geometry
resource_1 4 3 LINESTRING (0.42 0.71, 0.81 0.10)
resource_1 2 7 LINESTRING (0.38 0.65, 0.90 0.22)
resource_2 1 5 LINESTRING (0.81 0.10, 0.42 0.71)

Synthesis

For each resource independently: the currently more-constrained side (whichever has less total remaining across all agents, capacity or need) picks a primary agent weighted by their own remaining value; the other side picks a secondary agent weighted by their remaining value times a distance decay from the primary agent's location (logistic(-distance / scale) -- the same sigmoid used for the ordered-logit models elsewhere in this pipeline, reused here purely as a spatial gravity term, not a probability -- closer agents are more likely paired). scale is the agent cloud's own bounding-box diagonal, computed once: a bare logistic(-distance) silently assumes distances are already O(1), true only for unit-square synthesis -- real agent geometry (e.g. a user-supplied agents.gpkg in metres) would either overflow math.exp past a few hundred units apart, or, well before that, collapse the decay to near-deterministic nearest-neighbor pairing. Normalizing by the cloud's own extent keeps the decay's shape the same regardless of what units the geometry is in. Self-pairing is excluded by agent id, not by coincidental location. The transacted quantity is min of the two specific agents' remaining values on their respective sides -- that's the batch size now, derived from the actual pair instead of sampled from an independent distribution, which is why batch_sizes.csv is gone: whichever agent runs out first is the batch size for that transaction. Both agents' remaining values are depleted by that quantity, and every quantity is recomputed -- which side is constrained, and every candidate's weight -- fresh on every iteration, since depleting one pair changes the totals for everyone.

The loop for a resource stops once either total (recomputed each pass) hits zero. Unlike synth agents' stopping condition, no separate "made zero progress" check is needed here: weight is the remaining value directly (not a probability of drawing a value that happens to be zero), so a zero-remaining agent has zero weight and can never be selected, so every drawn pair has strictly positive remaining on both sides and every iteration depletes a real, positive amount. Termination is guaranteed by construction.

Same deterministic-consumer-with-real-randomness contract as synth agents: --sigma throws unconditionally (nothing here is an invented shape either), and the command is a no-op once desire_lines.gpkg already exists, for the same reason -- the pairing draws are genuinely random, so a re-run would silently produce a different set of lines rather than recomputing the same ones.

synth network

A GeoDataFrame of road links, written to network.gpkg: link_id, grade, road_type, oneway, and a 2-point LineString. Self-contained -- never reads any other file.

link_id grade road_type oneway geometry
0 2.145 road_type_1 True LINESTRING (0.12 0.88, 0.53 0.41)
1 -4.732 road_type_2 False LINESTRING (0.53 0.41, 0.77 0.09)
2 0.318 road_type_1 True LINESTRING (0.77 0.09, 0.12 0.88)

Synthesis

Points are scattered uniformly over the unit square and connected by Delaunay triangulation, which needs at least 3 non-degenerate points -- below that, the two smaller cases are handled directly rather than attempted: zero or one point produces zero edges (nothing to connect), and exactly two points are connected directly to each other, skipping triangulation. random_count(sigma) is the only place --sigma acts, for both the point count and the size of the invented road_type vocabulary (road_type_1, road_type_2, ...); grade (triangular(-6.0, 6.0, 0.0), unchanged from the original implementation) is a value, not a count, and stays fixed regardless of sigma.

oneway is a plain boolean rather than a three-way flag. A LineString's start and end are just two points with no inherent direction of travel, so when a link is one-way the start/end order is chosen (a coin flip) to already match the allowed direction, rather than storing an arbitrary order alongside a flag saying whether to walk it backward -- the same footgun OSM's oneway=-1 tag exists to patch, avoided here by controlling construction instead of correcting it after the fact. When oneway is false, both directions are legal and the stored order is immaterial. grade is always relative to whichever order ends up stored -- the reverse direction is its negation, not an independently sampled value, since a road's slope is one physical fact, not two.

Alternatively, hand it a network.gpkg that already has geometry entirely populated (a real road network extract, for instance) with grade/road_type/oneway left entirely empty, and it synthesizes those attributes for exactly that geometry, in that order -- ignoring --sigma for the point/edge count, since there's nothing left for it to invent there. link_id isn't part of this contract either way -- it's a plain positional index, not a real identifier, so it's always reassigned fresh, the same way agent_id is never something a caller supplies. One asymmetry worth naming: for invented geometry, oneway's start/end choice is guaranteed correct by construction (see above); for given geometry, a one-way link's "forward" direction is just whatever the input's coordinate order already is -- there's no way to guarantee that matches anything real without having authored the geometry ourselves, an unavoidable property of the shape-only case rather than a gap in this logic. A file with any of grade/road_type/oneway already populated anywhere is left alone and the command is a no-op: partial attribute filling would mean regenerating some links' attributes but not others, which has no well-defined meaning. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has geometry has nothing left for it to control.

synth time-intervals

One row per time interval, written to time_intervals.csv: a label (interval_1, interval_2, ... when invented) and a duration. Self-contained -- never reads any other file.

time_interval duration
interval_1 1.158
interval_2 2.030
interval_3 0.487

Synthesis

random_count(sigma) is the only place --sigma acts, for the interval count. duration is a value, not a count, and its distribution is deliberately unassuming (lognormvariate(0.0, 1.0), fixed regardless of sigma): the simulation period and what a single interval represents (an hour, a day, a month) are entirely up to whoever uses this data, so there's no canonical shape worth preserving or inventing around.

Row order is not incidental: network_loads treats file order as chronological order, walking intervals in the order they appear rather than parsing the label, so rows are always written interval_1, interval_2, ... in that order and never shuffled.

Alternatively, hand it a time_intervals.csv that already has time_interval filled in (real labels like AM_peak/midday are welcome) with duration left entirely empty, and it synthesizes durations for exactly those labels, in that order -- the count and the labels come from what you gave it, not from --sigma. A file with duration already populated anywhere is left alone and the command is a no-op: partial duration filling would mean regenerating some intervals' durations but not others, which has no well-defined meaning. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has interval labels has nothing left for it to control.

synth departures

One row per (resource, time_interval) a resource departs in, written to departures.csv: resource, time_interval, and probability -- each resource's probabilities sum to 1 across the intervals it uses. Self-contained -- never reads any other file, so its own interval labels are independent of time_intervals.csv, invented or given.

resource time_interval probability
resource_1 interval_1 0.214
resource_1 interval_3 0.786
resource_2 interval_2 1.000

Synthesis

random_count(sigma) is the only place --sigma acts, twice: once for n_resources, and once for the size of a shared time-slot menu (interval_1, interval_2, ...). That menu is a property of the simulated period itself, decided once, not derived from any one resource's needs -- unlike the old version of this file, which sized the menu after the fact as the max of independently-drawn per-resource counts. Each resource then draws a random_subset of that menu -- bounded selection from an already-fixed set, not new invention, so deliberately not sigma-driven either, the same "doesn't necessarily participate in everything" pattern random_strata uses for resource/stratum participation.

Within a resource's chosen intervals, probabilities come from the same canonical ordered logit used throughout this pipeline (McCullagh 1980): a single beta and len(intervals) - 1 sorted cutpoints (mus), both Normal(0, 1) and fixed regardless of sigma, since they're values, not counts. logistic(mu - beta) turns the cutpoints into cumulative probabilities from 0 to 1; consecutive differences are the probability mass for each interval, in the same sorted order the intervals were drawn in.

Alternatively, hand it a departures.csv that already has resource/time_interval filled in (real resource names and which intervals each one uses) with probability left entirely empty, and it fills probabilities for exactly those pairs, grouped by resource -- the shape comes from what you gave it, not from --sigma. The row order within a resource's given pairs doesn't need to mean anything -- unlike time_intervals.csv, this file makes no chronological claim, so pairs are grouped and filled in whatever order they're given. A file with probability already populated anywhere is left alone and the command is a no-op: probability is filled per resource-group internally, but eligibility to fill anything at all is still a file-wide, all-or-nothing check, same as every other leaf command -- partial filling would mean regenerating some resources' distributions but not others, which has no well-defined meaning. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has resource/interval pairs has nothing left for it to control.

synth dwell-times

One row per resource, written to dwell_times.csv: dwell_time (how long a vehicle dwells before its return trip) and load_pct (the fraction of load carried back). Self-contained -- never reads any other file.

resource dwell_time load_pct
resource_1 0.915 0.329
resource_2 2.481 0.774

Synthesis

random_count(sigma) is the only place --sigma acts, for the resource count. dwell_time and load_pct are both values, not counts, and stay fixed regardless of sigma: dwell_time gets the same deliberately unassuming lognormvariate(0.0, 1.0) treatment as time-intervals' duration (a positive time span with no canonical shape to preserve), and load_pct is already the canonical choice for "a fraction in [0, 1)" as plain random.random(), nothing legacy to migrate there.

Alternatively, hand it a dwell_times.csv that already has resource filled in (real resource names are welcome) with dwell_time/load_pct left entirely empty, and it fills both for exactly those resources -- checked jointly, since they're synthesized together, so one column filled while the other isn't is rejected rather than partially accepted. A file with dwell_time/load_pct already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has resource labels has nothing left for it to control.

synth vehicle-velocities

One row per (vehicle, road_type) pair, written to vehicle_velocities.csv: vehicle, road_type, and a velocity. Self-contained -- never reads any other file.

vehicle road_type velocity
vehicle_1 road_type_1 1.563
vehicle_1 road_type_2 0.842
vehicle_2 road_type_1 2.104

Synthesis

random_count(sigma) is the only place --sigma acts, for n_vehicles and n_road_types independently -- their full cross product is what gets a row each. velocity is a value, not a count, and gets the same deliberately unassuming lognormvariate(0.0, 1.0) treatment as time-intervals' duration and dwell-times' dwell_time.

Alternatively, hand it a vehicle_velocities.csv that already has vehicle/road_type filled in (real vehicle and road types are welcome) with velocity left entirely empty, and it fills velocities for exactly those pairs, in that order -- the shape comes from what you gave it, not from --sigma. Given pairs don't need to be a full cross product: a caller who knows a boat has no meaningful velocity on highway can simply omit that pair, rather than being forced to enumerate every combination the way full synthesis does. A file with velocity already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has vehicle/road_type pairs has nothing left for it to control.

synth vehicle-capacities

One row per (vehicle, resource) pair, written to vehicle_capacities.csv: vehicle, resource, and a capacity. Self-contained -- never reads any other file.

vehicle resource capacity
vehicle_1 resource_1 4
vehicle_1 resource_2 1
vehicle_2 resource_1 7

Synthesis

random_count(sigma) is the only place --sigma acts, for n_vehicles and n_resources independently -- their full cross product is what gets a row each. capacity is a value, not a count, despite needing the same "positive whole number" shape a count needs -- a vehicle carries 12 pallets, not 12.7, but that's a domain constraint on this value's type, not a reason to tie it to sigma (unlike resource_level in capacities.csv/needs.csv, which legitimately is sigma-driven because it plays a dual shape-defining role capacity doesn't have here). So capacity is fixed regardless of sigma, drawn with the same shape random_count uses (ceil(lognormvariate(0, 1))) but as a literal, not parameterized by --sigma.

Alternatively, hand it a vehicle_capacities.csv that already has vehicle/resource filled in (real vehicles and resources are welcome) with capacity left entirely empty, and it fills capacities for exactly those pairs, in that order -- the shape comes from what you gave it, not from --sigma. Given pairs don't need to be a full cross product, same reasoning as vehicle-velocities. A file with capacity already populated anywhere is left alone and the command is a no-op; whichever state the file is in, every non-empty capacity must be a whole number. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has vehicle/resource pairs has nothing left for it to control.

synth consolidation-radii

One row per (vehicle, resource) pair, written to consolidation_radii.csv: vehicle, resource, and a positive radius in the shared agents/network coordinate units.

vehicle resource radius
vehicle_1 resource_1 1.126
vehicle_1 resource_2 0.382

Synthesis

This independent leaf follows vehicle-capacities: with no file it independently synthesizes vehicle and resource counts with random_count(sigma), takes their cross product, and draws each unbounded positive radius from lognormvariate(0, 1). --sigma controls only the vehicle/resource shape, never radius values. A supplied shape-only file with distinct vehicle/resource pairs and an entirely empty radius column is filled in place; a complete file is preserved. Network loads uses the intersection of this file with its other vehicle/resource inputs, so a missing radius makes a vehicle ineligible for that resource.

synth road-capacities

One row per road type, written to road_capacities.csv: road_type and a capacity. Self-contained -- never reads any other file.

road_type capacity
road_type_1 2.001
road_type_2 0.667

Synthesis

random_count(sigma) is the only place --sigma acts, for the road type count. capacity is a value, not a count, and gets the same deliberately unassuming lognormvariate(0.0, 1.0) treatment as vehicle-velocities' velocity -- a plain continuous PCU (passenger-car-unit) figure, no whole-number constraint the way vehicle-capacities' capacity had.

Alternatively, hand it a road_capacities.csv that already has road_type filled in (real road types are welcome) with capacity left entirely empty, and it fills capacities for exactly those road types, in that order -- the count comes from the shape you gave it, not from --sigma. A file with capacity already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has road type labels has nothing left for it to control.

synth alternative-specific-constants

One row per (vehicle, resource) pair, written to alternative_specific_constants.csv: vehicle, resource, and an alternative_specific_constant. Self-contained -- never reads any other file.

vehicle resource alternative_specific_constant
vehicle_1 resource_1 -0.368
vehicle_1 resource_2 1.569
vehicle_2 resource_1 0.766

Synthesis

random_count(sigma) is the only place --sigma acts, for n_vehicles and n_resources independently -- their full cross product is what gets a row each. alternative_specific_constant is the discrete-choice model's ASC: the baseline utility of choosing this vehicle for this resource before time/distance are factored in. Unlike vehicle-capacities' capacity, it's genuinely unrestricted in sign by definition -- one alternative is typically normalized to zero and the rest float above or below it -- so it gets the same Normal(0, 1) treatment as every other effect coefficient in this pipeline (supply-effects and friends), fixed regardless of sigma since it's a value, not a count.

Alternatively, hand it an alternative_specific_constants.csv that already has vehicle/resource filled in (real vehicles and resources are welcome) with alternative_specific_constant left entirely empty, and it fills constants for exactly those pairs, in that order -- the shape comes from what you gave it, not from --sigma. Given pairs don't need to be a full cross product, same reasoning as vehicle-velocities. A file with alternative_specific_constant already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has vehicle/resource pairs has nothing left for it to control.

synth vehicles

One row per vehicle, written to vehicles.csv: vehicle, vehicle_type, bpr_alpha, bpr_beta, time_coefficient, distance_coefficient, and pcu. Self-contained -- never reads any other file. vehicle_type isn't used anywhere in this pipeline yet, but emission-factors needs it once it's built.

vehicle vehicle_type bpr_alpha bpr_beta time_coefficient distance_coefficient pcu
vehicle_1 vehicle_type_3 0.206 2.755 -0.299 -0.607 0.849
vehicle_2 vehicle_type_6 1.238 6.361 -2.075 -0.162 0.436

Synthesis

random_count(sigma) is the only place --sigma acts, for the vehicle count and for the size of the vehicle_type vocabulary (vehicle_type_1, vehicle_type_2, ...) each vehicle draws from -- the vocabulary size stays sigma-driven even when a vehicle_list is given (shape-only mode below), the same way network's road_type vocabulary stays sigma-driven even when geometry is given: --sigma always throws once vehicles.csv already exists, so a caller can never actually exercise that control either way.

bpr_alpha/bpr_beta are the BPR congestion function's parameters (t = t0 * (1 + alpha * (v/c)^beta)), and unlike every other positive value in this pipeline, they're triangular, not lognormal. Both have a well-known real convention (the 1964 Bureau of Public Roads defaults, alpha=0.15, beta=4) and a well-known calibrated range in the transportation literature (roughly alpha in [0.05, 2], beta in [2, 10]), so triangular's hard bounds plus a mode at the standard value map onto that directly -- bpr_alpha = triangular(0.05, 2.0, 0.15), bpr_beta = triangular(2.0, 10.0, 4.0) -- the same reasoning network's grade uses triangular(-6, 6, 0) instead of an unbounded distribution.

time_coefficient/distance_coefficient are the discrete-choice model's marginal utility of route time/distance. Unlike alternative_specific_constant, these aren't sign-free: a longer route should never look more attractive, so both are forced negative (-lognormvariate(0.0, 1.0)) -- a positive draw would be a real modeling error, not just an unusual value. No real-world magnitude convention exists for them the way BPR's parameters have one, since route time/distance are in this pipeline's own arbitrary units, so only the sign is constrained, not the magnitude.

pcu (passenger-car-unit equivalent) stays a plain unassuming positive value (lognormvariate(0.0, 1.0)) -- median 1.0 already coincides with the real PCU baseline for a car, and there's no way to tie it more precisely to vehicle_type here since those labels are synthetic, not real categories with known PCU multipliers.

Alternatively, hand it a vehicles.csv that already has vehicle filled in (real vehicle names are welcome) with the other six columns left entirely empty, and it fills all six for exactly those vehicles, in that order -- checked jointly, since they're synthesized together, so some columns filled while others aren't is rejected rather than partially accepted. A file with those six columns already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has a vehicle list has nothing left for it to control.

synth network-loads

Combines eleven upstream files -- network.gpkg, desire_lines.gpkg, departures.csv, time_intervals.csv, dwell_times.csv, vehicles.csv, vehicle_velocities.csv, vehicle_capacities.csv, consolidation_radii.csv, road_capacities.csv, and alternative_specific_constants.csv -- into network_loads.csv: one row per (link, time interval, resource, vehicle, direction) the simulation actually put traffic on.

link_id time_interval resource vehicle forward vehicle_count velocity load_pct
19 morning parcels van True 2 0.919 1.000
16 morning parcels van True 1 5.298 0.400
28 evening parcels van False 1 0.820 0.500

vehicle_count is a whole number of vehicles: presence on a link during an interval is a yes-or-no question, not a fraction. velocity is that vehicle's actual velocity there (grade- and congestion-adjusted, averaged if several of its trips crossed the same link that interval), and load_pct is the mean fraction of capacity those vehicles were carrying -- below 1 for a part-loaded final vehicle in a run, and for return trips, which run at dwell_times.csv's load_pct.

forward says which way the traffic went: True along the link's own start-to-end coordinate order, False against it. A two-way link travelled both ways in one interval gets two rows, not one. Splitting rather than pre-summing is what keeps the grade's sign recoverable downstream -- grade is stored relative to that same start-to-end order, so a backward traversal negates it, and network-emissions has no way to reconstruct that from a merged row. It also stops two genuinely different velocities being averaged into one meaningless number. Anything wanting the both-ways total just sums the pair; the reverse isn't possible once they're merged.

Synthesis

The network is filtered twice before anything moves. Links whose road_type has no entry in road_capacities.csv are dropped outright -- with no capacity there's no meaningful v/c ratio. Then each vehicle keeps only the links whose road_type it has a velocity for in vehicle_velocities.csv, so every vehicle routes on its own subgraph and a vehicle simply never appears on a road it can't use.

Each resource's whole transacted flow is spread across intervals by departures.csv, renormalized over just the intervals time_intervals.csv actually defines (the two files are synthesized independently, so departures may name intervals that don't exist) and rounded to whole units, since a resource is counted in whole units.

Flow leaves as origin/resource shipments, then a weighted draw chooses an origin and a seed destination transaction. Vehicles with an ASC, capacity, and consolidation radius for that resource bid for the seed origin-to-destination route. Those that can reach the seed enter a multinomial logit on alternative_specific_constant + time_coefficient * route time + distance_coefficient * route distance, and one is drawn. Its consolidation radius includes the seed first and then outstanding destinations nearest to it from the same origin/resource. The interval's remaining resource budget limits the bundle; enough selected vehicles carry that amount to the seed, with only the final vehicle part-loaded. If no vehicle can reach the seed, only that transaction is discarded and the origin's other transactions remain eligible.

Grade is signed relative to a link's own start-to-end coordinate order, so traversing a two-way link backward flips it -- the climb becomes the descent. That's why a route carries a direction alongside each link id rather than a bare link id: the sign has to be recoverable when the trip is simulated, not just when the graph is built.

Velocities are frozen for the duration of an interval and recomputed at the end of it, from the loads that interval actually produced, via the BPR volume-delay function (1 + alpha * (v/c)^beta, with each vehicle's own bpr_alpha/bpr_beta). So every trip in an interval sees the same congestion, and routing decides on the state the network was in when the interval opened. Loads deliberately do not accumulate across intervals: a vehicle that has driven on is no longer on the link behind it, so v/c is recomputed from scratch each interval rather than summed forever.

Trips that don't finish within an interval carry their position into the next one and resume at the updated velocities. On arrival, dwell_times.csv decides how long the vehicle sits before returning; the return departs at the start of the first interval beginning at or after that, since every trip fires at an interval boundary, and is dropped if that falls past the end of the simulated period or if no return route exists.

Completed interval rows are written to network_loads.csv incrementally rather than retained until the entire synthesis finishes. Shortest-path results are cached only within an interval and with a fixed bound; identical in-flight vehicles share one counted trip state. These limits preserve routing and load results while preventing memory use from growing with completed interval output, distinct origins, or identical dispatched vehicles.

Like synth agents and synth desire-lines, this invents no shape, so --sigma throws unconditionally, and the command is a no-op once network_loads.csv already exists -- the draws here are genuinely random, so a re-run would silently produce a different assignment rather than recomputing the same one.

synth copert-v-coefficients

One row per (vehicle_type, pollutant, gradient_bin, payload_bin) key, written to copert_v_coefficients.csv: that key plus the COPERT V hot-emission-function coefficients alpha through eta and a reduction factor rf. Self-contained -- never reads any other file, including vehicles.csv; vehicle_type is matched up against it later, by whichever command combines them.

vehicle_type pollutant gradient_bin payload_bin alpha beta gamma delta epsilon zeta eta rf
vehicle_type_1 pollutant_1 -6 0 1.502 1.358 -0.753 -0.358 0.891 0.017 -1.565 0.157
vehicle_type_1 pollutant_1 -6 50 -0.450 -0.061 0.854 0.908 -0.698 0.550 -0.183 0.829
vehicle_type_1 pollutant_1 -4 0 0.643 -0.650 0.252 1.285 -0.188 -1.771 0.958 0.462

Synthesis

gradient_bin (-6, -4, -2, 0, 2, 4, 6) and payload_bin (0, 50, 100) are fixed COPERT V categories, not something --sigma or a caller invents -- the same status as vehicles.py's BPR defaults. random_count(sigma) is the only place --sigma acts, for n_vehicle_types and n_pollutants independently; on full synthesis, their full cross product against the fixed bins is what gets a row each.

alpha through eta are the fitted coefficients of COPERT's hot-emission speed function. Unlike vehicles.csv's bpr_alpha/bpr_beta, there's no single real-world default to anchor to: COPERT's published values are indexed by real vehicle/pollutant/Euro-class categories, and vehicle_type/pollutant here are synthetic labels, not entries in that taxonomy, so there's no principled real coefficient set to center on. They get the same sign-free Normal(0, 1) treatment as alternative_specific_constant. rf, the reduction factor, is conventionally bounded to [0, 1], so it's a plain uniform draw.

Alternatively, hand it a copert_v_coefficients.csv that already has all four key columns filled in (real categories are welcome) with the eight coefficient columns left entirely empty, and it fills them for exactly those rows, in that order -- checked jointly, so some coefficients filled while others aren't is rejected rather than partially accepted. Given rows don't need to cover the full bin grid, same reasoning as vehicle-velocities' pairs. A file with those eight columns already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has its key columns has nothing left for it to control.

synth emission-factors

One row per (vehicle_type, pollutant) pair, written to emission_factors.csv: vehicle_type, pollutant, and an emission_factor. Self-contained -- never reads any other file, same reasoning as copert-v-coefficients.

vehicle_type pollutant emission_factor
vehicle_type_1 pollutant_1 0.227
vehicle_type_1 pollutant_2 1.845
vehicle_type_2 pollutant_1 0.612

Synthesis

random_count(sigma) is the only place --sigma acts, for n_vehicle_types and n_pollutants independently -- their full cross product is what gets a row each. Deliberately not stratified by gradient_bin/payload_bin the way copert_v_coefficients is: this models COPERT V's non-exhaust emissions (brake wear, tire wear, road surface wear, resuspension), which per COPERT's own methodology aren't gradient- or payload-dependent -- they scale with speed and vehicle weight instead, which is a network-emissions concern (via network_loads' velocity), not something this file's shape needs to carry. emission_factor gets the same unassuming lognormvariate(0.0, 1.0) treatment as vehicle-velocities' velocity.

Alternatively, hand it an emission_factors.csv that already has vehicle_type/pollutant filled in (real categories are welcome) with emission_factor left entirely empty, and it fills factors for exactly those pairs, in that order -- the shape comes from what you gave it, not from --sigma. Given pairs don't need to be a full cross product, same reasoning as vehicle-velocities. A file with emission_factor already populated anywhere is left alone and the command is a no-op. Passing --sigma against a file that already exists throws either way (shape-only or complete): --sigma only ever controls count invention, and a file that already has vehicle_type/pollutant pairs has nothing left for it to control.

synth network-emissions

Combines five upstream files -- network_loads.csv, network.gpkg, vehicles.csv, copert_v_coefficients.csv, and emission_factors.csv -- into network_emissions.csv: one row per (link, time interval, resource, vehicle, direction, pollutant, source) that actually emitted anything.

link_id time_interval resource vehicle forward pollutant source grams
15 morning parcels van False nox exhaust 1.026
15 morning parcels van False pm10 non-exhaust 0.071
5 morning parcels van True nox exhaust 0.032
5 morning parcels van True pm10 non-exhaust 0.071

Exhaust and non-exhaust are reported as separate rows, never summed into one number, even for the same pollutant. They're different physical mechanisms that happen to produce the same substance -- tailpipe chemistry versus brake and tire wear -- and the source methodology plots them side by side for exactly that reason. Summing them here would throw away the ability to say how much of a PM figure is which; a consumer wanting the combined total just adds the two rows.

Synthesis

Exhaust follows COPERT V. Each load row's velocity, its link's grade, and its vehicles' mean load are snapped onto COPERT's published gradient/payload grid (nearest bin, clamped at the ends), the speed-dependent function gives a factor in g/km, and that scales by distance travelled -- link length times vehicle count, which is Eq. 8's N * M * e directly:

(alpha*V^2 + beta*V + gamma + delta/V) / (epsilon*V^2 + zeta*V + eta) * (1 - RF)

Grade is signed relative to the link's own start-to-end coordinate order, so a backward traversal negates it before snapping -- the climb becomes the descent, landing on a different coefficient row entirely. That's the whole reason network_loads.csv carries a direction per row. A factor that comes out negative is clamped to zero (no such thing as negative emissions), and a row is skipped rather than emitted when the function has nothing meaningful to say: at or below zero velocity (the delta/V term is undefined, and a stationary vehicle isn't accruing distance-based emissions) or where the denominator vanishes.

Non-exhaust (brake wear, tire wear, road surface wear, resuspension) uses emission_factors.csv's flat factor scaled by the same distance. It's deliberately not gradient/payload stratified, and deliberately does not vary with velocity: COPERT V doesn't publish non-exhaust factors against that grid, and while the source methodology notes non-exhaust emissions do vary with speed, it gives no functional form for it. Inventing one here would be fabricating a relationship the reference doesn't specify -- if a velocity dependence is wanted later, emission_factors.csv gaining velocity bins is the place for it.

A pollutant only gets an exhaust row where coefficients exist for that vehicle type at the snapped bin combination, and only gets a non-exhaust row where an emission factor exists for that vehicle type -- the two tables are synthesized independently, so a pollutant may have one, both, or neither. Links absent from network.gpkg and vehicles absent from vehicles.csv are skipped rather than guessed at.

Like synth network-loads, this invents no shape, so --sigma throws unconditionally, and the command is a no-op once network_emissions.csv already exists.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

urban_dollop-0.7.3.tar.gz (100.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

urban_dollop-0.7.3-py3-none-any.whl (102.0 kB view details)

Uploaded Python 3

File details

Details for the file urban_dollop-0.7.3.tar.gz.

File metadata

  • Download URL: urban_dollop-0.7.3.tar.gz
  • Upload date:
  • Size: 100.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for urban_dollop-0.7.3.tar.gz
Algorithm Hash digest
SHA256 2914b0f110f4bfd95c046f94dcc7907ea74e4a2f154f0ece169990f310c0ae28
MD5 21d02b99eaf106fe89c0ce0b390d275d
BLAKE2b-256 c1abab17aab2bd7b74b9bec5e96591297e4862392bdabf20cb61e4c5c242f76f

See more details on using hashes here.

Provenance

The following attestation bundles were made for urban_dollop-0.7.3.tar.gz:

Publisher: deployment.yaml on hcubasd/urban-dollop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file urban_dollop-0.7.3-py3-none-any.whl.

File metadata

  • Download URL: urban_dollop-0.7.3-py3-none-any.whl
  • Upload date:
  • Size: 102.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for urban_dollop-0.7.3-py3-none-any.whl
Algorithm Hash digest
SHA256 87ca4fe0e362b7f99538cc51cea985215f7fbe6031a81c7eac78bcf789cc8eda
MD5 4449e0aaf984757f3af45a1729f3e8e4
BLAKE2b-256 42819ede8a46d9b53fbcf60ced7b062d285025270d111e5923a4a470a5b666a8

See more details on using hashes here.

Provenance

The following attestation bundles were made for urban_dollop-0.7.3-py3-none-any.whl:

Publisher: deployment.yaml on hcubasd/urban-dollop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.7.3 This release

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page