Axiom-backed RulesEngine adapter: Belgium as the first Axiom-engine country#268
Conversation
…ountry populace.frame.adapters.axiom.AxiomEngine wraps the Axiom engine's dense vectorized surface for a RuleSpec country module — the first adapter proving the protocol against a second engine. Belgium (rulespec-be) is the pilot: BE_SCHEMA (person + household), an adapter-owned entity-table H5 dataset format (AxiomEntityTableDataset, US/UK single-year layout), and a strict export gate that rejects persisted derived columns. Decisions per populace#260: no materialize(..., reform=...) extension — the engine has no parameter overlay, so a counterfactual is a second adapter over a reform-compiled module; frame column dtypes are authoritative for engine inputs (RuleSpec inputs are declared by usage, not typed), and variable_metadata refuses input names rather than fabricating dtype/period. The RulesEngine behavioral contract now runs parameterized over adapters (test_rules_engine_contract.py): policyengine-us and Axiom pass the same suite. The Belgian pilot smoke reproduces hand-computed CIR 1992 article 130 liabilities (10,000/30,000/60,000 -> 2,500/9,612/23,620) and the full BE PIT pipeline materializes 200k persons in about a second. Provide side: TheAxiomFoundation/axiom-rules-engine#63. Fixes #260. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Axiom adapter only blocked columns that are derived rules of the compiled module, silently writing a formula_owned_excluded column that is not a module rule — the US adapter unions the contract list in. Union it in and add regression tests for the open-contract and closed-with-optional cases the shared contract suite never exercises. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial re-review (re-run of the review that died at last night's session limit)Reviewed at Defect → fix
Fixed by unioning the contract list in (one line, mirroring the US adapter) plus two regression tests: open-contract block (mirrors Clean probes (highlights)
Cosmetic (PR body, no code change)
🤖 Generated with Claude Code |
Fixes #260. Part of #259 (populace-be epic).
The first non-PolicyEngine
RulesEngineadapter:populace.frame.adapters.axiom.AxiomEnginewraps the Axiom rules engine's dense vectorized surface (axiom_rules_engine.CompiledDenseProgram) for a RuleSpec country module, with Belgium (rulespec-be) as the pilot. Provide-side counterpart: TheAxiomFoundation/axiom-rules-engine#63 (fixes the dense extension build, exposesexecute_f64andderived_metadata) — the engine-backed tests here need that branch.The four design decisions #260 asked for
_time_period— read/written byAxiomEntityTableDatasetin the adapter module (the adapter owns the format because there is no policyengine-be package).populace.datageneralizes by pointing a registry entry'sengine_module/engine_classhere; the registry entry itself lands with the release channel (Stand up the populace-be release channel #265).BE_SCHEMA = EntitySchema(group_entities=("household",)): Belgian PIT is individual with household-level elements, and BE-SILC arrives as person + household registers (Add the BE-SILC source stage with licence guardrails and a private artifact repo #262 owns populating them). RuleSpec scopes rules to engine entities (Person,Household); the adapter maps frame→engine names viaentity_names(default: capitalize). Engine entities with no mapped frame entity (rulespec-be also definesChild,Vehicle,Gift, ...) are invisible to the kernel until a frame entity is mapped to them — resolving or materializing their variables raises with the mapping named. Fiscal/benefit units beyond the household enter as group entities when the encoded slice needs them.derived_metadatasurface).write_datasetrejects any persisted derived column — strict from day one, matching the current US adapter posture (a stored engine output would mask reforms). Forbidden/required/defaults/closed-contract semantics mirrorPolicyEngineUSEngine.materialize(..., reform=...)protocol change; that extension stays on the protocol's known-future list for engines with true overlay support.One boundary the issue didn't anticipate: RuleSpec inputs are declared by usage, not typed — the dense surface enumerates input names but no dtypes. The frame's column dtypes are therefore authoritative (
bool→ Bool for truthiness-context predicates, integer → Integer, float → Decimal/f64), andvariable_metadatarefuses input names rather than fabricating dtype/period for them. Typed input specs are named follow-up on the engine side (TheAxiomFoundation/axiom-rules-engine#62).Acceptance, as verified
test_rules_engine_contract.pyruns one shared suite overPolicyEngineUSEngineandAxiomEngine: protocol conformance, schema/bundle agreement, metadata coherence, input enumeration (sorted, unique, computed excluded), row-aligned materialization, weight authority on export, round-trip persistence. Both cases pass locally; each case skips where its engine isn't installed (CI runs the US case once policyengine-us is present; the Axiom engine is not on PyPI yet).TestBelgianPilotSlice(gated onPOPULACE_RULESPEC_BEpointing at a rulespec-be checkout) materializesbelgium_pit_article_130_base_taxfor a toy 3-person frame and reproduces the hand-computed 2025 liabilities: taxable income 10,000 / 30,000 / 60,000 → 2,500 / 9,612 / 23,620 (25% to 16,320; 40% to 28,800; 45% to 49,840; 50% above). Passing locally against rulespec-be @ 7ddb923.axiom_rules_engineis imported lazily inside one method; the module imports and constructs without it (tested), and the no-engine error names the install path.arithmetic="f64"opts into the fast mode.Notes
populace-frame[axiom]carries only the PyPI-resolvable dependency (pytables for the HDF5 format); the engine installs from an axiom-rules-engine checkout until it publishes wheels — the ImportError documents this.tablesjoins the dev group so the dataset-format round-trip tests run in CI without any engine.materializeraisesNotImplementedErrornaming the relations if a module declares them.tests/fixtures/axiom_toy_country.yaml) is a self-contained RuleSpec with person + household scopes, a truthiness predicate, and a Month-period rule, so the full adapter surface exercises without the corpus.🤖 Generated with Claude Code