Unitary Core — Unit-Agnostic Modeling Standard¶
Phase 4, modeling-technique addendum (rev 2, draft for review). The BU logic is unitary: currency and market specifics never appear inside computational logic. Rev 2 sharpens two boundaries:
- Currency: USD is the single basis. All logic computes in USD. Published FX rates generate currency-specific outputs at the presentation layer — FX conversion is a boundary operation, not logic.
- Physical units: ISO standards, actual numbers. kW, kWh, Ah, km — and we use real specific values. No abstract "1 kWh battery": we build models for concrete configurations, e.g. an SNS operating 100 Ah batteries on 3-phase 6 kW chargers.
The three-layer split¶
| Layer | Owns | Examples |
|---|---|---|
| Logic (unitary core) | equations, identities, behavioral structure | price wedge, utilization ratio, degradation-per-EFC shape, IRR, integer capacity |
| Profile (coloring) | coefficients, concrete values, FX | USD cost anchors, KES FX rate, tariff $/kWh, duty %, battery spec values |
| Presentation | display formatting | local-currency conversion via published FX, unit suffixes, rounding — rendered as the uniform BU Dashboard |
The rule: logic owns equations; the profile owns coefficients. If an equation's shape changes per market, it is not one equation — it is two, and both belong in logic with a profile switch selecting between them.
Canonical internal representation¶
| Dimension | Canonical form | Note |
|---|---|---|
| Time | period index (months) | calendar labels are presentation |
| Energy / power | kWh / kW (ISO) | values are specific per configuration |
| Charge / capacity | Ah (ISO) | battery spec, actual numbers |
| Count | units (batteries, vehicles, swaps, stations) | integers stay integers |
| Money | USD | one basis everywhere; FX only at presentation |
Worked example — LCOB, unitary¶
LCOB_usd_per_kwh = (CAPEX_usd − residual_usd) / usable_energy_kwh ← logic (USD in, USD out)
LCOB_local = LCOB_usd_per_kwh × FX_rate ← presentation
Kenya and Tanzania profiles differ only in the USD inputs; the logic line is identical.
Granularity: unitary in representation, specific in structure¶
Unitariness governs representation (USD, ISO units) and shared equation structure. It does not mean one fully-generic model parameterized over all product variants. The BU boundary follows the operational unit, not type-system elegance.
Decision (Sep 2026): mixed-fleet BUs. A swap network operating both 100 Ah and 45 Ah batteries is modeled as one mixed SNS BU — not as two pure SNS models combined.
Why:
- Real stations share infrastructure, demand pools, overheads, and slot-allocation decisions across battery types. Splitting into pure models fabricates P&L boundaries that do not exist; recombining loses the interactions that determine profitability (slot allocation, demand cannibalization between segments, mixed-fleet rotation scheduling).
- Forcing all variants into one generic parameterized engine instead creates combinatory complexity: every equation grows a product-variant index, most combinations are unphysical, and the business logic becomes unreadable. Formally unitary, semantically opaque.
- Business common sense wins: the model must read like the business. A mixed SNS reads like a mixed station.
Battery types inside a mixed BU are structured data — a fleet composition (list of battery classes with Ah rating, kWh, charger requirement, share) driving the mixed rotation and allocation logic — not separate objects and not flattened generic parameters.
Decision rule going forward:
| Situation | Modeling choice |
|---|---|
| Variants share operational behavior (same cabinet family, minor capacity difference) | one BU class, parameters differ |
| Variants differ operationally (different cabinets, charger phases, customer segments) but coexist at one site | one mixed BU, structured fleet composition |
| Genuinely separate operations (different sites, different P&L) | separate BU objects |
The Profile-Swap Test (acceptance gate)¶
A BU model is unitary iff: swapping the profile (Kenya → Tanzania: FX, tariffs, cost anchors) runs the same code unchanged with correct results. Product-configuration differences are handled by the granularity rule above — new structured composition, not a code fork.
Standing pytest fixture: same BU object built with two profiles; assert structural outputs (ratio relationships, breakeven period index) match and no hardcoded unit leaks.
Actionable implementation steps¶
- Dimension tags in the parameter schema (
schema.py):dimensionfield perParameter(usd | usd_per_kwh | energy | power | count | time | dimensionless). - Canonicalization at the boundary:
load_profile()validates and delivers USD + ISO values; local-currency inputs must declare their FX source row. - BU objects accept canonical input only — setters reject non-canonical quantities (assert, fail fast).
- Presentation layer converts USD → local currency via published FX at report/bundle time only.
- Profile-Swap Test fixture in
models/tests/— the unitary gate, run for every BU class. - Fleet-composition structure in the SNS schema: ordered battery-class list driving mixed allocation logic.
What this buys¶
- One logic codebase, N markets — bundles differ by profile files, not code forks.
- USD basis keeps model outputs internationally comparable and investor-ready; FX is a mechanical step, never a modeling question.
- Specific configurations (100 Ah + 6 kW 3-phase) keep the model honest to the business; mixed BUs keep the P&L honest to the station.
- Dimensionally homogeneous equations are self-checking: a unit bug cannot hide.
Next step (4.4): map Maxwell v12 onto the BU catalog + transaction map — line-by-line provenance, adopt/contrast/reject table.