Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/scale-threshold-uprating.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support scale threshold uprating via bracket-level `metadata.uprating` (value side), bracket per-component `metadata.threshold.uprating`/`metadata.amount.uprating`, bracket `metadata.uprate_thresholds`, and scale-level `metadata.threshold_uprating`, without changing the working per-leaf `threshold.metadata.uprating` form.
1 change: 1 addition & 0 deletions policyengine_core/parameters/parameter_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def propagate_uprating(self) -> None:
bracket.propagate_uprating(
self.metadata.get("uprating"),
threshold=self.metadata.get("uprate_thresholds", False),
threshold_uprating=self.metadata.get("threshold_uprating"),
)

def get_descendants(self) -> Iterable:
Expand Down
86 changes: 78 additions & 8 deletions policyengine_core/parameters/parameter_scale_bracket.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable
from typing import Iterable, Optional
from policyengine_core.parameters import Parameter, ParameterNode
from policyengine_core.parameters.config import COMMON_KEYS

Expand All @@ -17,6 +17,12 @@ class ParameterScaleBracket(ParameterNode):
# flagged by the loader (issue #505).
_allowed_keys = frozenset(_component_keys).union(COMMON_KEYS)

# The component that carries the bracket's threshold, versus the components
# that carry its "value" (amount/rate/base). A bare ``uprating`` applies to
# the value side; the threshold is opted in separately (issue #390).
_threshold_key = "threshold"
_value_keys = ("amount", "rate", "average_rate", "base")

@staticmethod
def allowed_unit_keys():
return [key + "_unit" for key in ParameterScaleBracket._component_keys]
Expand All @@ -26,11 +32,75 @@ def get_descendants(self) -> Iterable[Parameter]:
if key in self.children:
yield self.children[key]

def propagate_uprating(self, uprating: str, threshold: bool = False) -> None:
def propagate_uprating(
self,
uprating: str,
threshold: bool = False,
threshold_uprating: Optional[str] = None,
) -> None:
"""Route uprating metadata down onto the bracket's component leaves.

Uprating is applied by reading each leaf ``Parameter``'s
``metadata["uprating"]``. Scale parameters carry their uprating intent on
the scale, the bracket, or the individual component, so this method
resolves all of those into per-leaf ``uprating`` entries. See issue #390.

Resolution order for a component (highest priority first):

1. the component leaf's own ``metadata.uprating`` (per-leaf form, e.g.
``brackets[i].threshold.metadata.uprating``);
2. a bracket-level per-component override
(``brackets[i].metadata.threshold.uprating``);
3. a bracket-level default: ``brackets[i].metadata.uprating`` for the
value side, and for the threshold either
``brackets[i].metadata.threshold_uprating`` or the bare bracket
``uprating`` when ``brackets[i].metadata.uprate_thresholds`` is set;
4. the scale-level default passed in by :class:`ParameterScale`
(``uprating`` for the value side, gated by ``uprate_thresholds`` for
the threshold; ``threshold_uprating`` for the threshold).

Args:
uprating: Scale-level default uprating for the value side (may be
``None``).
threshold: Whether the scale-level ``uprating`` should also apply to
thresholds (the scale's ``uprate_thresholds`` flag).
threshold_uprating: Scale-level uprating dedicated to thresholds (may
be ``None``).
"""
bracket_meta = self.metadata

# Bracket-level defaults, falling back to the scale-level ones.
bracket_value_uprating = bracket_meta.get("uprating", uprating)
bracket_threshold_uprating = bracket_meta.get(
"threshold_uprating", threshold_uprating
)
bracket_uprate_thresholds = bracket_meta.get("uprate_thresholds", threshold)

for key in self._component_keys:
if key in self.children:
if key == "threshold" and not threshold:
continue
self.children[key].metadata["uprating"] = uprating or self.children[
key
].metadata.get("uprating")
if key not in self.children:
continue
child = self.children[key]

# (2) bracket-level per-component override, e.g. metadata.threshold.
component_override = None
component_meta = bracket_meta.get(key)
if isinstance(component_meta, dict):
component_override = component_meta.get("uprating")

if key == self._threshold_key:
# Threshold: dedicated index, else bare bracket uprating when the
# bracket/scale opts thresholds in.
default = bracket_threshold_uprating
if default is None and bracket_uprate_thresholds:
default = bracket_value_uprating
else:
# Value side (amount/rate/base/average_rate).
default = bracket_value_uprating

resolved = (
child.metadata.get("uprating") # (1) per-leaf wins
or component_override # (2)
or default # (3)/(4)
)
if resolved is not None:
child.metadata["uprating"] = resolved
Loading
Loading