Skip to content

Commit e929d43

Browse files
committed
GEP 10: max/min preserve the operand unit; a schedule param function may declare its output unit
1 parent 701cd12 commit e929d43

1 file changed

Lines changed: 40 additions & 5 deletions

File tree

docs/geps/gep-10.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -476,9 +476,11 @@ What the converter *returns* carries no unit declaration of its own — see
476476

477477
A `@param_function` that *builds* a structured object — a dataclass of related
478478
parameters, 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
482484
check 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

534536
A converter-built **schedule** takes neither annotations nor casts when its blob
535537
declares 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

539558
The annotations and casts do not need to travel for the *numbers* to be right:
540559
parameters 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

Comments
 (0)