Coding conventions
Conventions the UNA codebase follows and expects contributors to respect. These aren’t stylistic preferences but consistency rules that keep the codebase legible as it grows.
Python style
PEP 8, with some slack. Line length up to ~100 characters is
acceptable; strict 79 is not enforced. Use black-style formatting
if you have a preference, but existing code isn’t formatted with it —
don’t reflow entire files gratuitously.
Type hints on public API. Every method on UNA, Settings,
and Topology that’s part of the public surface should have type
hints on parameters and return values. Internal helper methods can
skip them but shouldn’t lie about them.
Docstrings on public API. Every public method needs a docstring
that explains what it does, what its parameters mean, and what it
returns (if anything). Use Google-style or NumPy-style; both are
parsed by sphinx-napoleon in the docs build.
def RunAccessibility(self) -> None:
"""
Compute per-origin accessibility scores.
Reads settings, builds topology, dispatches to the appropriate
engine, and exports results to disk.
Raises:
ValueError: If required settings are missing or invalid.
"""
Logging
Every log message goes through the topology logger. No
print statements in the urban_network_analysis package. The logger is available on
self.topology.logger for engines and on self.logger for tools
that stash it in __init__:
self.logger.log("MyTool", "Loaded 12,345 zones from disk", v=1)
Verbosity levels:
v=0— errors only.v=1— summary messages the user should always see.v=2— detailed diagnostic messages, per-cluster or per-batch progress.v=3— very verbose; only useful for debugging.
Follow existing tag naming: the first argument to .log() is the
subsystem name ("AccessibilityWElevation", "BuildAccessPoints")
so users can grep the log for a specific step.
Errors and validation
Fail loudly, early, and with useful messages. Settings.Validation()
runs first and raises ValueError with a clear message describing
what’s wrong and how to fix it:
errors.append(
f"flow_gravity_cap must be > 0 when "
f"flow_decay_method='gravity_cap'; got {cap}. Set it to the "
f"gravity value (sum of destination_weight × distance_decay "
f"within search_radius) at which an origin's trip generation "
f"should saturate at 100%."
)
Prefer messages that:
State the observed problem.
Say what’s expected instead.
Point at the setting name to change.
Raise, don’t return None. Errors surface as exceptions, not as sentinel return values. This lets Python’s normal error-handling machinery work.
CRS handling
Every loaded layer must match the network CRS. Topology.crs
is set when AddNetwork runs, and every subsequent Add* call
validates the loaded layer against it. Copy the pattern from
BuildAccessPoints:
if self.crs is not None:
if gdf.crs is None:
raise ValueError(
f"{label} file has no CRS defined, but network CRS is {self.crs}. "
f"Please assign CRS {self.crs} to the file before loading."
)
elif not gdf.crs.equals(self.crs):
raise ValueError(
f"{label} file CRS ({gdf.crs}) does not match network CRS ({self.crs}). "
f"Please reproject to {self.crs} before loading."
)
Output handling
Every output goes through Base. Engines don’t call
gpd.to_file or pd.to_csv directly. If you need a new output
type, add a private writer to urban_network_analysis/Engines/Base.py and call it
from the appropriate Export* method.
Respect the output flags. Every output writer checks
settings.output_geojson, settings.output_feather,
settings.output_csv and only writes the formats the user asked
for.
Timestamp subfolder. If settings.output_wStamp is True (the
default), every run creates a subfolder named
<folder_prefix><timestamp> where <timestamp> is
YYYY-MM-DD_HHMM. This is standard across every Base export
method — copy the pattern, don’t reinvent it.
State management
Engines are stateless consumers of Topology. No engine
mutates the shared topology state. If you need to modify edge weights
(e.g. for obstacle handling), do it via Topology helpers like
get_obstacle_arc_penalties() — never by writing back to
topology.network.weights.
Results live on the instance. Every engine or tool stores its
outputs as attributes on self. Downstream code composes at the
UNA level by reading una.accessibility.reach,
una.flow.edge_flow, etc.
No hidden global state. No module-level mutable dictionaries, no
class-level list defaults, no thread-local shortcuts. Everything a
run needs comes from the Settings and Topology passed in.
Settings extensions
One prefix per subsystem. Flow-specific settings all start with
flow_. Obstacle-specific settings all start with
obstacle_points_. Observer settings all start with
observer_points_. New tools add a new prefix. No setting spans
multiple subsystems.
Validate at Settings-load time, not at engine runtime. Every
enum and range check goes in Settings.Validation() so errors
surface before any heavy work starts. The Literal-enforcement loop
handles enum values automatically; you only need to add
numeric-range and required-when-X-is-True conditionals.
Compact serialization for JSON. Settings.ToDict(compact=True)
should return only fields that differ from their defaults. This keeps
saved projects small and diff-friendly. If you add a field, make sure
its default is set on the dataclass — never compute the default
inside __post_init__ if you can avoid it.
Testing
There’s no formal test suite in the repo today. If you’re adding a new engine or tool, a minimal smoke test in a Jupyter notebook against the Boston tutorial data is expected — one run that exercises every enabled setting, with the output inspected visually in QGIS.
For features that touch the math directly (new decay curve, new gravity-cap variant), a numerical test against hand-computed expected values on a tiny synthetic network is strongly preferred.
Documentation
New features come with docs. Adding a new engine, tool, or point type without documenting it is not complete. At minimum:
A user_guide page describing what it does and when to use it.
Cross-references from
settings_reference.rstif new Settings fields are involved.An update to
index.rstif a top-level page is added.Docstrings on new public API surfaces (autodoc picks these up).
Code examples in docs must run. Every .. code-block:: python
in the docs should be runnable as-is against the current API.
Broken examples are a bug, not an oversight.
Versioning and changelog
UNA follows loose semantic versioning: MAJOR.MINOR.PATCH.
Patch releases fix bugs. Minor releases add features. Major
releases break the API.
When you land a feature or bug fix, add a bullet to
docs/changelog.rst under the “Unreleased” section. When a
release cuts, rename that section to the released version number
with the date.
Deprecation
Deprecate before removing. When renaming a Settings field or a
method, keep the old name as a deprecated alias for one release. The
old name should log a warning when used, pointing at the new name.
Reference example: flow_max_destinations_per_origin is a
deprecated alias for flow_k_nearest_destinations and will be
removed in a future release.