PolicyEngine Core simulation engine - the foundation powering all PolicyEngine calculations
From essentialnpx claudepluginhub policyengine/policyengine-claude --plugin data-scienceThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
PolicyEngine Core is the microsimulation engine that powers all PolicyEngine calculations. It's a fork of OpenFisca-Core adapted for PolicyEngine's needs.
When you use policyengine.org to calculate taxes or benefits, PolicyEngine Core is the "calculator" running behind the scenes.
Core provides:
You don't interact with Core directly - you use it through:
Core ensures:
When writing PolicyEngine code, you'll encounter Core concepts:
Variables:
Parameters:
Entities:
Periods:
from policyengine_us import Simulation
# When you create a simulation
sim = Simulation(situation=household)
# Core manages:
# - Entity relationships
# - Variable dependencies
# - Parameter lookups
# - Period conversions
# When you calculate
result = sim.calculate("income_tax", 2026)
# Core:
# 1. Checks if already calculated
# 2. Identifies dependencies (income → AGI → taxable income → tax)
# 3. Calculates dependencies first
# 4. Applies formulas
# 5. Returns result
Core (policyengine-core):
Country packages (policyengine-us, etc.):
Relationship:
policyengine-core (engine)
↓ powers
policyengine-us (US rules)
↓ used by
policyengine-api (REST API)
↓ serves
policyengine-app (web interface)
Location: PolicyEngine/policyengine-core Origin: Fork of OpenFisca-Core
Clone:
git clone https://github.com/PolicyEngine/policyengine-core
To see current structure:
tree policyengine_core/
# Key directories:
# - variables/ - Variable class and infrastructure
# - parameters/ - Parameter class and infrastructure
# - entities/ - Entity definitions
# - simulations/ - Simulation class
# - periods/ - Period handling
# - reforms/ - Reform application
To understand a specific component:
# Variable system
cat policyengine_core/variables/variable.py
# Parameter system
cat policyengine_core/parameters/parameter.py
# Simulation engine
cat policyengine_core/simulations/simulation.py
# Entity system
cat policyengine_core/entities/entity.py
Variable:
# To see Variable class implementation
cat policyengine_core/variables/variable.py
# Variables in country packages inherit from this:
from policyengine_core.variables import Variable
class income_tax(Variable):
value_type = float
entity = Person
label = "Income tax"
definition_period = YEAR
def formula(person, period, parameters):
# Vectorized formula
return calculate_tax(...)
Simulation:
# To see Simulation class implementation
cat policyengine_core/simulations/simulation.py
# Manages calculation graph and caching
sim = Simulation(situation=situation)
sim.calculate("variable", period)
Parameters:
# To see Parameter handling
cat policyengine_core/parameters/parameter_node.py
# Access in formulas:
parameters(period).gov.irs.credits.ctc.amount.base_amount
Core requires vectorized operations - no if-elif-else with arrays:
❌ Wrong (scalar logic):
if age < 18:
eligible = True
else:
eligible = False
✅ Correct (vectorized):
eligible = age < 18 # NumPy boolean array
Why: Core processes many households simultaneously for performance.
To see vectorization examples:
# Search for where() usage (vectorized if-then-else)
grep -r "np.where" policyengine_core/
# Find select() usage (vectorized case statements)
grep -r "select" policyengine_core/
Core automatically resolves variable dependencies:
class taxable_income(Variable):
def formula(person, period, parameters):
# Core automatically calculates these first:
agi = person("adjusted_gross_income", period)
deduction = person("standard_deduction", period)
return agi - deduction
class income_tax(Variable):
def formula(person, period, parameters):
# Core knows to calculate taxable_income first
taxable = person("taxable_income", period)
return apply_brackets(taxable, ...)
To see dependency resolution:
# Find trace functionality
grep -r "trace" policyengine_core/simulations/
# Enable in your code:
simulation.trace = True
simulation.calculate("income_tax", 2026)
To see period implementation:
cat policyengine_core/periods/period.py
# Period types:
# - YEAR: 2024
# - MONTH: 2024-01
# - ETERNITY: permanent values
Usage in variables:
# Annual variable
definition_period = YEAR # Called with 2024
# Monthly variable
definition_period = MONTH # Called with "2024-01"
# Convert periods
yearly_value = person("monthly_income", period.this_year) * 12
To run Core tests:
cd policyengine-core
make test
# Specific test
pytest tests/core/test_variables.py -v
To test in country package:
# Changes to Core affect all country packages
cd policyengine-us
uv pip install -e ../policyengine-core # Local development install
make test
PolicyEngine Core differs from OpenFisca-Core:
To see PolicyEngine changes:
# Compare to OpenFisca
# Core fork diverged to add:
# - Enhanced performance
# - Better error messages
# - PolicyEngine-specific features
# See commit history for PolicyEngine changes
git log --oneline
Clone repo:
git clone https://github.com/PolicyEngine/policyengine-core
Install for development:
make install
Make changes to variable.py, simulation.py, etc.
Test locally:
make test
Test in country package:
cd ../policyengine-us
uv pip install -e ../policyengine-core
make test
Format and commit:
make format
git commit -m "Description"
Changes to Core affect:
Critical: Always test in multiple country packages before merging.
Current variable types:
# See supported types
grep "value_type" policyengine_core/variables/variable.py
Types: int, float, bool, str, Enum, date
Formula signature:
def formula(entity, period, parameters):
# entity: Person, TaxUnit, Household, etc.
# period: 2024, "2024-01", etc.
# parameters: Parameter tree for period
return calculated_value
To see formula examples:
# Search country packages for formulas
grep -A 10 "def formula" ../policyengine-us/policyengine_us/variables/ | head -50
Accessing parameters in formulas:
# Navigate parameter tree
param = parameters(period).gov.irs.credits.ctc.amount.base_amount
# Parameters automatically valid for period
# No need to check dates manually
To see parameter structure:
# Example from country package
tree ../policyengine-us/policyengine_us/parameters/gov/
Core caches calculations automatically:
# First call calculates
tax1 = sim.calculate("income_tax", 2026)
# Second call returns cached value
tax2 = sim.calculate("income_tax", 2026) # Instant
When parameter lookups happen inside loops, batch them beforehand to avoid repeated function call overhead:
❌ Inefficient (repeated lookups):
# Inside uprate_parameters or similar functions
for instant in instants:
value = uprating_parameter(instant) # Repeated function calls
# ... use value
✅ Efficient (batched lookups):
# Pre-compute all values before the loop
value_cache = {
instant: uprating_parameter(instant)
for instant in instants
}
# Use cached values in loop
for instant in instants:
value = value_cache[instant] # Fast dictionary lookup
# ... use value
Why it matters:
uprate_parameters reduced from 15s to 13.8s (8% improvement) by batching lookupsWhen to batch:
parameters(period).path.to.value callsTo find optimization opportunities:
# Profile import time
python -m cProfile -o profile.stats -c "from policyengine_us.system import system"
# Search for parameter lookup hotspots
grep -r "parameters(period)" policyengine_core/parameters/
# Set variable to zero in reform
reform = {
"income_tax": {
"2026-01-01.2100-12-31": 0
}
}
Country packages add variables by inheriting from Core's Variable class.
See policyengine-us-skill for variable creation patterns.
Repository: https://github.com/PolicyEngine/policyengine-core
Documentation:
Related skills:
Variable not found:
# Error: Variable 'income_tax' not found
# Solution: Variable is defined in country package, not Core
# Use policyengine-us, not policyengine-core directly
Scalar vs array operations:
# Error: truth value of array is ambiguous
# Solution: Use np.where() instead of if-else
# See vectorization section above
Period mismatch:
# Error: Cannot compute variable_name for period 2024-01
# Solution: Check definition_period matches request
# YEAR variables need YEAR periods (2024, not "2024-01")
To debug:
# Enable tracing
sim.trace = True
sim.calculate("variable", period)
# See calculation dependency tree
Before contributing:
Development standards: