@@ -476,9 +476,11 @@ What the converter *returns* carries no unit declaration of its own — see
476476
477477A `@param_function` that *builds* a structured object — a dataclass of related
478478parameters, a schedule assembled from a `require_converter` blob — returns something
479- that is not a quantity : there is no unit to declare and no scalar body to dry-run. It
480- states exactly that with `unit=UNSET_UNIT`. The `unit=` argument remains required, so
481- the sentinel is always an explicit statement, never an omission, and the mandatory-units
479+ that is not a quantity, so it has no scalar body to dry-run. A dataclass has no single
480+ unit to declare and states exactly that with `unit=UNSET_UNIT`; a schedule has one — its
481+ output axis — which it may name (see the schedule paragraph below), or leave to the
482+ blob's axes with `unit=UNSET_UNIT`. The `unit=` argument remains required, so the
483+ sentinel is always an explicit statement, never an omission, and the mandatory-units
482484check accepts it.
483485
484486**Units live on the dataclass fields.** A parameter dataclass states each scalar field's
@@ -533,8 +535,25 @@ age as money.
533535
534536A converter-built **schedule** takes neither annotations nor casts when its blob
535537declares axes : it screens at the call site like a parameter-declared schedule (the axes
536- form above). Only an axis-less converter-built schedule stays opaque, and the author
537- casts the call's result.
538+ form above). When the blob cannot collapse to a single axis — its entries carry
539+ genuinely different units (an area table that also holds a `PERSON_COUNT` cutoff), so
540+ the honest declaration is per-key units, not one `output_unit:` — the building
541+ ` @param_function` states the **schedule's output unit as its own `unit=`** instead. That
542+ unit is read as the schedule's output axis (concrete currency allowed, no name-suffix
543+ rule — it is not a column), so `look_up`/`piecewise_polynomial` on it screens the
544+ consumer just like the axes form, with no cast :
545+
546+ ` ` ` python
547+ @param_function(unit=Unit.SQUARE_METER.PER_HH)
548+ def berechtigte_wohnfläche_eigentum(
549+ parameter_berechtigte_wohnfläche_eigentum: RawParamValue, # honest per-key units
550+ max_anzahl_personen: dict[str, int],
551+ xnp: ModuleType,
552+ ) -> ConsecutiveIntLookupTableParamValue: ...
553+ ` ` `
554+
555+ Only a schedule that declares neither — no axes, no `@param_function` output unit —
556+ stays opaque, and the author casts the call's result.
538557
539558The annotations and casts do not need to travel for the *numbers* to be right :
540559parameters are never converted ({ref}`Currency <gep-10-currency>`), so every number
@@ -641,6 +660,22 @@ def betrag_m(einkommen_m: float, befreit: bool) -> float:
641660 return einkommen_m
642661` ` `
643662
663+ A `0.0` floor in `max`/`min` (and `xnp.maximum`/`xnp.minimum`/`xnp.clip`) is the same
664+ allowed sign test, and the clamp carries the quantity's unit on *every* branch — the one
665+ where the `0.0` wins included — so a clamped value stays usable downstream without a
666+ `cast_unit` on the floor :
667+
668+ ` ` ` python
669+ @policy_function(unit=Unit.DIMENSIONLESS)
670+ def nettoquote(bruttolohn_m: float, abzüge_m: float) -> float:
671+ bereinigt_m = max(bruttolohn_m - abzüge_m, 0.0) # CURRENCY/month on both branches
672+ return bereinigt_m / bruttolohn_m # a clean dimensionless ratio
673+ ` ` `
674+
675+ A non-zero literal bound is still rejected (`max(einkommen_m, 1000.0)` hides a monthly
676+ amount), exactly as `einkommen_m + 1000.0` is — promote it to a parameter or tag it in
677+ place.
678+
644679(gep-10-currency)=
645680
646681# # Currency
0 commit comments