Skip to content

pseudo-dimension tags are lost in cancelling products — kWh × USD/MWh is not USD #321

Description

@withtwoemms

Found while building a sector input-cost model on top of #292's currency design. Distinct from #318 (which is about Dimension(none) factors being dropped) — here the factor survives and the dimension tag is lost.

The defect

A pseudo-dimension tag does not survive multiplication by a dimensionally-cancelling ratio. The product's dimension silently becomes Dimension(none):

>>> usd = Unit(name="USD", dimension=COUNT)
>>> UnitProduct({UnitFactor(usd, Scale.one): 1.0}).dimension
Dimension(count)                                      # correct

>>> UnitProduct({UnitFactor(usd, Scale.one): 1.0,
...              UnitFactor(watt, Scale.kilo): 1.0,
...              UnitFactor(watt, Scale.mega): -1.0}).dimension
Dimension(none)                                       # count lost

kW/MW is dimensionless, so count × dimensionless must be count. The tag survives only while the product reduces to a single tagged factor; add a cancelling pair and it demotes. USD·km/m behaves identically, so it is not specific to power.

Why it breaks a real use case

Prices are quoted per one scale and quantities metered in another — universally, in energy. So the product of a quantity and its own rate does not reduce, and then will not aggregate:

>>> Number(0.5, MWh) * Number(48.0, usd_per_MWh)
<24.0 USD>              dim = count          # matched scales: fine
>>> Number(0.5, kWh) * Number(48.0, usd_per_MWh)
<24.0 kW·USD/MW>        dim = none           # mismatched: not USD

Both are arithmetically right — canonical_magnitude is 24.0 and 0.024 respectively. But the second is not addable to another USD amount:

>>> steel_cost + power_cost
TypeError: Cannot add Numbers with different dimensions: Dimension(count) vs Dimension(none)

So any cost model that mixes rate denominations fails at aggregation — which is most of them. A sector analysis pulling steel at USD/tonne and power at USD/MWh, against quantities in tonnes and kWh, cannot sum its own line items.

Cause

A UnitProduct's dimension is resolved from the summed dimension vector. A pseudo-dimension contributes the zero vector (ADR 005 — deliberately, so it is multiplicatively transparent), and the tag is not carried through the composition. Zero-vector then resolves to none unless the product happens to reduce to one tagged factor.

Related but separate: the same-dimension ratio is not folded to a scalar at all. kW/MW stays two factors rather than becoming a factor of 1e-3. Magnitude is preserved in canonical_magnitude, so that half is closer to cosmetic — but a fix likely touches the same reduction path, which is why both are noted here.

Scope

Affects every pseudo-dimension, not just count:

product dimension expected
USD count count
USD·kW/MW none count
USD·km/m none count

The same shape applies to radian·m/m, percent·kg/g, and so on.

Relationship to ADR 012

012 asks whether a named dimensionless unit is a distinguishable symbol or unity. This is evidence that the current answer is inconsistent: a tagged unit is distinguishable alone and not distinguishable inside a cancelling product.

Under Option A (dimensionless with kinds) the tag would not exist to lose, and money-ness would ride on the kind — but then the kind's dimension must match the unit's, and a kind cannot refine none while the unit reads count, so the mismatch surfaces there instead.

Under Option B or the count-stays-tagged branch of Option C, this is a straightforward bug: the tag must compose.

Either way it should be fixed or explicitly declared intended, because currency ships on Dimension(count) per 012's recorded decision, and this is the first thing a price calculation hits.

Suggested direction

When a product's summed vector is zero, resolve the dimension from the surviving tagged factors rather than defaulting to none — with a rule for the case where two different tags are present (USD·rad/rad has one tag; USD·rad has two and is arguably ill-formed). Folding same-dimension ratios to a scalar first would make the common case moot and is probably the cheaper half.

Verified against main at d7922fd.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions