Architecture overview
This page describes how UNA is organized internally — what lives where, how data flows through the classes, and the design principles that guide every extension. If you plan to modify UNA or add a new analysis, read this first.
The authoritative version of this architecture is DESIGN.md at the
repository root; this page mirrors it and adds cross-references into
the rest of the docs.
High-level layout
UNA ← public API, owns Settings and Topology;
│ also hosts batch orchestration + compositing
├── Settings ← single dataclass; every parameter lives here
├── Topology ← network + origin/destination/observer/obstacle
│ layers, spatial indices, snapping
└── Engines/
├── Base ← abstract base + all export methods
├── Accessibility ← Reach/Gravity/KNN (no elevation, no turns)
├── AccessibilityWElevation ← same + directional weights from z
├── AccessibilityWTurns ← turn-aware line graph
├── Flow ← K-alternatives betweenness engine
└── AggregateFlow ← scalable gradient-overlap flow engine
(standalone — builds its own graph,
so the two flow engines evolve
independently)
The physical layout of the repository mirrors this (src-layout — the
installable package lives under src/):
urban_network_analysis/ ← repository root
├── pyproject.toml ← package metadata + dynamic version
├── DESIGN.md
├── README.md
├── LICENSE
├── setup/una.yml ← conda environment for the dependencies
├── examples/ ← driver-script templates (UNA_Workspace.py,
│ UNA_Batch.py) — copy anywhere, edit, run
├── docs/ ← this documentation (docs/Boston = tutorial data)
└── src/
└── urban_network_analysis/
├── __init__.py ← __version__ + re-exports (UNA, Settings, Topology)
├── Logger.py
├── Settings.py
├── Topology.py
├── UNA.py
└── Engines/
├── __init__.py
├── Base.py
├── Accessibility.py
├── AccessibilityWElevation.py
├── AccessibilityWTurns.py
├── Flow.py
├── AggregateFlow.py
└── _betweenness_numba.py
Data flow through one run
For a single call to una.RunAccessibility() or una.RunFlow():
The user sets parameters on
una.settings(aSettingsinstance).The run method calls
settings.Validation()to catch invalid enum values, negative radii, or missing required inputs.The run method builds the topology via
topology.AddNetwork(settings)→AddOrigins(settings)→AddDestinations(settings)→ and (conditionally)AddObservers(settings)andAddObstacles(settings).Based on
settings.turnsandsettings.elevation, UNA instantiates the appropriate engine.The engine runs its analysis (Dijkstra sweeps, path enumeration, accumulation) and stores results as numpy arrays on itself (
engine.reach,engine.edge_flow, etc.).The engine calls one of
Base’s export methods to write results to disk in the formats enabled by/user_guide/settings_reference:output_geojson,/user_guide/settings_reference:output_feather, and/user_guide/settings_reference:output_csv.
Topology is rebuilt at every run. It is not cached between calls
— engines do not mutate topology, and every Run* call starts from
a fresh network/origin/destination load. This adds a few seconds of
overhead per run but eliminates a class of “stale cache” bugs.
The four core classes
Settings
A single @dataclass in urban_network_analysis/Settings.py. Every configurable
parameter lives here. Setting a value is:
una.settings.search_radius = 500
There are no hidden settings elsewhere in the codebase. See Settings reference for the full field list.
Notable methods:
Validation()— validates enums, ranges, required-when combinations.Reset()— restores all fields to defaults.ToDict(compact=True)— serialize only non-default fields; used for saving to JSON.ApplyRow(row: dict)— reset then apply one row of a CSV/JSON project pairing.
Topology
Holds the network graph and the point layers attached to it. Owns methods for loading and snapping each point-type layer:
AddNetwork(settings)— read the line layer, build the graph, create a spatial index.AddOrigins(settings)— read the point layer, snap to nearest edge, capture weight column.AddDestinations(settings)— same as origins.AddObservers(settings)— load observer points ifobserver_points_fileis set; snap; store snapping info.AddObstacles(settings)— load obstacle points; snap; compute per-arc and per-node penalty arrays.BuildClusters(...)— spatial clustering for parallel processing.BuildTurnPenalties(...)— precompute per-junction turn costs.
Also carries helper methods engines call directly, notably
get_obstacle_arc_penalties() (per-arc AB/BA penalty arrays that
stack additively with any elevation penalty).
Engines
Each engine inherits from Base and implements Centrality(settings).
It stores its results as instance arrays and calls one of Base’s
export methods to write to disk. No engine reads from disk directly
— all I/O passes through Base.
Accessibility — Dijkstra-based; produces per-origin Reach, Gravity, and KNN scores. Selected when
turns=Falseandelevation=False.AccessibilityWElevation — same math with directional arc weights from z-coordinates. Selected when
elevation=Trueandturns=False.AccessibilityWTurns — turn-aware routing via line-graph transformation. Selected when
turns=True(handles both turns and elevation).Flow — per-edge betweenness with K-alternatives and Huff destination choice. Selected when
flow_engine = "k_alternatives".AggregateFlow — scalable gradient-overlap flow engine, and the default (
flow_engine = "aggregate_flow"). Standalone: it builds its own DiGraph/CSR pipeline (including obstacle injection, elevation weights, and origin virtual nodes) rather than sharing Flow’s, so the two flow engines evolve independently.
See Engines and scope discipline for what belongs inside the urban_network_analysis package and
what should live outside the package.
Base
Provides all export logic. Engines do not implement their own file I/O — they store results on themselves and call inherited methods like:
ExportAccessibilityResults(settings, folder_prefix, file_name)ExportFlowResult(settings, folder_prefix, file_name)ExportODM(settings, folder_prefix, file_name, format, speed)
Plus private writers for the auxiliary output files:
_write_observer_points()_write_obstacle_points_usage()_write_network_nodes_flow()_write_destinations_used_origins()
Format is controlled by the output_* settings; the
timestamp-subfolder convention is enforced when
/user_guide/settings_reference:output_wStamp = True.
Engine selection logic
RunAccessibility() picks the engine on flags:
Settings |
Engine used |
|---|---|
|
|
|
|
RunFlow() dispatches on flow_engine: "k_alternatives"
(default) → Flow, "aggregate_flow" → AggregateFlow. Within
Flow, turn-aware routing switches on internally when
turns = True; there is no separate class name to worry about. Both
engines populate the same result attributes, so export and batch
compositing work identically downstream.
RunODM() uses the same dispatch as RunAccessibility() but
calls the engine’s OD_Matrix() method instead of Centrality().
The Project workflow
A project is a list of Settings snapshots. Each snapshot is a
deepcopy — mutating una.settings after saving to project does not
affect the saved entries. Full details on
Project workflow — RunBatch().
Key methods:
una.SaveSettingsToProject()— snapshot current settings.una.ExportProjectAsJSON(folder, file_name, compact=True)— write the whole project to disk.una.ExportProjectAsCSV(file_name)— same as CSV.una.RunBatch(analysis, pairing_file=None)— execute every snapshot with the matching Run* method (analysisis"accessibility"or"flow"). When any row setsbatch_composite_output, UNA’s built-in batch compositor collects each row’s engine result and writes a joint composite file at the end.una.ConvertProject_csv_to_json(csv_file, json_file, compact=True)— convert a project pairing table between formats.
Design principles
The architecture is guided by a few consistent principles. Follow them when extending UNA.
Single source of truth for parameters. Every configurable value
lives on Settings. If a new feature needs a parameter, add it to
Settings — not to a constructor argument on some engine.
Engines don’t mutate topology. An engine reads the topology and writes results to itself. It never modifies the shared topology state, so re-running the same topology through a different engine is always safe.
Rebuild topology per run. Loses a bit of runtime but eliminates cache-invalidation bugs. Deemed a worthwhile trade-off for maintenance.
Explicit dispatch, not implicit magic. The Run* methods dispatch
to engines with visible if statements in UNA.py, not through
a registry or a decorator. New contributors can find the dispatch
logic by reading one file.
Every output goes through Base. Engines expose numeric arrays; Base handles all serialization. Adding a new output format (say, Parquet) means editing Base, not every engine.
One prefix per subsystem. Flow-specific settings all start with
flow_; obstacle-specific settings with obstacle_points_. No
setting spans multiple subsystems.
Scope discipline — a tight, contained core. The the urban_network_analysis package
package deliberately owns only: topology loading and CSR construction,
impedance modeling (elevation, turns, obstacles), the Dijkstra-based
accessibility and flow engines, result export, and batch execution.
Anything else — visualization, notebooks, tutorials, custom analysis
scripts, downstream pipelines — lives outside the package as user
scripts (UNA_Workspace.py, UNA_Batch.py) or separate
repositories. Rule of thumb: if a new capability doesn’t strictly need
to touch the CSR or the Settings dataclass, it belongs outside
the urban_network_analysis package.