| name | policyengine-core |
| description | PolicyEngine Core simulation engine - the foundation powering all PolicyEngine calculations |
PolicyEngine Core
PolicyEngine Core is the microsimulation engine that powers all PolicyEngine calculations. It's a fork of OpenFisca-Core adapted for PolicyEngine's needs.
For Users
What is Core?
When you use policyengine.org to calculate taxes or benefits, PolicyEngine Core is the "calculator" running behind the scenes.
Core provides:
- The simulation engine that processes tax rules
- Variable and parameter management
- Entity relationships (person → family → household)
- Period handling (2024, 2025, etc.)
You don't interact with Core directly - you use it through:
- Web app: policyengine.org
- Python packages: policyengine-us, policyengine-uk
- API: api.policyengine.org
Why Core Matters
Core ensures:
- ✅ Accuracy - Calculations follow official rules exactly
- ✅ Consistency - Same rules applied everywhere
- ✅ Transparency - All rules traceable to legislation
- ✅ Performance - Vectorized calculations for speed
For Analysts
Understanding Core Concepts
When writing PolicyEngine code, you'll encounter Core concepts:
Variables:
- Represent quantities (income_tax, ctc, snap, etc.)
- Defined for specific entities (person, household, tax_unit)
- Calculated from formulas or set directly
Parameters:
- Policy rules that change over time (tax rates, benefit amounts)
- Organized hierarchically (gov.irs.credits.ctc.amount.base_amount)
- Stored in YAML files
Entities:
- Person: Individual
- Family: Family unit
- Tax unit: Tax filing unit
- Household: Physical household
- Marital unit: Marital status grouping
- SPM unit: Supplemental Poverty Measure unit
Periods:
- Year: 2024, 2025, etc.
- Month: 2024-01, 2024-02, etc.
- Specific dates: 2024-06-15
Core in Action
from policyengine_us import Simulation
sim = Simulation(situation=household)
result = sim.calculate("income_tax", 2026)
Core vs Country Packages
Core (policyengine-core):
- Generic simulation engine
- No specific tax/benefit rules
- Variable and parameter infrastructure
Country packages (policyengine-us, etc.):
- Built on Core
- Contain specific tax/benefit rules
- Define variables and parameters for that country
Relationship:
policyengine-core (engine)
↓ powers
policyengine-us (US rules)
↓ used by
policyengine-api (REST API)
↓ serves
policyengine-app (web interface)
For Contributors
Repository
Location: PolicyEngine/policyengine-core
Origin: Fork of OpenFisca-Core
Clone:
git clone https://github.com/PolicyEngine/policyengine-core
Current Architecture
To see current structure:
tree policyengine_core/
To understand a specific component:
cat policyengine_core/variables/variable.py
cat policyengine_core/parameters/parameter.py
cat policyengine_core/simulations/simulation.py
cat policyengine_core/entities/entity.py
Key Classes
Variable:
cat policyengine_core/variables/variable.py
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):
return calculate_tax(...)
Simulation:
cat policyengine_core/simulations/simulation.py
sim = Simulation(situation=situation)
sim.calculate("variable", period)
Parameters:
cat policyengine_core/parameters/parameter_node.py
parameters(period).gov.irs.credits.ctc.amount.base_amount
Vectorization (Critical!)
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
Why: Core processes many households simultaneously for performance.
To see vectorization examples:
grep -r "np.where" policyengine_core/
grep -r "select" policyengine_core/
Formula Dependencies
Core automatically resolves variable dependencies:
class taxable_income(Variable):
def formula(person, period, parameters):
agi = person("adjusted_gross_income", period)
deduction = person("standard_deduction", period)
return agi - deduction
class income_tax(Variable):
def formula(person, period, parameters):
taxable = person("taxable_income", period)
return apply_brackets(taxable, ...)
To see dependency resolution:
grep -r "trace" policyengine_core/simulations/
simulation.trace = True
simulation.calculate("income_tax", 2026)
Period Handling
To see period implementation:
cat policyengine_core/periods/period.py
Usage in variables:
definition_period = YEAR
definition_period = MONTH
yearly_value = person("monthly_income", period.this_year) * 12
Testing Core Changes
To run Core tests:
cd policyengine-core
make test
pytest tests/core/test_variables.py -v
To test in country package:
cd policyengine-us
uv pip install -e ../policyengine-core
make test
Key Differences from OpenFisca
PolicyEngine Core differs from OpenFisca-Core:
To see PolicyEngine changes:
git log --oneline
Core Development Workflow
Making Changes to Core
-
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"
Understanding Impact
Changes to Core affect:
- ✅ All country packages (US, UK, Canada, IL, NG)
- ✅ The API
- ✅ The web app
- ✅ All analysis tools
Critical: Always test in multiple country packages before merging.
Common Core Patterns
Pattern 1: Adding a New Variable Type
Current variable types:
grep "value_type" policyengine_core/variables/variable.py
Types: int, float, bool, str, Enum, date
Pattern 2: Custom Formulas
Formula signature:
def formula(entity, period, parameters):
return calculated_value
To see formula examples:
grep -A 10 "def formula" ../policyengine-us/policyengine_us/variables/ | head -50
Pattern 3: Parameter Access
Accessing parameters in formulas:
param = parameters(period).gov.irs.credits.ctc.amount.base_amount
To see parameter structure:
tree ../policyengine-us/policyengine_us/parameters/gov/
Advanced Topics
Formula Caching
Core caches calculations automatically:
tax1 = sim.calculate("income_tax", 2026)
tax2 = sim.calculate("income_tax", 2026)
Performance Optimization: Batching Parameter Lookups
When parameter lookups happen inside loops, batch them beforehand to avoid repeated function call overhead:
❌ Inefficient (repeated lookups):
for instant in instants:
value = uprating_parameter(instant)
✅ Efficient (batched lookups):
value_cache = {
instant: uprating_parameter(instant)
for instant in instants
}
for instant in instants:
value = value_cache[instant]
Why it matters:
- Parameter lookups involve instant/period conversions and tree traversal
- In large parameter sets (like policyengine-us), this can cause millions of redundant calls
- Example:
uprate_parameters reduced from 15s to 13.8s (8% improvement) by batching lookups
When to batch:
- Parameter lookups inside loops
- Multiple lookups of the same value at different points in code
- Any repeated
parameters(period).path.to.value calls
To find optimization opportunities:
python -m cProfile -o profile.stats -c "from policyengine_us.system import system"
grep -r "parameters(period)" policyengine_core/parameters/
Neutralizing Variables
reform = {
"income_tax": {
"2026-01-01.2100-12-31": 0
}
}
Adding Variables
Country packages add variables by inheriting from Core's Variable class.
See policyengine-us-skill for variable creation patterns.
Resources
Repository: https://github.com/PolicyEngine/policyengine-core
Documentation:
Related skills:
- policyengine-us-skill - Using Core through country packages
- policyengine-standards-skill - Code quality standards
Troubleshooting
Common Issues
Variable not found:
Scalar vs array operations:
Period mismatch:
To debug:
sim.trace = True
sim.calculate("variable", period)
Contributing to Core
Before contributing:
- Read Core README
- Understand OpenFisca architecture
- Test changes in multiple country packages
- Follow policyengine-standards-skill
Development standards:
- Python 3.10-3.13
- Ruff formatting (79-char)
- Comprehensive tests
- No breaking changes without discussion