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
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ class TakeupGapKernel(KernelBase):
Determinism.DETERMINISTIC,
numeric=Numeric.BITWISE,
seed_source=SeedSource.NONE,
dependencies=("numpy", "pandas", "scipy"),
)
_required = frozenset(
{
Expand Down Expand Up @@ -376,6 +377,7 @@ class TakeupBandsKernel(KernelBase):
"""``takeup.bands@1``: labelled envelopes of matching scenario tables."""

ref = "takeup.bands@1"
# Bounds select decoded Python numbers; no numerical library computes them.
capabilities = Capabilities(
Determinism.DETERMINISTIC,
numeric=Numeric.BITWISE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
NONNEGATIVE = st.floats(
min_value=0, max_value=1500, allow_nan=False, allow_infinity=False
)
# A unit increase is representable throughout NONNEGATIVE's float64 range.
POSITIVE_INCREMENT = st.floats(
min_value=1, max_value=1500, allow_nan=False, allow_infinity=False
)


def _case_root(tmp_path):
Expand Down Expand Up @@ -240,7 +244,8 @@ def test_as_is_zero_for_excess_assets_and_each_statutory_exclusion(tmp_path, exc


@EXECUTED
@given(income=NONNEGATIVE, increment=NONNEGATIVE)
@example(income=80, increment=10)
@given(income=NONNEGATIVE, increment=POSITIVE_INCREMENT)
def test_nonbeneficiary_as_is_nonincreasing_in_income(tmp_path, income, increment):
tmp_path = _case_root(tmp_path)
fixture = make_entitlement_fixture(
Expand All @@ -262,7 +267,8 @@ def test_nonbeneficiary_as_is_nonincreasing_in_income(tmp_path, income, incremen


@EXECUTED
@given(costs=NONNEGATIVE, increment=NONNEGATIVE)
@example(costs=40, increment=20)
@given(costs=NONNEGATIVE, increment=POSITIVE_INCREMENT)
def test_as_is_nondecreasing_in_qualifying_costs_until_the_cap(
tmp_path, costs, increment
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
NumericScope,
Slice,
)
from microcosm.graph import kernel as graph_kernel
from microcosm.graph.canonical import canonical_json
from test_support.microcosm_build.transport_graph import (
COUNTRY,
Expand Down Expand Up @@ -211,6 +212,57 @@ def test_neutral_kernel_contracts_and_idempotent_registration() -> None:
assert kernel.capabilities.consumes_se is False


@pytest.mark.parametrize("distribution", ("numpy", "pandas", "scipy"))
def test_gap_identity_changes_with_numerical_dependency_version(
monkeypatch, distribution
) -> None:
before = TAKEUP_GAP.implementation_hash()
installed_version = graph_kernel.importlib_metadata.version
monkeypatch.setattr(
graph_kernel.importlib_metadata,
"version",
lambda name: (
installed_version(name) + ".changed"
if name == distribution
else installed_version(name)
),
)
assert TAKEUP_GAP.implementation_hash() != before


@pytest.mark.parametrize("kernel", (TAKEUP_GAP, TAKEUP_BANDS), ids=lambda k: k.ref)
def test_takeup_identity_is_unchanged_with_same_dependency_versions(
monkeypatch, kernel
) -> None:
before = kernel.implementation_hash()
installed_version = graph_kernel.importlib_metadata.version
monkeypatch.setattr(
graph_kernel.importlib_metadata,
"version",
lambda name: installed_version(name),
)
assert kernel.implementation_hash() == before
assert type(kernel)().implementation_hash() == before


@pytest.mark.parametrize("distribution", ("numpy", "pandas", "scipy"))
def test_bands_identity_ignores_numerical_dependency_versions(
monkeypatch, distribution
) -> None:
before = TAKEUP_BANDS.implementation_hash()
installed_version = graph_kernel.importlib_metadata.version
monkeypatch.setattr(
graph_kernel.importlib_metadata,
"version",
lambda name: (
installed_version(name) + ".changed"
if name == distribution
else installed_version(name)
),
)
assert TAKEUP_BANDS.implementation_hash() == before


@settings(
max_examples=24,
deadline=None,
Expand Down
18 changes: 18 additions & 0 deletions packages/microcosm-frame/src/microcosm/frame/rules_kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import json
import math
from collections.abc import Mapping
from dataclasses import replace
from types import MappingProxyType

import numpy as np
Expand Down Expand Up @@ -204,10 +205,27 @@ class SimulateCounterfactualKernel(SimulateRulesByRefKernel):
contain all policy coefficients and overrides; ``resource_sha256`` may
bind the declaring spec resource. Neither engine bindings nor spec
fingerprints enter the implementation hash.

NumPy and pandas versions enter the implementation hash because bridge
arithmetic and person/group reductions use their numerical operations.
Caller-supplied dependencies also bind the engines' behavior.
"""

ref = "simulate.counterfactual@1"

def __init__(
self,
engines: Mapping[str, RulesEngine],
dependencies: tuple[str, ...] = (),
) -> None:
super().__init__(engines, dependencies)
self.capabilities = replace(
self.capabilities,
dependencies=tuple(
dict.fromkeys((*self.capabilities.dependencies, "numpy", "pandas"))
),
)

def run(self, context: KernelContext) -> KernelResult:
components, terms, frame = self._prepare(context)
values = self._evaluate(frame, components, terms)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
SimulateSolveZeroKernel,
)
from microcosm.graph import KernelRegistry, Node, Owned, Slice
from microcosm.graph import kernel as graph_kernel
from test_support.microcosm_frame.kernels import _context
from tools import transport_bridge_differential as differential_tool

Expand Down Expand Up @@ -474,6 +475,69 @@ def test_adding_same_adapter_binding_does_not_change_bridge_implementation_hash(
assert first.implementation_hash() == second.implementation_hash()


@pytest.mark.parametrize("cls", [SimulateCounterfactualKernel, SimulateSolveZeroKernel])
@pytest.mark.parametrize("distribution", ["numpy", "pandas"])
def test_bridge_numerical_dependency_version_changes_implementation_hash(
monkeypatch, cls, distribution
):
kernel = cls({RATE_REF: RatesEngine()})
before = kernel.implementation_hash()
installed_version = graph_kernel.importlib_metadata.version
monkeypatch.setattr(
graph_kernel.importlib_metadata,
"version",
lambda name: (
installed_version(name) + (".changed" if name == distribution else "")
),
)
assert kernel.implementation_hash() != before


@pytest.mark.parametrize("cls", [SimulateCounterfactualKernel, SimulateSolveZeroKernel])
def test_bridge_implementation_hash_is_stable_for_unchanged_dependency_versions(
monkeypatch, cls
):
kernel = cls({RATE_REF: RatesEngine()})
before = kernel.implementation_hash()
versions = {
name: graph_kernel.importlib_metadata.version(name)
for name in kernel.capabilities.dependencies
}
monkeypatch.setattr(
graph_kernel.importlib_metadata, "version", versions.__getitem__
)
assert kernel.implementation_hash() == before
assert cls({RATE_REF: RatesEngine()}).implementation_hash() == before


@pytest.mark.parametrize("cls", [SimulateCounterfactualKernel, SimulateSolveZeroKernel])
def test_bridge_implementation_hash_ignores_unused_scipy_version(monkeypatch, cls):
kernel = cls({RATE_REF: RatesEngine()})
before = kernel.implementation_hash()
installed_version = graph_kernel.importlib_metadata.version
monkeypatch.setattr(
graph_kernel.importlib_metadata,
"version",
lambda name: installed_version(name) + (".changed" if name == "scipy" else ""),
)
assert kernel.implementation_hash() == before


@pytest.mark.parametrize("cls", [SimulateCounterfactualKernel, SimulateSolveZeroKernel])
def test_bridge_declared_engine_dependency_version_changes_implementation_hash(
monkeypatch, cls
):
kernel = cls({RATE_REF: RatesEngine()}, dependencies=("scipy",))
before = kernel.implementation_hash()
installed_version = graph_kernel.importlib_metadata.version
monkeypatch.setattr(
graph_kernel.importlib_metadata,
"version",
lambda name: installed_version(name) + (".changed" if name == "scipy" else ""),
)
assert kernel.implementation_hash() != before


@pytest.mark.parametrize(
"returned,match", [([1], "wrong shape"), ([np.nan, 1], "finite")]
)
Expand Down
Loading