From ed760d9c33640a4093471692eba2469a9285f42f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 03:19:53 -0400 Subject: [PATCH] Bind transport kernel identities to numerical dependency versions Declare NumPy, pandas and SciPy for takeup gaps, and NumPy and pandas for counterfactual and zero-solving bridges while preserving engine declarations. Keep bands independent of numerical libraries and test version changes, stable identities and unused dependencies. Require positive, representable monotonicity increments and add explicit income and cost examples that catch wrong-direction toy entitlement mutants. Co-Authored-By: GPT-6.1 Sol --- .../build/transport/takeup_kernels.py | 2 + .../test_transport_entitlement_graph.py | 10 ++- .../shared/test_transport_takeup_kernels.py | 52 +++++++++++++++ .../src/microcosm/frame/rules_kernels.py | 18 ++++++ .../shared/test_counterfactual_kernels.py | 64 +++++++++++++++++++ 5 files changed, 144 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py index 89a0f7a8a8..e1a4658a75 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py @@ -252,6 +252,7 @@ class TakeupGapKernel(KernelBase): Determinism.DETERMINISTIC, numeric=Numeric.BITWISE, seed_source=SeedSource.NONE, + dependencies=("numpy", "pandas", "scipy"), ) _required = frozenset( { @@ -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, diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_entitlement_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_entitlement_graph.py index 0cc5c07c74..8043e17a36 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_entitlement_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_entitlement_graph.py @@ -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): @@ -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( @@ -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 ): diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_takeup_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_takeup_kernels.py index bd7010a2c5..4cef3bbdc0 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_takeup_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_takeup_kernels.py @@ -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, @@ -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, diff --git a/packages/microcosm-frame/src/microcosm/frame/rules_kernels.py b/packages/microcosm-frame/src/microcosm/frame/rules_kernels.py index 9c7ad185c3..7453c0d1e4 100644 --- a/packages/microcosm-frame/src/microcosm/frame/rules_kernels.py +++ b/packages/microcosm-frame/src/microcosm/frame/rules_kernels.py @@ -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 @@ -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) diff --git a/packages/microcosm-frame/tests/engine_free/shared/test_counterfactual_kernels.py b/packages/microcosm-frame/tests/engine_free/shared/test_counterfactual_kernels.py index 581e80ae7c..7a4bfc7d7f 100644 --- a/packages/microcosm-frame/tests/engine_free/shared/test_counterfactual_kernels.py +++ b/packages/microcosm-frame/tests/engine_free/shared/test_counterfactual_kernels.py @@ -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 @@ -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")] )