Python API¶
bikescore-bna scores a single city in-process, with no database and no server. The
deliverable surface is small: build a config, hand in the input files, get scores back.
from bikescore_bna import build_config, score_city
config = build_config("default")
result = score_city(inputs, config) # inputs: {"osm": ..., "boundary": ..., ...}
scores = result.output("scores", "scores.parquet")
Config¶
BNAConfig.with_defaults() -> BNAConfig
build_config(scenario: str | dict | Path | None = None,
overrides: dict | None = None) -> BNAConfig
# None → with_defaults()
# "default"/name → bundled scenario
# dict / Path → caller-supplied scenario doc
list_bundled_scenarios() -> list[str]
Scoring¶
score_city(inputs: dict[str, Path], config: BNAConfig,
*, workdir: Path | None = None,
pinned: dict[str, Path] | None = None,
to_stage: str | None = None) -> ScoreResult
Runs every stage in PIPELINE order into workdir — no SQLite, no content-addressed
hashing, no run store, no graphlib. It is the minimal driver: the stages, in order, and
nothing else.
inputs— dataset-input name → file path. Must cover everydataset_inputsname the stages declare, e.g.{"osm": ..., "boundary": ..., "census": ..., "lodes_main": ...}(seeacquire_city, 38g).workdir— directory to write stage outputs into (created if missing). Outputs persist here for reuse; there is no temp dir that gets silently discarded. Defaults to a fresh timestamped folder under./bikescore-bna-runs/.pinned—{stage_name: output_dir}of prebuilt stage outputs. A pinned stage is not recomputed; its directory is used verbatim as the upstream for later stages (e.g. supply a custom network forparse).to_stage— stop after this stage (inclusive) for a partial run.
ScoreResult carries stage_dirs: dict[str, Path] (every stage that ran or was pinned →
its output directory), workdir: Path (the run root), and a convenience
result.output(stage, filename) -> Path. ScoreResult.from_dir(workdir) rebuilds a
result from a folder a prior run wrote to, so export/validate can reuse it without
recomputing.
The stage contract — StageSpec, PIPELINE, run_stage¶
This is the public plugin contract: a larger orchestration layer can consume these to
build a content-addressed run store, a dynamic DAG, and a UI on top of bikescore-bna,
without bikescore-bna ever depending on it — the dependency direction is one way, into the
library. score_city itself uses only this contract.
@dataclass(frozen=True)
class StageSpec:
name: str
depends_on: tuple[str, ...] # prior stage names; static, resolved before exec
dataset_inputs: tuple[str, ...] # named external inputs (opaque to the orchestrator)
version: str # author-declared cache-buster (opaque)
run: Callable[[dict[str, Path], Path, BNAConfig], None] # (input_paths, out_dir, config)
cacheable: bool = True # False → always recompute (foreign/nondeterministic)
PIPELINE: list[StageSpec] # the fixed, ordered 11-stage sequence
def run_stage(stage, upstream_dirs, dataset_paths, output_dir, config) -> None: ...
PIPELINEis an explicit ordered list, not a runtime graph. The full pipeline is a fixed sequence (no config-driven stage inclusion), so core needs only ordering. A dynamic DAG system (topological sort, ancestors/descendants, reuse planner,--from/--towindows) is something a larger tool can layer on, re-deriving its graph from the samedepends_onmetadata. One source of truth for deps; a stdlib drift-guard test assertsPIPELINEis a valid topological order.run_stageis the shared primitive: it assemblesinput_paths(eachdepends_onname → its upstream output dir; eachdataset_inputsname → its path under a"dataset:<name>"key), then callsstage.run.score_cityloops it; an orchestration engine can wrap the same primitive with hash/reuse/persist, so the two execution paths cannot drift. Dataset-path resolution stays out of the primitive (score_citypasses caller paths; an orchestrator resolves its ownfile_id → path).- The metadata is the orchestration contract.
versionand the co-locatedconfig_slice_for_stage/_STAGE_CONFIG_FIELDS(inbikescore_bna.config) look "dead" in the library —score_cityreads onlydepends_on/dataset_inputs— but they are the cache-buster and the fine-grained-invalidation key any caching layer relies on. They stay co-located with the stage that owns them (the author is the only party who knows the right value); moving them out of the library would force external edits on every algorithm change.
Determinism invariant (the reuse contract)¶
Content-addressed reuse in any caching layer silently assumes each stage's output is a
deterministic function of (config-slice, upstream output hashes, dataset hashes,
version) and nothing else — no unseeded RNG, wall-clock timestamps, environment reads,
or unordered output. A violation is a silent parity bug, not a crash. A stage that cannot
honour it sets cacheable=False (always recompute); downstream still stays
content-addressed on that stage's actual output hash.
Execution-model assumptions (A–F)¶
The stage contract is designed so a generic "cached DAG-of-file-stages runner" can
drive bikescore-bna as plugin #1. Such a runner makes zero domain assumptions (nothing
about bikes / OSM / scoring) but a small fixed set of execution-model assumptions. Any
compute library that ships list[StageSpec] + a config factory + (optionally) an
InputProvider is orchestratable the same way, whatever its domain.
| Assumption | What it means | |
|---|---|---|
| A | File I/O through directories | Stages exchange state only as files: read upstream from input_paths dirs, write all outputs into output_dir. No in-memory handoff, shared DB, or hidden globals — content-addressed reuse hashes files. |
| B | Determinism | Output is a deterministic function of inputs + config-slice + version (the invariant above). |
| C | Static, declared DAG | depends_on is fixed metadata resolved before execution — no data-dependent graph shape. |
| D | Named external inputs | dataset_inputs names files the orchestrator supplies; names are opaque to it, but something (an InputProvider) must produce them. |
| E | Hashable, opaque config | A caching layer hashes config per stage and passes it to run; a slice_config(cfg, stage) hook is an optimization for fine-grained invalidation (whole-config hashing is coarser but still correct). |
| F | Author-declared version | version is an opaque cache-buster the library author bumps on behavioural change. |
Foreign libraries (the adapter pattern). A–F cannot be imposed on a third-party
library; it is integrated by writing an adapter that implements StageSpec over its
native API. An adapter can always provide the interface assumptions (A, C, D, F) but can
only partially guarantee the semantic ones (B determinism, complete E). Hence
cacheable: execution needs only A + C + D + a run callable (always suppliable); reuse
additionally needs B + complete-E + F. Safe default for anything uncertain:
cacheable=False — correctness over reuse. You lose reuse at that node, never the
ability to orchestrate.
Rule & config analysis — bikescore_bna.decision.analysis¶
Static analysis over the decision DSL (Decision objects + field catalogs) — no
orchestration, database, or run store. It powers BNAConfig.validate() and the
orchestration app's bikescore-bna rules commands and rule-builder endpoints.
from bikescore_bna.decision.analysis import (
validate_decision, # static checks: unknown-field, op-type, enum-domain, duplicate-id
find_unreachable, # rules shadowed by an earlier, broader rule
check_exhaustive, # passes that can fall through with no default
coverage, # winning-rule tallies + never-fired detection over a frame
trace, # per-row clause/rule/pass explanation
unique_contexts, # exact, finite threshold-partitioned decision contexts
simulate, # before/after diff of two rule versions over those contexts
cross_engine_diff, # equivalence harness vs a reference decider
)
Resolve-time producer/consumer and $var: validation — what BNAConfig.validate()
calls to reject a config at save/resolve:
from bikescore_bna.decision.analysis.producer_consumer import unproduced_references
from bikescore_bna.decision.analysis.variables import (
undeclared_variables, null_variables, orphan_overrides,
)
unproduced_references(config)— columns an active rule/stage references that no active attribute, ruleset output, base OSM tag, or stage produces.undeclared_variables(config)— referenced$var:names neither declared in the scenario'svariablesnor supplied by a stage's fixed_RULE_VARIABLESvocabulary (discovered by walkingPIPELINE).null_variables/orphan_overrides— advisory: declared-but-null variables, and city overrides no active rule references.