Initial commit
This commit is contained in:
18
.claude-plugin/plugin.json
Normal file
18
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "api-development",
|
||||||
|
"description": "API development - Flask endpoints, caching, services, and integration patterns",
|
||||||
|
"version": "0.0.0-2025.11.28",
|
||||||
|
"author": {
|
||||||
|
"name": "PolicyEngine",
|
||||||
|
"email": "hello@policyengine.org"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/policyengine-api-skill",
|
||||||
|
"./skills/policyengine-core-skill",
|
||||||
|
"./skills/policyengine-us-skill",
|
||||||
|
"./skills/policyengine-uk-skill",
|
||||||
|
"./skills/policyengine-design-skill",
|
||||||
|
"./skills/policyengine-standards-skill",
|
||||||
|
"./skills/policyengine-writing-skill"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# api-development
|
||||||
|
|
||||||
|
API development - Flask endpoints, caching, services, and integration patterns
|
||||||
100
plugin.lock.json
Normal file
100
plugin.lock.json
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:PolicyEngine/policyengine-claude:api-development",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "fc9b2ce7ec8753ea2421e3fab88e06f8229cd387",
|
||||||
|
"treeHash": "04bb71ef391bee5bc67d21d30c90dbcb03f391e635de60d70e9c6f5141612b1c",
|
||||||
|
"generatedAt": "2025-11-28T10:12:36.259353Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "api-development",
|
||||||
|
"description": "API development - Flask endpoints, caching, services, and integration patterns"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "10ac80e0b432371a1db39c245358469918e1aa3829bd6890d0266ea27f000e49"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "6bbcc684fe3787dabf68f8a886bcf9b9e62c2b21f3ea2c5c0f6914aacfbff9d7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-writing-skill/SKILL.md",
|
||||||
|
"sha256": "7b1b6dfecb7db0cfde2cc87548e5292c4851c1d9062b549907cbcd9959d7fb19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-us-skill/SKILL.md",
|
||||||
|
"sha256": "434d93548e3c792320c2ac4e736ec3327218829df30e3b7f06336bac833b2833"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-us-skill/examples/donation_sweep.yaml",
|
||||||
|
"sha256": "2d9294d931daa667c66c8a8a011524d15adbafb068f764b3d261086e0774ff7e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-us-skill/examples/single_filer.yaml",
|
||||||
|
"sha256": "d14503e102c796e2141d56d85db4405f1681756d58158469f97ebc0a6c0f022f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-us-skill/scripts/situation_helpers.py",
|
||||||
|
"sha256": "0fa2858e702ff64bbece06e1b383d9a07a034abb72f5467323aca8c8db40a97d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-standards-skill/SKILL.md",
|
||||||
|
"sha256": "8d59f51c19d85bfd2d3cba44e51e33a2061e34bc7523a10a0811a0919e76aa5b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-uk-skill/SKILL.md",
|
||||||
|
"sha256": "02cfc0e284c7d57be4cc265a6b57145f5485022a9b4b29a300e43c575a476890"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml",
|
||||||
|
"sha256": "6d8577eded1f047b7c064b4344de3e1cdb85e410f6d3240986fe44f2efef4669"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-uk-skill/examples/couple.yaml",
|
||||||
|
"sha256": "e1d115fae8f72cea29c00fb2aeb712bff8e2f6ed29ddd6515692695a1a79ffc1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-uk-skill/examples/family_with_children.yaml",
|
||||||
|
"sha256": "b6f69b29ae9827b20a2d01c8fc5daf614d1fe875d4d680b7a21273f707671c16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-uk-skill/examples/single_person.yaml",
|
||||||
|
"sha256": "5f96d30e99f1e0f834cdcaf575bc260d681314d614f27d975f60d05202606995"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-uk-skill/scripts/situation_helpers.py",
|
||||||
|
"sha256": "8281fad497f12c98b82a10a5474e502a2e48dd4ece0bdc2ff6da846b9fc12049"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-design-skill/SKILL.md",
|
||||||
|
"sha256": "ca0653af9eda135b5f98c64446b35474ba7bd641dae111c666e6f6995e39c70c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-api-skill/SKILL.md",
|
||||||
|
"sha256": "017c98bf207b8374ab6bb7d3ab2eaff3fc8e6358b61461d24e7a68093f8cf2b9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/policyengine-core-skill/SKILL.md",
|
||||||
|
"sha256": "c4b7c24d87a787c018a9595925f5dfa1eed52540ca804cff6d4706dfdca96b33"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "04bb71ef391bee5bc67d21d30c90dbcb03f391e635de60d70e9c6f5141612b1c"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
478
skills/policyengine-api-skill/SKILL.md
Normal file
478
skills/policyengine-api-skill/SKILL.md
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
---
|
||||||
|
name: policyengine-api
|
||||||
|
description: PolicyEngine API - Flask REST service powering policyengine.org and programmatic access
|
||||||
|
---
|
||||||
|
|
||||||
|
# PolicyEngine API
|
||||||
|
|
||||||
|
The PolicyEngine API is a Flask-based REST service that provides tax and benefit calculations for the web app and programmatic users.
|
||||||
|
|
||||||
|
## For Users 👥
|
||||||
|
|
||||||
|
### What is the API?
|
||||||
|
|
||||||
|
When you use policyengine.org, the API processes your calculations on our servers.
|
||||||
|
|
||||||
|
**API base:** https://api.policyengine.org
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Runs tax and benefit calculations
|
||||||
|
- Stores and retrieves policy reforms
|
||||||
|
- Computes population-wide impacts
|
||||||
|
- Serves parameter and variable metadata
|
||||||
|
|
||||||
|
### Public Access
|
||||||
|
|
||||||
|
The API is publicly accessible with rate limits:
|
||||||
|
- **Unauthenticated:** 100 requests/minute
|
||||||
|
- **Authenticated:** 1,000 requests/minute
|
||||||
|
|
||||||
|
**Try it:**
|
||||||
|
```bash
|
||||||
|
curl https://api.policyengine.org/us/policy/2
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
**OpenAPI spec:** https://api.policyengine.org/docs
|
||||||
|
|
||||||
|
**Interactive docs:** Swagger UI at API docs endpoint
|
||||||
|
|
||||||
|
## For Analysts 📊
|
||||||
|
|
||||||
|
### Using the API
|
||||||
|
|
||||||
|
**Option 1: Python client (recommended)**
|
||||||
|
```python
|
||||||
|
# Use the policyengine package
|
||||||
|
# See policyengine-python-client-skill
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Direct API calls**
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Calculate household impact
|
||||||
|
response = requests.post(
|
||||||
|
"https://api.policyengine.org/us/calculate",
|
||||||
|
json={
|
||||||
|
"household": household_situation,
|
||||||
|
"policy_id": None # or reform_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = response.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Endpoints
|
||||||
|
|
||||||
|
**Household calculations:**
|
||||||
|
```
|
||||||
|
POST /us/calculate
|
||||||
|
POST /uk/calculate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Policy management:**
|
||||||
|
```
|
||||||
|
GET /us/policy/{policy_id}
|
||||||
|
POST /us/policy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Economy impacts:**
|
||||||
|
```
|
||||||
|
GET /us/economy/{policy_id}/over/{baseline_policy_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Metadata:**
|
||||||
|
```
|
||||||
|
GET /us/parameters
|
||||||
|
GET /us/variables
|
||||||
|
GET /us/parameter/{parameter_name}
|
||||||
|
GET /us/variable/{variable_name}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limits and Performance
|
||||||
|
|
||||||
|
**Rate limits:**
|
||||||
|
- 100 req/min (unauthenticated)
|
||||||
|
- 1,000 req/min (authenticated - contact team)
|
||||||
|
|
||||||
|
**Response times:**
|
||||||
|
- Household calculation: ~200-500ms
|
||||||
|
- Population impact: ~5-30 seconds
|
||||||
|
- Cached results: <100ms
|
||||||
|
|
||||||
|
**Optimization:**
|
||||||
|
- Use the same policy_id for multiple requests (caching)
|
||||||
|
- Batch calculations when possible
|
||||||
|
- Use webhooks for long-running jobs (population impacts)
|
||||||
|
|
||||||
|
## For Contributors 💻
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
**Location:** PolicyEngine/policyengine-api
|
||||||
|
|
||||||
|
**Clone:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/PolicyEngine/policyengine-api
|
||||||
|
cd policyengine-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Architecture
|
||||||
|
|
||||||
|
**To see current structure:**
|
||||||
|
```bash
|
||||||
|
tree policyengine_api/
|
||||||
|
|
||||||
|
# Key directories:
|
||||||
|
ls policyengine_api/
|
||||||
|
# - endpoints/ - HTTP endpoint handlers
|
||||||
|
# - routes/ - Route registration
|
||||||
|
# - services/ - Business logic
|
||||||
|
# - compute_api/ - Calculation services
|
||||||
|
# - economy_api/ - Economy impact calculations
|
||||||
|
# - utils/ - Helpers (caching, validation)
|
||||||
|
# - data/ - Static data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Implementation Patterns
|
||||||
|
|
||||||
|
**Reference endpoint (read this first):**
|
||||||
|
```bash
|
||||||
|
cat policyengine_api/endpoints/economy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**This demonstrates:**
|
||||||
|
- Standard endpoint structure
|
||||||
|
- Request validation
|
||||||
|
- Caching pattern
|
||||||
|
- Error handling
|
||||||
|
- Response formatting
|
||||||
|
|
||||||
|
**To find other endpoints:**
|
||||||
|
```bash
|
||||||
|
ls policyengine_api/endpoints/
|
||||||
|
# - household.py
|
||||||
|
# - policy.py
|
||||||
|
# - economy.py
|
||||||
|
# - metadata.py
|
||||||
|
# - etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard Endpoint Pattern (Stable)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from policyengine_api.utils import cache
|
||||||
|
|
||||||
|
blueprint = Blueprint("my_endpoint", __name__)
|
||||||
|
|
||||||
|
@blueprint.route("/us/calculate", methods=["POST"])
|
||||||
|
def calculate():
|
||||||
|
"""Standard pattern: validate, cache-check, compute, cache, return."""
|
||||||
|
try:
|
||||||
|
# 1. Get and validate input
|
||||||
|
data = request.json
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "No data provided"}), 400
|
||||||
|
|
||||||
|
# 2. Generate cache key
|
||||||
|
cache_key = f"calc_{hash(str(data))}"
|
||||||
|
|
||||||
|
# 3. Check cache
|
||||||
|
cached = cache.get(cache_key)
|
||||||
|
if cached:
|
||||||
|
return jsonify(cached)
|
||||||
|
|
||||||
|
# 4. Compute
|
||||||
|
result = perform_calculation(data)
|
||||||
|
|
||||||
|
# 5. Cache result
|
||||||
|
cache.set(cache_key, result, expire=3600)
|
||||||
|
|
||||||
|
# 6. Return
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e), "status": "error"}), 500
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current implementation details:**
|
||||||
|
```bash
|
||||||
|
# See actual endpoint for current pattern
|
||||||
|
cat policyengine_api/endpoints/household.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
|
||||||
|
**To see current caching implementation:**
|
||||||
|
```bash
|
||||||
|
# Redis configuration
|
||||||
|
cat policyengine_api/utils/cache.py
|
||||||
|
|
||||||
|
# Find cache usage
|
||||||
|
grep -r "cache\." policyengine_api/endpoints/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
- Redis for caching
|
||||||
|
- Cache keys based on inputs
|
||||||
|
- TTL varies by endpoint (1 hour to 1 day)
|
||||||
|
- Clear cache on parameter changes
|
||||||
|
|
||||||
|
### Background Jobs
|
||||||
|
|
||||||
|
For long-running calculations (population impacts):
|
||||||
|
|
||||||
|
**To see current implementation:**
|
||||||
|
```bash
|
||||||
|
# RQ (Redis Queue) usage
|
||||||
|
grep -r "@job" policyengine_api/
|
||||||
|
|
||||||
|
# Job patterns
|
||||||
|
cat policyengine_api/economy_api/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
- Use RQ for jobs > 5 seconds
|
||||||
|
- Return job_id immediately
|
||||||
|
- Poll for completion
|
||||||
|
- Cache results
|
||||||
|
|
||||||
|
### Country Integration
|
||||||
|
|
||||||
|
**How API loads country packages:**
|
||||||
|
```bash
|
||||||
|
cat policyengine_api/country.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
- Dynamically imports country packages
|
||||||
|
- Routes by country code (/us/, /uk/)
|
||||||
|
- Manages multiple model versions
|
||||||
|
|
||||||
|
### Service Layer
|
||||||
|
|
||||||
|
**Business logic separated from endpoints:**
|
||||||
|
```bash
|
||||||
|
ls policyengine_api/services/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
```python
|
||||||
|
# endpoints/household.py
|
||||||
|
from policyengine_api.services import household_service
|
||||||
|
|
||||||
|
@app.route("/us/calculate", methods=["POST"])
|
||||||
|
def calculate():
|
||||||
|
result = household_service.calculate(data)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
# services/household_service.py
|
||||||
|
def calculate(data):
|
||||||
|
# Business logic here
|
||||||
|
simulation = create_simulation(data)
|
||||||
|
return simulation.calculate(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
**To see current test patterns:**
|
||||||
|
```bash
|
||||||
|
ls tests/
|
||||||
|
cat tests/test_household.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run tests:**
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
pytest tests/test_economy.py -v
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
make test-coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Server
|
||||||
|
|
||||||
|
**Start locally:**
|
||||||
|
```bash
|
||||||
|
make debug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test endpoint:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/us/policy/2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
**To see deployment configuration:**
|
||||||
|
```bash
|
||||||
|
# Google Cloud Platform
|
||||||
|
cat app.yaml # App Engine config
|
||||||
|
cat cloudbuild.yaml # Cloud Build config
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
cat .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current deployment:**
|
||||||
|
- Google App Engine
|
||||||
|
- Cloud SQL (PostgreSQL)
|
||||||
|
- Redis (caching)
|
||||||
|
- Cloud Build (CI/CD)
|
||||||
|
|
||||||
|
### API Versions
|
||||||
|
|
||||||
|
**To see versioning strategy:**
|
||||||
|
```bash
|
||||||
|
grep -r "version" policyengine_api/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current approach:**
|
||||||
|
- API version in URLs (may add /v1/ prefix)
|
||||||
|
- Country package versions independent
|
||||||
|
- Breaking changes rare (backwards compatible)
|
||||||
|
|
||||||
|
## Architecture Diagrams
|
||||||
|
|
||||||
|
### Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User/App → API Gateway → Flask App → Country Package → Core Engine
|
||||||
|
↓
|
||||||
|
Redis Cache
|
||||||
|
↓
|
||||||
|
Background Job (if needed)
|
||||||
|
↓
|
||||||
|
PostgreSQL (storage)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
policyengine-core
|
||||||
|
↓
|
||||||
|
policyengine-us, policyengine-uk, etc.
|
||||||
|
↓
|
||||||
|
policyengine-api (you are here)
|
||||||
|
↓
|
||||||
|
policyengine-app (consumes API)
|
||||||
|
```
|
||||||
|
|
||||||
|
**To understand dependencies:**
|
||||||
|
- See `policyengine-core-skill` for engine patterns
|
||||||
|
- See `policyengine-us-skill` for country model usage
|
||||||
|
- See `policyengine-app-skill` for how app calls API
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Task 1: Add New Endpoint
|
||||||
|
|
||||||
|
1. **Study reference implementation:**
|
||||||
|
```bash
|
||||||
|
cat policyengine_api/endpoints/economy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create new endpoint file:**
|
||||||
|
```python
|
||||||
|
# policyengine_api/endpoints/my_endpoint.py
|
||||||
|
# Follow the pattern from economy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Register route:**
|
||||||
|
```bash
|
||||||
|
# See route registration
|
||||||
|
cat policyengine_api/routes/__init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Add tests:**
|
||||||
|
```bash
|
||||||
|
# Follow test pattern
|
||||||
|
cat tests/test_economy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Modify Caching Behavior
|
||||||
|
|
||||||
|
**See current caching:**
|
||||||
|
```bash
|
||||||
|
cat policyengine_api/utils/cache.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common changes:**
|
||||||
|
- Adjust TTL (time to live)
|
||||||
|
- Change cache key generation
|
||||||
|
- Add cache invalidation
|
||||||
|
|
||||||
|
### Task 3: Update Country Package Version
|
||||||
|
|
||||||
|
**To see how versions are managed:**
|
||||||
|
```bash
|
||||||
|
# Requirements
|
||||||
|
cat requirements.txt | grep policyengine-
|
||||||
|
|
||||||
|
# Update and deploy
|
||||||
|
# See deployment docs in README
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security and Best Practices
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
|
||||||
|
**Always validate:**
|
||||||
|
- Country code (us, uk, ca)
|
||||||
|
- Policy ID format
|
||||||
|
- Household structure
|
||||||
|
- Parameter values
|
||||||
|
|
||||||
|
**See validation examples:**
|
||||||
|
```bash
|
||||||
|
grep -r "validate" policyengine_api/endpoints/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
**Standard error response:**
|
||||||
|
```python
|
||||||
|
return jsonify({
|
||||||
|
"error": "Error message",
|
||||||
|
"details": additional_context,
|
||||||
|
"status": "error"
|
||||||
|
}), status_code
|
||||||
|
```
|
||||||
|
|
||||||
|
**See error patterns:**
|
||||||
|
```bash
|
||||||
|
grep -A 5 "jsonify.*error" policyengine_api/endpoints/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
**To see logging configuration:**
|
||||||
|
```bash
|
||||||
|
cat policyengine_api/gcp_logging.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
- Google Cloud Logging
|
||||||
|
- Log all errors
|
||||||
|
- Log slow queries (>1s)
|
||||||
|
- Don't log sensitive data
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **policyengine-python-client-skill** - Using the API
|
||||||
|
- **policyengine-core-skill** - Understanding the engine
|
||||||
|
- **policyengine-us-skill** - Country model integration
|
||||||
|
- **policyengine-app-skill** - How app consumes API
|
||||||
|
- **policyengine-standards-skill** - Code quality
|
||||||
|
- **policyengine-writing-skill** - API documentation style
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
**Repository:** https://github.com/PolicyEngine/policyengine-api
|
||||||
|
**Live API:** https://api.policyengine.org
|
||||||
|
**Documentation:** https://api.policyengine.org/docs
|
||||||
|
**Status:** https://status.policyengine.org
|
||||||
487
skills/policyengine-core-skill/SKILL.md
Normal file
487
skills/policyengine-core-skill/SKILL.md
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
```python
|
||||||
|
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", 2024)
|
||||||
|
|
||||||
|
# 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 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:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/PolicyEngine/policyengine-core
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Architecture
|
||||||
|
|
||||||
|
**To see current structure:**
|
||||||
|
```bash
|
||||||
|
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:**
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Classes
|
||||||
|
|
||||||
|
**Variable:**
|
||||||
|
```python
|
||||||
|
# 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:**
|
||||||
|
```python
|
||||||
|
# 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:**
|
||||||
|
```python
|
||||||
|
# To see Parameter handling
|
||||||
|
cat policyengine_core/parameters/parameter_node.py
|
||||||
|
|
||||||
|
# Access in formulas:
|
||||||
|
parameters(period).gov.irs.credits.ctc.amount.base_amount
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vectorization (Critical!)
|
||||||
|
|
||||||
|
Core requires vectorized operations - no if-elif-else with arrays:
|
||||||
|
|
||||||
|
**❌ Wrong (scalar logic):**
|
||||||
|
```python
|
||||||
|
if age < 18:
|
||||||
|
eligible = True
|
||||||
|
else:
|
||||||
|
eligible = False
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Correct (vectorized):**
|
||||||
|
```python
|
||||||
|
eligible = age < 18 # NumPy boolean array
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Core processes many households simultaneously for performance.
|
||||||
|
|
||||||
|
**To see vectorization examples:**
|
||||||
|
```bash
|
||||||
|
# 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/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Formula Dependencies
|
||||||
|
|
||||||
|
Core automatically resolves variable dependencies:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:**
|
||||||
|
```bash
|
||||||
|
# Find trace functionality
|
||||||
|
grep -r "trace" policyengine_core/simulations/
|
||||||
|
|
||||||
|
# Enable in your code:
|
||||||
|
simulation.trace = True
|
||||||
|
simulation.calculate("income_tax", 2024)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Period Handling
|
||||||
|
|
||||||
|
**To see period implementation:**
|
||||||
|
```bash
|
||||||
|
cat policyengine_core/periods/period.py
|
||||||
|
|
||||||
|
# Period types:
|
||||||
|
# - YEAR: 2024
|
||||||
|
# - MONTH: 2024-01
|
||||||
|
# - ETERNITY: permanent values
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in variables:**
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Core Changes
|
||||||
|
|
||||||
|
**To run Core tests:**
|
||||||
|
```bash
|
||||||
|
cd policyengine-core
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
pytest tests/core/test_variables.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**To test in country package:**
|
||||||
|
```bash
|
||||||
|
# Changes to Core affect all country packages
|
||||||
|
cd policyengine-us
|
||||||
|
pip install -e ../policyengine-core # Local development install
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Differences from OpenFisca
|
||||||
|
|
||||||
|
PolicyEngine Core differs from OpenFisca-Core:
|
||||||
|
|
||||||
|
**To see PolicyEngine changes:**
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Development Workflow
|
||||||
|
|
||||||
|
### Making Changes to Core
|
||||||
|
|
||||||
|
1. **Clone repo:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/PolicyEngine/policyengine-core
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install for development:**
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make changes** to variable.py, simulation.py, etc.
|
||||||
|
|
||||||
|
4. **Test locally:**
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Test in country package:**
|
||||||
|
```bash
|
||||||
|
cd ../policyengine-us
|
||||||
|
pip install -e ../policyengine-core
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Format and commit:**
|
||||||
|
```bash
|
||||||
|
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:**
|
||||||
|
```bash
|
||||||
|
# See supported types
|
||||||
|
grep "value_type" policyengine_core/variables/variable.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Types:** int, float, bool, str, Enum, date
|
||||||
|
|
||||||
|
### Pattern 2: Custom Formulas
|
||||||
|
|
||||||
|
**Formula signature:**
|
||||||
|
```python
|
||||||
|
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:**
|
||||||
|
```bash
|
||||||
|
# Search country packages for formulas
|
||||||
|
grep -A 10 "def formula" ../policyengine-us/policyengine_us/variables/ | head -50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Parameter Access
|
||||||
|
|
||||||
|
**Accessing parameters in formulas:**
|
||||||
|
```python
|
||||||
|
# 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:**
|
||||||
|
```bash
|
||||||
|
# Example from country package
|
||||||
|
tree ../policyengine-us/policyengine_us/parameters/gov/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Formula Caching
|
||||||
|
|
||||||
|
Core caches calculations automatically:
|
||||||
|
```python
|
||||||
|
# First call calculates
|
||||||
|
tax1 = sim.calculate("income_tax", 2024)
|
||||||
|
|
||||||
|
# Second call returns cached value
|
||||||
|
tax2 = sim.calculate("income_tax", 2024) # Instant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neutralizing Variables
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set variable to zero in reform
|
||||||
|
reform = {
|
||||||
|
"income_tax": {
|
||||||
|
"2024-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:**
|
||||||
|
- Core API docs (see README in repo)
|
||||||
|
- OpenFisca docs (original): https://openfisca.org/doc/
|
||||||
|
|
||||||
|
**Related skills:**
|
||||||
|
- **policyengine-us-skill** - Using Core through country packages
|
||||||
|
- **policyengine-standards-skill** - Code quality standards
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Variable not found:**
|
||||||
|
```python
|
||||||
|
# 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:**
|
||||||
|
```python
|
||||||
|
# Error: truth value of array is ambiguous
|
||||||
|
# Solution: Use np.where() instead of if-else
|
||||||
|
# See vectorization section above
|
||||||
|
```
|
||||||
|
|
||||||
|
**Period mismatch:**
|
||||||
|
```python
|
||||||
|
# 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:**
|
||||||
|
```python
|
||||||
|
# Enable tracing
|
||||||
|
sim.trace = True
|
||||||
|
sim.calculate("variable", period)
|
||||||
|
# See calculation dependency tree
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing to Core
|
||||||
|
|
||||||
|
**Before contributing:**
|
||||||
|
1. Read Core README
|
||||||
|
2. Understand OpenFisca architecture
|
||||||
|
3. Test changes in multiple country packages
|
||||||
|
4. Follow policyengine-standards-skill
|
||||||
|
|
||||||
|
**Development standards:**
|
||||||
|
- Python 3.10-3.13
|
||||||
|
- Black formatting (79-char)
|
||||||
|
- Comprehensive tests
|
||||||
|
- No breaking changes without discussion
|
||||||
880
skills/policyengine-design-skill/SKILL.md
Normal file
880
skills/policyengine-design-skill/SKILL.md
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
---
|
||||||
|
name: policyengine-design
|
||||||
|
description: PolicyEngine visual identity - colors, fonts, logos, and branding for web apps, calculators, charts, and research
|
||||||
|
---
|
||||||
|
|
||||||
|
# PolicyEngine Design System
|
||||||
|
|
||||||
|
PolicyEngine's visual identity and branding guidelines for creating consistent user experiences across web apps, calculators, charts, and research outputs.
|
||||||
|
|
||||||
|
## For Users 👥
|
||||||
|
|
||||||
|
### PolicyEngine Visual Identity
|
||||||
|
|
||||||
|
**Brand colors:**
|
||||||
|
- **Teal** (#39C6C0) - Primary accent color (buttons, highlights, interactive elements)
|
||||||
|
- **Blue** (#2C6496) - Secondary color (links, charts, headers)
|
||||||
|
|
||||||
|
**Typography:**
|
||||||
|
- **Charts:** Roboto Serif
|
||||||
|
- **Web app:** System fonts (sans-serif)
|
||||||
|
- **Streamlit apps:** Default sans-serif
|
||||||
|
|
||||||
|
**Logo:**
|
||||||
|
- Used in charts (bottom right)
|
||||||
|
- Blue version for light backgrounds
|
||||||
|
- White version for dark backgrounds
|
||||||
|
|
||||||
|
### Recognizing PolicyEngine Content
|
||||||
|
|
||||||
|
**You can identify PolicyEngine content by:**
|
||||||
|
- Teal accent color (#39C6C0) on buttons and interactive elements
|
||||||
|
- Blue (#2C6496) in charts and links
|
||||||
|
- Roboto Serif font in charts
|
||||||
|
- PolicyEngine logo in chart footer
|
||||||
|
- Clean, minimal white backgrounds
|
||||||
|
- Data-focused, quantitative presentation
|
||||||
|
|
||||||
|
## For Analysts 📊
|
||||||
|
|
||||||
|
### Chart Branding
|
||||||
|
|
||||||
|
When creating charts for PolicyEngine analysis, follow these guidelines:
|
||||||
|
|
||||||
|
#### Color Palette
|
||||||
|
|
||||||
|
**Primary colors:**
|
||||||
|
```python
|
||||||
|
TEAL_ACCENT = "#39C6C0" # Primary color (teal)
|
||||||
|
BLUE_PRIMARY = "#2C6496" # Secondary color (blue)
|
||||||
|
DARK_GRAY = "#616161" # Text color
|
||||||
|
```
|
||||||
|
|
||||||
|
**Extended palette:**
|
||||||
|
```python
|
||||||
|
# Blues
|
||||||
|
BLUE = "#2C6496"
|
||||||
|
BLUE_LIGHT = "#D8E6F3"
|
||||||
|
BLUE_PRESSED = "#17354F"
|
||||||
|
BLUE_98 = "#F7FAFD"
|
||||||
|
DARK_BLUE_HOVER = "#1d3e5e"
|
||||||
|
DARKEST_BLUE = "#0C1A27"
|
||||||
|
|
||||||
|
# Teals
|
||||||
|
TEAL_ACCENT = "#39C6C0"
|
||||||
|
TEAL_LIGHT = "#F7FDFC"
|
||||||
|
TEAL_PRESSED = "#227773"
|
||||||
|
|
||||||
|
# Grays
|
||||||
|
DARK_GRAY = "#616161"
|
||||||
|
GRAY = "#808080"
|
||||||
|
MEDIUM_LIGHT_GRAY = "#BDBDBD"
|
||||||
|
MEDIUM_DARK_GRAY = "#D2D2D2"
|
||||||
|
LIGHT_GRAY = "#F2F2F2"
|
||||||
|
|
||||||
|
# Accents
|
||||||
|
WHITE = "#FFFFFF"
|
||||||
|
BLACK = "#000000"
|
||||||
|
DARK_RED = "#b50d0d" # For negative values
|
||||||
|
```
|
||||||
|
|
||||||
|
**See current colors:**
|
||||||
|
```bash
|
||||||
|
cat policyengine-app/src/style/colors.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Plotly Chart Formatting
|
||||||
|
|
||||||
|
**Standard PolicyEngine chart:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
def format_fig(fig):
|
||||||
|
"""Format chart with PolicyEngine branding."""
|
||||||
|
fig.update_layout(
|
||||||
|
# Typography
|
||||||
|
font=dict(
|
||||||
|
family="Roboto Serif",
|
||||||
|
color="black"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Background
|
||||||
|
plot_bgcolor="white",
|
||||||
|
template="plotly_white",
|
||||||
|
|
||||||
|
# Margins (leave room for logo)
|
||||||
|
margin=dict(
|
||||||
|
l=50,
|
||||||
|
r=100,
|
||||||
|
t=50,
|
||||||
|
b=120,
|
||||||
|
pad=4
|
||||||
|
),
|
||||||
|
|
||||||
|
# Chart size
|
||||||
|
height=600,
|
||||||
|
width=800,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add PolicyEngine logo (bottom right)
|
||||||
|
fig.add_layout_image(
|
||||||
|
dict(
|
||||||
|
source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png",
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
x=1.0,
|
||||||
|
y=-0.10,
|
||||||
|
sizex=0.10,
|
||||||
|
sizey=0.10,
|
||||||
|
xanchor="right",
|
||||||
|
yanchor="bottom"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean modebar
|
||||||
|
fig.update_layout(
|
||||||
|
modebar=dict(
|
||||||
|
bgcolor="rgba(0,0,0,0)",
|
||||||
|
color="rgba(0,0,0,0)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.add_trace(go.Scatter(x=x_data, y=y_data, line=dict(color=TEAL_ACCENT)))
|
||||||
|
fig = format_fig(fig)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current implementation:**
|
||||||
|
```bash
|
||||||
|
# See format_fig in action
|
||||||
|
cat givecalc/ui/visualization.py
|
||||||
|
cat policyengine-app/src/pages/policy/output/...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Chart Colors
|
||||||
|
|
||||||
|
**For line charts:**
|
||||||
|
- Primary line: Teal (#39C6C0) or Blue (#2C6496)
|
||||||
|
- Background lines: Light gray (rgb(180, 180, 180))
|
||||||
|
- Markers: Teal with 70% opacity
|
||||||
|
|
||||||
|
**For bar charts:**
|
||||||
|
- Positive values: Teal (#39C6C0)
|
||||||
|
- Negative values: Dark red (#b50d0d)
|
||||||
|
- Neutral: Gray
|
||||||
|
|
||||||
|
**For multiple series:**
|
||||||
|
Use variations of blue and teal, or discrete color scale:
|
||||||
|
```python
|
||||||
|
colors = ["#2C6496", "#39C6C0", "#17354F", "#227773"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Typography
|
||||||
|
|
||||||
|
**Charts:**
|
||||||
|
```python
|
||||||
|
font=dict(family="Roboto Serif", size=14, color="black")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Axis labels:**
|
||||||
|
```python
|
||||||
|
xaxis=dict(
|
||||||
|
title=dict(text="Label", font=dict(size=14)),
|
||||||
|
tickfont=dict(size=12)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load Roboto font:**
|
||||||
|
```python
|
||||||
|
# In Streamlit apps
|
||||||
|
st.markdown("""
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streamlit App Branding
|
||||||
|
|
||||||
|
**Streamlit configuration (.streamlit/config.toml):**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[theme]
|
||||||
|
base = "light"
|
||||||
|
primaryColor = "#39C6C0" # Teal accent
|
||||||
|
backgroundColor = "#FFFFFF" # White background
|
||||||
|
secondaryBackgroundColor = "#F7FDFC" # Teal light
|
||||||
|
textColor = "#616161" # Dark gray
|
||||||
|
|
||||||
|
[client]
|
||||||
|
toolbarMode = "minimal"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current implementation:**
|
||||||
|
```bash
|
||||||
|
cat givecalc/.streamlit/config.toml
|
||||||
|
cat salt-amt-calculator/.streamlit/config.toml # Other calculators
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logo Usage
|
||||||
|
|
||||||
|
**Logo URLs:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Blue logo (for light backgrounds)
|
||||||
|
LOGO_BLUE = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png"
|
||||||
|
|
||||||
|
# White logo (for dark backgrounds)
|
||||||
|
LOGO_WHITE = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.png"
|
||||||
|
|
||||||
|
# SVG versions (scalable)
|
||||||
|
LOGO_BLUE_SVG = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.svg"
|
||||||
|
LOGO_WHITE_SVG = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.svg"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logo placement in charts:**
|
||||||
|
- Bottom right corner
|
||||||
|
- 10% of chart width
|
||||||
|
- Slightly below bottom edge (y=-0.10)
|
||||||
|
|
||||||
|
**Current logos:**
|
||||||
|
```bash
|
||||||
|
ls policyengine-app/src/images/logos/policyengine/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Example: Branded Chart
|
||||||
|
|
||||||
|
```python
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
# PolicyEngine colors
|
||||||
|
TEAL_ACCENT = "#39C6C0"
|
||||||
|
BLUE_PRIMARY = "#2C6496"
|
||||||
|
|
||||||
|
# Create chart
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Add data
|
||||||
|
fig.add_trace(go.Scatter(
|
||||||
|
x=incomes,
|
||||||
|
y=taxes,
|
||||||
|
mode='lines',
|
||||||
|
name='Tax liability',
|
||||||
|
line=dict(color=TEAL_ACCENT, width=3)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Apply PolicyEngine branding
|
||||||
|
fig.update_layout(
|
||||||
|
# Typography
|
||||||
|
font=dict(family="Roboto Serif", size=14, color="black"),
|
||||||
|
|
||||||
|
# Title and labels
|
||||||
|
title="Tax liability by income",
|
||||||
|
xaxis_title="Income",
|
||||||
|
yaxis_title="Tax ($)",
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
xaxis_tickformat="$,.0f",
|
||||||
|
yaxis_tickformat="$,.0f",
|
||||||
|
|
||||||
|
# Appearance
|
||||||
|
plot_bgcolor="white",
|
||||||
|
template="plotly_white",
|
||||||
|
|
||||||
|
# Size and margins
|
||||||
|
height=600,
|
||||||
|
width=800,
|
||||||
|
margin=dict(l=50, r=100, t=50, b=120, pad=4)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add logo
|
||||||
|
fig.add_layout_image(
|
||||||
|
dict(
|
||||||
|
source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png",
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
x=1.0,
|
||||||
|
y=-0.10,
|
||||||
|
sizex=0.10,
|
||||||
|
sizey=0.10,
|
||||||
|
xanchor="right",
|
||||||
|
yanchor="bottom"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show
|
||||||
|
fig.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
## For Contributors 💻
|
||||||
|
|
||||||
|
### Brand Assets
|
||||||
|
|
||||||
|
**Repository:** PolicyEngine/policyengine-app-v2 (current), PolicyEngine/policyengine-app (legacy)
|
||||||
|
|
||||||
|
**Logo files:**
|
||||||
|
```bash
|
||||||
|
# Logos in app (both v1 and v2 use same logos)
|
||||||
|
ls policyengine-app/src/images/logos/policyengine/
|
||||||
|
# - blue.png - For light backgrounds
|
||||||
|
# - white.png - For dark backgrounds
|
||||||
|
# - blue.svg - Scalable blue logo
|
||||||
|
# - white.svg - Scalable white logo
|
||||||
|
# - banners/ - Banner variations
|
||||||
|
# - profile/ - Profile/avatar versions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access logos:**
|
||||||
|
```bash
|
||||||
|
# View logo files (v1 repo has the assets)
|
||||||
|
cd policyengine-app/src/images/logos/policyengine/
|
||||||
|
ls -la
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Definitions
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT: App V2 Transition**
|
||||||
|
|
||||||
|
PolicyEngine is transitioning to policyengine-app-v2 with updated design tokens. Use app-v2 colors for new projects.
|
||||||
|
|
||||||
|
**Current colors (policyengine-app-v2):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// policyengine-app-v2/app/src/designTokens/colors.ts
|
||||||
|
|
||||||
|
// Primary (teal) - 50 to 900 scale
|
||||||
|
primary[500]: "#319795" // Main teal
|
||||||
|
primary[400]: "#38B2AC" // Lighter teal
|
||||||
|
primary[600]: "#2C7A7B" // Darker teal
|
||||||
|
|
||||||
|
// Blue scale
|
||||||
|
blue[700]: "#026AA2" // Primary blue
|
||||||
|
blue[500]: "#0EA5E9" // Lighter blue
|
||||||
|
|
||||||
|
// Gray scale
|
||||||
|
gray[700]: "#344054" // Dark text
|
||||||
|
gray[100]: "#F2F4F7" // Light backgrounds
|
||||||
|
|
||||||
|
// Semantic
|
||||||
|
success: "#22C55E"
|
||||||
|
warning: "#FEC601"
|
||||||
|
error: "#EF4444"
|
||||||
|
|
||||||
|
// Background
|
||||||
|
background.primary: "#FFFFFF"
|
||||||
|
background.secondary: "#F5F9FF"
|
||||||
|
|
||||||
|
// Text
|
||||||
|
text.primary: "#000000"
|
||||||
|
text.secondary: "#5A5A5A"
|
||||||
|
```
|
||||||
|
|
||||||
|
**To see current design tokens:**
|
||||||
|
```bash
|
||||||
|
cat policyengine-app-v2/app/src/designTokens/colors.ts
|
||||||
|
cat policyengine-app-v2/app/src/styles/colors.ts # Mantine integration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Legacy colors (policyengine-app - still used in some projects):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// policyengine-app/src/style/colors.js
|
||||||
|
TEAL_ACCENT = "#39C6C0" // Old teal (slightly different from v2)
|
||||||
|
BLUE = "#2C6496" // Old blue
|
||||||
|
DARK_GRAY = "#616161" // Old dark gray
|
||||||
|
```
|
||||||
|
|
||||||
|
**To see legacy colors:**
|
||||||
|
```bash
|
||||||
|
cat policyengine-app/src/style/colors.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in React (app-v2):**
|
||||||
|
```typescript
|
||||||
|
import { colors } from 'designTokens';
|
||||||
|
|
||||||
|
<Button style={{ backgroundColor: colors.primary[500] }} />
|
||||||
|
<Text style={{ color: colors.text.primary }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in Python/Plotly (use legacy colors for now):**
|
||||||
|
```python
|
||||||
|
# For charts, continue using legacy colors until officially migrated
|
||||||
|
TEAL_ACCENT = "#39C6C0" # From original app
|
||||||
|
BLUE_PRIMARY = "#2C6496" # From original app
|
||||||
|
|
||||||
|
# Or use app-v2 colors
|
||||||
|
TEAL_PRIMARY = "#319795" # From app-v2
|
||||||
|
BLUE_PRIMARY_V2 = "#026AA2" # From app-v2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
**Fonts:**
|
||||||
|
|
||||||
|
**For charts (Plotly):**
|
||||||
|
```python
|
||||||
|
font=dict(family="Roboto Serif")
|
||||||
|
```
|
||||||
|
|
||||||
|
**For web apps:**
|
||||||
|
```javascript
|
||||||
|
// System font stack
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Loading Google Fonts:**
|
||||||
|
```html
|
||||||
|
<!-- In Streamlit or HTML -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Serif:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chart Formatting Function
|
||||||
|
|
||||||
|
**Reference implementation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GiveCalc format_fig function
|
||||||
|
cat givecalc/ui/visualization.py
|
||||||
|
|
||||||
|
# Shows:
|
||||||
|
# - Roboto Serif font
|
||||||
|
# - White background
|
||||||
|
# - Logo placement
|
||||||
|
# - Margin configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern to follow:**
|
||||||
|
```python
|
||||||
|
def format_fig(fig: go.Figure) -> go.Figure:
|
||||||
|
"""Format figure with PolicyEngine branding.
|
||||||
|
|
||||||
|
This function is used across PolicyEngine projects to ensure
|
||||||
|
consistent chart appearance.
|
||||||
|
"""
|
||||||
|
# Font
|
||||||
|
fig.update_layout(
|
||||||
|
font=dict(family="Roboto Serif", color="black")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
fig.update_layout(
|
||||||
|
template="plotly_white",
|
||||||
|
plot_bgcolor="white"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Size
|
||||||
|
fig.update_layout(height=600, width=800)
|
||||||
|
|
||||||
|
# Margins (room for logo)
|
||||||
|
fig.update_layout(
|
||||||
|
margin=dict(l=50, r=100, t=50, b=120, pad=4)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Logo
|
||||||
|
fig.add_layout_image(
|
||||||
|
dict(
|
||||||
|
source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png",
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
x=1.0,
|
||||||
|
y=-0.10,
|
||||||
|
sizex=0.10,
|
||||||
|
sizey=0.10,
|
||||||
|
xanchor="right",
|
||||||
|
yanchor="bottom"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean modebar
|
||||||
|
fig.update_layout(
|
||||||
|
modebar=dict(
|
||||||
|
bgcolor="rgba(0,0,0,0)",
|
||||||
|
color="rgba(0,0,0,0)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streamlit Theme Configuration
|
||||||
|
|
||||||
|
**Standard .streamlit/config.toml:**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[theme]
|
||||||
|
base = "light"
|
||||||
|
primaryColor = "#39C6C0" # Teal accent
|
||||||
|
backgroundColor = "#FFFFFF" # White
|
||||||
|
secondaryBackgroundColor = "#F7FDFC" # Teal light
|
||||||
|
textColor = "#616161" # Dark gray
|
||||||
|
font = "sans serif"
|
||||||
|
|
||||||
|
[client]
|
||||||
|
toolbarMode = "minimal"
|
||||||
|
showErrorDetails = true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Create .streamlit directory in your project
|
||||||
|
mkdir .streamlit
|
||||||
|
|
||||||
|
# Copy configuration
|
||||||
|
cat > .streamlit/config.toml << 'EOF'
|
||||||
|
[theme]
|
||||||
|
base = "light"
|
||||||
|
primaryColor = "#39C6C0"
|
||||||
|
backgroundColor = "#FFFFFF"
|
||||||
|
secondaryBackgroundColor = "#F7FDFC"
|
||||||
|
textColor = "#616161"
|
||||||
|
font = "sans serif"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current examples:**
|
||||||
|
```bash
|
||||||
|
cat givecalc/.streamlit/config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Patterns by Project Type
|
||||||
|
|
||||||
|
### Streamlit Calculators (GiveCalc, SALT Calculator, etc.)
|
||||||
|
|
||||||
|
**Required branding:**
|
||||||
|
1. ✅ .streamlit/config.toml with PolicyEngine theme
|
||||||
|
2. ✅ Charts use format_fig() function with logo
|
||||||
|
3. ✅ Teal accent color for interactive elements
|
||||||
|
4. ✅ Roboto Serif for charts
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
import streamlit as st
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
TEAL_ACCENT = "#39C6C0"
|
||||||
|
|
||||||
|
# Streamlit UI
|
||||||
|
st.title("Calculator Name") # Uses theme colors automatically
|
||||||
|
st.button("Calculate", type="primary") # Teal accent from theme
|
||||||
|
|
||||||
|
# Charts
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.add_trace(go.Scatter(x=x, y=y, line=dict(color=TEAL_ACCENT)))
|
||||||
|
fig = format_fig(fig) # Add branding
|
||||||
|
st.plotly_chart(fig)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jupyter Notebooks / Analysis Scripts
|
||||||
|
|
||||||
|
**Required branding:**
|
||||||
|
1. ✅ Charts use format_fig() with logo
|
||||||
|
2. ✅ PolicyEngine color palette
|
||||||
|
3. ✅ Roboto Serif font
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
TEAL_ACCENT = "#39C6C0"
|
||||||
|
BLUE_PRIMARY = "#2C6496"
|
||||||
|
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.add_trace(go.Scatter(
|
||||||
|
x=data.income,
|
||||||
|
y=data.tax_change,
|
||||||
|
line=dict(color=TEAL_ACCENT, width=3)
|
||||||
|
))
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
font=dict(family="Roboto Serif", size=14),
|
||||||
|
title="Tax impact by income",
|
||||||
|
xaxis_title="Income",
|
||||||
|
yaxis_title="Tax change ($)",
|
||||||
|
plot_bgcolor="white"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add logo
|
||||||
|
fig.add_layout_image(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### React App Components
|
||||||
|
|
||||||
|
**Color usage:**
|
||||||
|
```javascript
|
||||||
|
import colors from "style/colors";
|
||||||
|
|
||||||
|
// Interactive elements
|
||||||
|
<Button style={{ backgroundColor: colors.TEAL_ACCENT }}>
|
||||||
|
Click me
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
// Links
|
||||||
|
<a style={{ color: colors.BLUE }}>Learn more</a>
|
||||||
|
|
||||||
|
// Text
|
||||||
|
<p style={{ color: colors.DARK_GRAY }}>Description</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current colors:**
|
||||||
|
```bash
|
||||||
|
cat policyengine-app/src/style/colors.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Visual Guidelines
|
||||||
|
|
||||||
|
### Chart Design Principles
|
||||||
|
|
||||||
|
1. **Minimal decoration** - Let data speak
|
||||||
|
2. **White backgrounds** - Clean, print-friendly
|
||||||
|
3. **Clear axis labels** - Always include units
|
||||||
|
4. **Formatted numbers** - Currency ($), percentages (%), etc.
|
||||||
|
5. **Logo inclusion** - Bottom right, never intrusive
|
||||||
|
6. **Consistent sizing** - 800x600 standard
|
||||||
|
7. **Roboto Serif** - Professional, readable font
|
||||||
|
|
||||||
|
### Color Usage Rules
|
||||||
|
|
||||||
|
**Primary actions:**
|
||||||
|
- Use TEAL_ACCENT (#39C6C0)
|
||||||
|
- Buttons, highlights, current selection
|
||||||
|
|
||||||
|
**Chart lines:**
|
||||||
|
- Primary data: TEAL_ACCENT or BLUE_PRIMARY
|
||||||
|
- Secondary data: BLUE_LIGHT or GRAY
|
||||||
|
- Negative values: DARK_RED (#b50d0d)
|
||||||
|
|
||||||
|
**Backgrounds:**
|
||||||
|
- Main: WHITE (#FFFFFF)
|
||||||
|
- Secondary: TEAL_LIGHT (#F7FDFC) or BLUE_98 (#F7FAFD)
|
||||||
|
- Plot area: WHITE
|
||||||
|
|
||||||
|
**Text:**
|
||||||
|
- Primary: BLACK (#000000)
|
||||||
|
- Secondary: DARK_GRAY (#616161)
|
||||||
|
- Muted: GRAY (#808080)
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
**Color contrast requirements:**
|
||||||
|
- Text on background: 4.5:1 minimum (WCAG AA)
|
||||||
|
- DARK_GRAY on WHITE: ✅ Passes
|
||||||
|
- TEAL_ACCENT on WHITE: ✅ Passes for large text
|
||||||
|
- Use sufficient line weights for visibility
|
||||||
|
|
||||||
|
**Don't rely on color alone:**
|
||||||
|
- Use patterns or labels for different data series
|
||||||
|
- Ensure charts work in grayscale
|
||||||
|
|
||||||
|
## Common Branding Tasks
|
||||||
|
|
||||||
|
### Task 1: Create Branded Plotly Chart
|
||||||
|
|
||||||
|
1. **Define colors:**
|
||||||
|
```python
|
||||||
|
TEAL_ACCENT = "#39C6C0"
|
||||||
|
BLUE_PRIMARY = "#2C6496"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create chart:**
|
||||||
|
```python
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.add_trace(go.Scatter(x=x, y=y, line=dict(color=TEAL_ACCENT)))
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Apply branding:**
|
||||||
|
```python
|
||||||
|
fig = format_fig(fig) # See implementation above
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Setup Streamlit Branding
|
||||||
|
|
||||||
|
1. **Create config directory:**
|
||||||
|
```bash
|
||||||
|
mkdir .streamlit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Copy theme config:**
|
||||||
|
```bash
|
||||||
|
cat givecalc/.streamlit/config.toml > .streamlit/config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify in app:**
|
||||||
|
```python
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
st.button("Test", type="primary") # Should be teal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Brand Consistency Check
|
||||||
|
|
||||||
|
**Checklist:**
|
||||||
|
- [ ] Charts use Roboto Serif font
|
||||||
|
- [ ] Primary color is TEAL_ACCENT (#39C6C0)
|
||||||
|
- [ ] Secondary color is BLUE_PRIMARY (#2C6496)
|
||||||
|
- [ ] White backgrounds
|
||||||
|
- [ ] Logo in charts (bottom right)
|
||||||
|
- [ ] Currency formatted with $ and commas
|
||||||
|
- [ ] Percentages formatted with %
|
||||||
|
- [ ] Streamlit config.toml uses PolicyEngine theme
|
||||||
|
|
||||||
|
## Reference Implementations
|
||||||
|
|
||||||
|
### Excellent Examples
|
||||||
|
|
||||||
|
**Streamlit calculators:**
|
||||||
|
```bash
|
||||||
|
# GiveCalc - Complete example
|
||||||
|
cat givecalc/ui/visualization.py
|
||||||
|
cat givecalc/.streamlit/config.toml
|
||||||
|
|
||||||
|
# Other calculators
|
||||||
|
ls salt-amt-calculator/
|
||||||
|
ls ctc-calculator/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Blog post charts:**
|
||||||
|
```bash
|
||||||
|
# Analysis with branded charts
|
||||||
|
cat policyengine-app/src/posts/articles/harris-eitc.md
|
||||||
|
cat policyengine-app/src/posts/articles/montana-tax-cuts-2026.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**React app components:**
|
||||||
|
```bash
|
||||||
|
# Charts in app
|
||||||
|
cat policyengine-app/src/pages/policy/output/DistributionalImpact.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't Use These
|
||||||
|
|
||||||
|
**❌ Wrong colors:**
|
||||||
|
```python
|
||||||
|
# Don't use random colors
|
||||||
|
color = "#FF5733"
|
||||||
|
color = "red"
|
||||||
|
color = "green"
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong fonts:**
|
||||||
|
```python
|
||||||
|
# Don't use other fonts for charts
|
||||||
|
font = dict(family="Arial")
|
||||||
|
font = dict(family="Times New Roman")
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Missing logo:**
|
||||||
|
```python
|
||||||
|
# Don't skip the logo in charts for publication
|
||||||
|
# All published charts should include PolicyEngine logo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assets and Resources
|
||||||
|
|
||||||
|
### Logo Files
|
||||||
|
|
||||||
|
**In policyengine-app repository:**
|
||||||
|
```bash
|
||||||
|
policyengine-app/src/images/logos/policyengine/
|
||||||
|
├── blue.png # Primary logo (light backgrounds)
|
||||||
|
├── white.png # Logo for dark backgrounds
|
||||||
|
├── blue.svg # Scalable blue logo
|
||||||
|
├── white.svg # Scalable white logo
|
||||||
|
├── banners/ # Banner variations
|
||||||
|
└── profile/ # Profile/avatar versions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Raw URLs for direct use:**
|
||||||
|
```python
|
||||||
|
# Use these URLs in code
|
||||||
|
LOGO_URL = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Font Files
|
||||||
|
|
||||||
|
**Roboto (charts):**
|
||||||
|
- Google Fonts: https://fonts.google.com/specimen/Roboto
|
||||||
|
- Family: Roboto Serif
|
||||||
|
- Weights: 300 (light), 400 (regular), 500 (medium), 700 (bold)
|
||||||
|
|
||||||
|
**Loading:**
|
||||||
|
```html
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto+Serif:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Reference Files
|
||||||
|
|
||||||
|
**JavaScript (React app):**
|
||||||
|
```bash
|
||||||
|
cat policyengine-app/src/style/colors.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python (calculators, analysis):**
|
||||||
|
```python
|
||||||
|
# Define in constants.py or at top of file
|
||||||
|
TEAL_ACCENT = "#39C6C0"
|
||||||
|
BLUE_PRIMARY = "#2C6496"
|
||||||
|
DARK_GRAY = "#616161"
|
||||||
|
WHITE = "#FFFFFF"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Brand Evolution
|
||||||
|
|
||||||
|
**Current identity (2025):**
|
||||||
|
- Teal primary (#39C6C0)
|
||||||
|
- Blue secondary (#2C6496)
|
||||||
|
- Roboto Serif for charts
|
||||||
|
- Minimal, data-focused design
|
||||||
|
|
||||||
|
**If brand evolves:**
|
||||||
|
- Colors defined in policyengine-app/src/style/colors.js are source of truth
|
||||||
|
- Update this skill to point to current definitions
|
||||||
|
- Never hardcode - always reference colors.js
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Color Codes
|
||||||
|
|
||||||
|
| Color | Hex | Usage |
|
||||||
|
|-------|-----|-------|
|
||||||
|
| Teal Accent | #39C6C0 | Primary interactive elements |
|
||||||
|
| Blue Primary | #2C6496 | Secondary, links, charts |
|
||||||
|
| Dark Gray | #616161 | Body text |
|
||||||
|
| White | #FFFFFF | Backgrounds |
|
||||||
|
| Teal Light | #F7FDFC | Secondary backgrounds |
|
||||||
|
| Dark Red | #b50d0d | Negative values, errors |
|
||||||
|
|
||||||
|
### Font Families
|
||||||
|
|
||||||
|
| Context | Font |
|
||||||
|
|---------|------|
|
||||||
|
| Charts | Roboto Serif |
|
||||||
|
| Web app | System sans-serif |
|
||||||
|
| Streamlit | Default sans-serif |
|
||||||
|
| Code blocks | Monospace |
|
||||||
|
|
||||||
|
### Logo URLs
|
||||||
|
|
||||||
|
| Background | Format | URL |
|
||||||
|
|------------|--------|-----|
|
||||||
|
| Light | PNG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png |
|
||||||
|
| Light | SVG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.svg |
|
||||||
|
| Dark | PNG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.png |
|
||||||
|
| Dark | SVG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.svg |
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **policyengine-app-skill** - React component styling
|
||||||
|
- **policyengine-analysis-skill** - Chart creation patterns
|
||||||
|
- **policyengine-writing-skill** - Content style (complements visual style)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
**Brand assets:** PolicyEngine/policyengine-app/src/images/
|
||||||
|
**Color definitions:** PolicyEngine/policyengine-app/src/style/colors.js
|
||||||
|
**Examples:** givecalc, salt-amt-calculator, crfb-tob-impacts
|
||||||
768
skills/policyengine-standards-skill/SKILL.md
Normal file
768
skills/policyengine-standards-skill/SKILL.md
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
---
|
||||||
|
name: policyengine-standards
|
||||||
|
description: PolicyEngine coding standards, formatters, CI requirements, and development best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# PolicyEngine Standards Skill
|
||||||
|
|
||||||
|
Use this skill to ensure code meets PolicyEngine's development standards and passes CI checks.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Before committing code to any PolicyEngine repository
|
||||||
|
- When CI checks fail with linting/formatting errors
|
||||||
|
- Setting up a new PolicyEngine repository
|
||||||
|
- Reviewing PRs for standard compliance
|
||||||
|
- When AI tools generate code that needs standardization
|
||||||
|
|
||||||
|
## Critical Requirements
|
||||||
|
|
||||||
|
### Python Version
|
||||||
|
⚠️ **MUST USE Python 3.13** - Do NOT downgrade to older versions
|
||||||
|
- Check version: `python --version`
|
||||||
|
- Use `pyproject.toml` to specify version requirements
|
||||||
|
|
||||||
|
### Command Execution
|
||||||
|
⚠️ **ALWAYS use `uv run` for Python commands** - Never use bare `python` or `pytest`
|
||||||
|
- ✅ Correct: `uv run python script.py`, `uv run pytest tests/`
|
||||||
|
- ❌ Wrong: `python script.py`, `pytest tests/`
|
||||||
|
- This ensures correct virtual environment and dependencies
|
||||||
|
|
||||||
|
### Documentation (Python Projects)
|
||||||
|
⚠️ **MUST USE Jupyter Book 2.0 (MyST-NB)** - NOT Jupyter Book 1.x
|
||||||
|
- Build docs: `myst build docs` (NOT `jb build`)
|
||||||
|
- Use MyST markdown syntax
|
||||||
|
|
||||||
|
## Before Committing - Checklist
|
||||||
|
|
||||||
|
1. **Write tests first** (TDD - see below)
|
||||||
|
2. **Format code**: `make format` or language-specific formatter
|
||||||
|
3. **Run tests**: `make test` to ensure all tests pass
|
||||||
|
4. **Check linting**: Ensure no linting errors
|
||||||
|
5. **Use config files**: Prefer config files over environment variables
|
||||||
|
6. **Reference issues**: Include "Fixes #123" in commit message
|
||||||
|
|
||||||
|
## Creating Pull Requests
|
||||||
|
|
||||||
|
### The CI Waiting Problem
|
||||||
|
|
||||||
|
**Common failure pattern:**
|
||||||
|
```
|
||||||
|
User: "Create a PR and mark it ready when CI passes"
|
||||||
|
Claude: "I've created the PR as draft. CI will take a while, I'll check back later..."
|
||||||
|
[Chat ends - Claude never checks back]
|
||||||
|
Result: PR stays in draft, user has to manually check CI and mark ready
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution: Use /create-pr Command
|
||||||
|
|
||||||
|
**When creating PRs, use the /create-pr command:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/create-pr
|
||||||
|
```
|
||||||
|
|
||||||
|
**This command:**
|
||||||
|
- ✅ Creates PR as draft
|
||||||
|
- ✅ Actually waits for CI (polls every 15 seconds)
|
||||||
|
- ✅ Marks ready when CI passes
|
||||||
|
- ✅ Reports failures with details
|
||||||
|
- ✅ Handles timeouts gracefully
|
||||||
|
|
||||||
|
**Why this works:**
|
||||||
|
The command contains explicit polling logic that Claude executes, so it actually waits instead of giving up.
|
||||||
|
|
||||||
|
### If /create-pr is Not Available
|
||||||
|
|
||||||
|
**If the command isn't installed, implement the pattern directly:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create PR as draft
|
||||||
|
gh pr create --draft --title "Title" --body "Body"
|
||||||
|
PR_NUMBER=$(gh pr view --json number --jq '.number')
|
||||||
|
|
||||||
|
# 2. Wait for CI (ACTUALLY WAIT - don't give up!)
|
||||||
|
POLL_INTERVAL=15
|
||||||
|
ELAPSED=0
|
||||||
|
|
||||||
|
while true; do # No timeout - wait as long as needed
|
||||||
|
CHECKS=$(gh pr checks $PR_NUMBER --json status,conclusion)
|
||||||
|
TOTAL=$(echo "$CHECKS" | jq '. | length')
|
||||||
|
COMPLETED=$(echo "$CHECKS" | jq '[.[] | select(.status == "COMPLETED")] | length')
|
||||||
|
|
||||||
|
echo "[$ELAPSED s] CI: $COMPLETED/$TOTAL completed"
|
||||||
|
|
||||||
|
if [ "$COMPLETED" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
|
||||||
|
FAILED=$(echo "$CHECKS" | jq '[.[] | select(.conclusion == "FAILURE")] | length')
|
||||||
|
if [ "$FAILED" -eq 0 ]; then
|
||||||
|
echo "✅ All CI passed! Marking ready..."
|
||||||
|
gh pr ready $PR_NUMBER
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "❌ CI failed. PR remains draft."
|
||||||
|
gh pr checks $PR_NUMBER
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep $POLL_INTERVAL
|
||||||
|
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Important: No timeout! Population simulations can take 30+ minutes.
|
||||||
|
```
|
||||||
|
|
||||||
|
### DO NOT Say "I'll Check Back Later"
|
||||||
|
|
||||||
|
**❌ WRONG:**
|
||||||
|
```
|
||||||
|
"I've created the PR as draft. CI checks will take a few minutes.
|
||||||
|
I'll check back later once they complete."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why wrong:** You cannot check back later. The chat session ends.
|
||||||
|
|
||||||
|
**✅ CORRECT:**
|
||||||
|
```
|
||||||
|
"I've created the PR as draft. Now polling CI status every 15 seconds..."
|
||||||
|
[Actually polls using while loop]
|
||||||
|
"CI checks completed. All passed! Marking PR as ready for review."
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Create Draft vs Ready
|
||||||
|
|
||||||
|
**Always create as draft when:**
|
||||||
|
- CI checks are configured
|
||||||
|
- User asks to wait for CI
|
||||||
|
- Making automated changes
|
||||||
|
- Unsure if CI will pass
|
||||||
|
|
||||||
|
**Create as ready only when:**
|
||||||
|
- User explicitly requests ready PR
|
||||||
|
- No CI configured
|
||||||
|
- CI already verified locally
|
||||||
|
|
||||||
|
### PR Workflow Standards
|
||||||
|
|
||||||
|
**Standard flow:**
|
||||||
|
```bash
|
||||||
|
# 1. Ensure branch is pushed
|
||||||
|
git push -u origin feature-branch
|
||||||
|
|
||||||
|
# 2. Create PR as draft
|
||||||
|
gh pr create --draft --title "..." --body "..."
|
||||||
|
|
||||||
|
# 3. Wait for CI (use polling loop - see pattern above)
|
||||||
|
|
||||||
|
# 4. If CI passes:
|
||||||
|
gh pr ready $PR_NUMBER
|
||||||
|
|
||||||
|
# 5. If CI fails:
|
||||||
|
echo "CI failed. PR remains draft. Fix issues and push again."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test-Driven Development (TDD)
|
||||||
|
|
||||||
|
PolicyEngine follows Test-Driven Development practices across all repositories.
|
||||||
|
|
||||||
|
### TDD Workflow
|
||||||
|
|
||||||
|
**1. Write test first (RED):**
|
||||||
|
```python
|
||||||
|
# tests/test_new_feature.py
|
||||||
|
def test_california_eitc_calculation():
|
||||||
|
"""Test California EITC for family with 2 children earning $30,000."""
|
||||||
|
situation = create_family(income=30000, num_children=2, state="CA")
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
ca_eitc = sim.calculate("ca_eitc", 2024)[0]
|
||||||
|
|
||||||
|
# Test fails initially (feature not implemented yet)
|
||||||
|
assert ca_eitc == 3000, "CA EITC should be $3,000 for this household"
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Implement feature (GREEN):**
|
||||||
|
```python
|
||||||
|
# policyengine_us/variables/gov/states/ca/tax/income/credits/ca_eitc.py
|
||||||
|
class ca_eitc(Variable):
|
||||||
|
value_type = float
|
||||||
|
entity = TaxUnit
|
||||||
|
definition_period = YEAR
|
||||||
|
|
||||||
|
def formula(tax_unit, period, parameters):
|
||||||
|
# Implementation to make test pass
|
||||||
|
federal_eitc = tax_unit("eitc", period)
|
||||||
|
return federal_eitc * parameters(period).gov.states.ca.tax.eitc.match
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Refactor (REFACTOR):**
|
||||||
|
```python
|
||||||
|
# Clean up, optimize, add documentation
|
||||||
|
# All while tests continue to pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### TDD Benefits
|
||||||
|
|
||||||
|
**Why PolicyEngine uses TDD:**
|
||||||
|
- ✅ **Accuracy** - Tests verify implementation matches regulations
|
||||||
|
- ✅ **Documentation** - Tests show expected behavior
|
||||||
|
- ✅ **Regression prevention** - Changes don't break existing features
|
||||||
|
- ✅ **Confidence** - Safe to refactor
|
||||||
|
- ✅ **Isolation** - Multi-agent workflow (test-creator and rules-engineer work separately)
|
||||||
|
|
||||||
|
### TDD in Multi-Agent Workflow
|
||||||
|
|
||||||
|
**Country model development:**
|
||||||
|
1. **@document-collector** gathers regulations
|
||||||
|
2. **@test-creator** writes tests from regulations (isolated, no implementation access)
|
||||||
|
3. **@rules-engineer** implements from regulations (isolated, no test access)
|
||||||
|
4. Both work from same source → tests verify implementation accuracy
|
||||||
|
|
||||||
|
**See policyengine-core-skill and country-models agents for details.**
|
||||||
|
|
||||||
|
### Test Examples
|
||||||
|
|
||||||
|
**Python (pytest):**
|
||||||
|
```python
|
||||||
|
def test_ctc_for_two_children():
|
||||||
|
"""Test CTC calculation for married couple with 2 children."""
|
||||||
|
situation = create_married_couple(
|
||||||
|
income_1=75000,
|
||||||
|
income_2=50000,
|
||||||
|
num_children=2,
|
||||||
|
child_ages=[5, 8]
|
||||||
|
)
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
ctc = sim.calculate("ctc", 2024)[0]
|
||||||
|
|
||||||
|
assert ctc == 4000, "CTC should be $2,000 per child"
|
||||||
|
```
|
||||||
|
|
||||||
|
**React (Jest + RTL):**
|
||||||
|
```javascript
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import TaxCalculator from './TaxCalculator';
|
||||||
|
|
||||||
|
test('displays calculated tax', () => {
|
||||||
|
render(<TaxCalculator income={50000} />);
|
||||||
|
|
||||||
|
// Test what user sees, not implementation
|
||||||
|
expect(screen.getByText(/\$5,000/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── test_variables/
|
||||||
|
│ ├── test_income.py
|
||||||
|
│ ├── test_deductions.py
|
||||||
|
│ └── test_credits.py
|
||||||
|
├── test_parameters/
|
||||||
|
└── test_simulations/
|
||||||
|
```
|
||||||
|
|
||||||
|
**React:**
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ └── TaxCalculator/
|
||||||
|
│ ├── TaxCalculator.jsx
|
||||||
|
│ └── TaxCalculator.test.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# With uv
|
||||||
|
uv run pytest tests/ -v
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
uv run pytest tests/test_credits.py::test_ctc_for_two_children -v
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
uv run pytest tests/ --cov=policyengine_us --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
**React:**
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
npm test -- --watch
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
npm test -- TaxCalculator.test.jsx
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
npm test -- --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Quality Standards
|
||||||
|
|
||||||
|
**Good tests:**
|
||||||
|
- ✅ Test behavior, not implementation
|
||||||
|
- ✅ Clear, descriptive names
|
||||||
|
- ✅ Single assertion per test (when possible)
|
||||||
|
- ✅ Include documentation (docstrings)
|
||||||
|
- ✅ Based on official regulations with citations
|
||||||
|
|
||||||
|
**Bad tests:**
|
||||||
|
- ❌ Testing private methods
|
||||||
|
- ❌ Mocking everything
|
||||||
|
- ❌ No assertion messages
|
||||||
|
- ❌ Magic numbers without explanation
|
||||||
|
|
||||||
|
### Example: TDD for New Feature
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Step 1: Write test (RED)
|
||||||
|
def test_new_york_empire_state_child_credit():
|
||||||
|
"""Test NY Empire State Child Credit for family with 1 child.
|
||||||
|
|
||||||
|
Based on NY Tax Law Section 606(c-1).
|
||||||
|
Family earning $50,000 with 1 child under 4 should receive $330.
|
||||||
|
"""
|
||||||
|
situation = create_family(
|
||||||
|
income=50000,
|
||||||
|
num_children=1,
|
||||||
|
child_ages=[2],
|
||||||
|
state="NY"
|
||||||
|
)
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
credit = sim.calculate("ny_empire_state_child_credit", 2024)[0]
|
||||||
|
|
||||||
|
assert credit == 330, "Should receive $330 for child under 4"
|
||||||
|
|
||||||
|
# Test fails - feature doesn't exist yet
|
||||||
|
|
||||||
|
# Step 2: Implement (GREEN)
|
||||||
|
# Create variable in policyengine_us/variables/gov/states/ny/...
|
||||||
|
# Test passes
|
||||||
|
|
||||||
|
# Step 3: Refactor
|
||||||
|
# Optimize, add documentation, maintain passing tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python Standards
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
- **Formatter**: Black with 79-character line length
|
||||||
|
- **Command**: `make format` or `black . -l 79`
|
||||||
|
- **Check without changes**: `black . -l 79 --check`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all Python files
|
||||||
|
make format
|
||||||
|
|
||||||
|
# Check if formatting is needed (CI-style)
|
||||||
|
black . -l 79 --check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
```python
|
||||||
|
# Imports: Grouped and alphabetized
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path # stdlib
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd # third-party
|
||||||
|
|
||||||
|
from policyengine_us import Simulation # local
|
||||||
|
|
||||||
|
# Naming conventions
|
||||||
|
class TaxCalculator: # CamelCase for classes
|
||||||
|
pass
|
||||||
|
|
||||||
|
def calculate_income_tax(income): # snake_case for functions
|
||||||
|
annual_income = income * 12 # snake_case for variables
|
||||||
|
return annual_income
|
||||||
|
|
||||||
|
# Type hints (recommended)
|
||||||
|
def calculate_tax(income: float, state: str) -> float:
|
||||||
|
"""Calculate state income tax.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
income: Annual income in dollars
|
||||||
|
state: Two-letter state code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tax liability in dollars
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Error handling - catch specific exceptions
|
||||||
|
try:
|
||||||
|
result = simulation.calculate("income_tax", 2024)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f"Invalid variable name: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_ctc_calculation():
|
||||||
|
"""Test Child Tax Credit calculation for family with 2 children."""
|
||||||
|
situation = create_family(income=50000, num_children=2)
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
ctc = sim.calculate("ctc", 2024)[0]
|
||||||
|
|
||||||
|
assert ctc == 4000, "CTC should be $2000 per child"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run tests:**
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Or with uv
|
||||||
|
uv run pytest tests/ -v
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
uv run pytest tests/test_tax.py::test_ctc_calculation -v
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
uv run pytest tests/ --cov=policyengine_us --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript/React Standards
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
- **Formatters**: Prettier + ESLint
|
||||||
|
- **Command**: `npm run lint -- --fix && npx prettier --write .`
|
||||||
|
- **CI Check**: `npm run lint -- --max-warnings=0`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all files
|
||||||
|
make format
|
||||||
|
|
||||||
|
# Or manually
|
||||||
|
npm run lint -- --fix
|
||||||
|
npx prettier --write .
|
||||||
|
|
||||||
|
# Check if formatting is needed (CI-style)
|
||||||
|
npm run lint -- --max-warnings=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
```javascript
|
||||||
|
// Use functional components only (no class components)
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
function TaxCalculator({ income, state }) {
|
||||||
|
const [tax, setTax] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Calculate tax when inputs change
|
||||||
|
calculateTax(income, state).then(setTax);
|
||||||
|
}, [income, state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Tax: ${tax.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File naming
|
||||||
|
// - Components: PascalCase.jsx (TaxCalculator.jsx)
|
||||||
|
// - Utilities: camelCase.js (formatCurrency.js)
|
||||||
|
|
||||||
|
// Environment config - use config file pattern
|
||||||
|
// src/config/environment.js
|
||||||
|
const config = {
|
||||||
|
API_URL: process.env.NODE_ENV === 'production'
|
||||||
|
? 'https://api.policyengine.org'
|
||||||
|
: 'http://localhost:5000'
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Component Size
|
||||||
|
- Keep components under 150 lines after formatting
|
||||||
|
- Extract complex logic into custom hooks
|
||||||
|
- Split large components into smaller ones
|
||||||
|
|
||||||
|
## Version Control Standards
|
||||||
|
|
||||||
|
### Changelog Management
|
||||||
|
|
||||||
|
**CRITICAL**: For PRs, ONLY modify `changelog_entry.yaml`. NEVER manually update `CHANGELOG.md` or `changelog.yaml`.
|
||||||
|
|
||||||
|
**Correct Workflow:**
|
||||||
|
1. Create `changelog_entry.yaml` at repository root:
|
||||||
|
```yaml
|
||||||
|
- bump: patch # or minor, major
|
||||||
|
changes:
|
||||||
|
added:
|
||||||
|
- Description of new feature
|
||||||
|
fixed:
|
||||||
|
- Description of bug fix
|
||||||
|
changed:
|
||||||
|
- Description of change
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Commit ONLY `changelog_entry.yaml` with your code changes
|
||||||
|
|
||||||
|
3. GitHub Actions automatically updates `CHANGELOG.md` and `changelog.yaml` on merge
|
||||||
|
|
||||||
|
**DO NOT:**
|
||||||
|
- ❌ Run `make changelog` manually during PR creation
|
||||||
|
- ❌ Commit `CHANGELOG.md` or `changelog.yaml` in your PR
|
||||||
|
- ❌ Modify main changelog files directly
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
|
||||||
|
1. **Create branches on PolicyEngine repos, NOT forks**
|
||||||
|
- Forks cause CI failures due to missing secrets
|
||||||
|
- Request write access if needed
|
||||||
|
|
||||||
|
2. **Branch naming**: `feature-name` or `fix-issue-123`
|
||||||
|
|
||||||
|
3. **Commit messages**:
|
||||||
|
```
|
||||||
|
Add CTC reform analysis for CRFB report
|
||||||
|
|
||||||
|
- Implement household-level calculations
|
||||||
|
- Add state-by-state comparison
|
||||||
|
- Create visualizations
|
||||||
|
|
||||||
|
Fixes #123
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **PR description**: Include "Fixes #123" to auto-close issues
|
||||||
|
|
||||||
|
### Common Git Pitfalls
|
||||||
|
|
||||||
|
**Never do these:**
|
||||||
|
- ❌ Force push to main/master
|
||||||
|
- ❌ Commit secrets or `.env` files
|
||||||
|
- ❌ Skip hooks with `--no-verify`
|
||||||
|
- ❌ Create versioned files (app_v2.py, component_new.jsx)
|
||||||
|
|
||||||
|
**Always do:**
|
||||||
|
- ✅ Fix original files in place
|
||||||
|
- ✅ Run formatters before pushing
|
||||||
|
- ✅ Reference issue numbers in commits
|
||||||
|
- ✅ Watch CI after filing PR
|
||||||
|
|
||||||
|
## Common AI Pitfalls
|
||||||
|
|
||||||
|
Since many PRs are AI-generated, watch for these common mistakes:
|
||||||
|
|
||||||
|
### 1. File Versioning
|
||||||
|
**❌ Wrong:**
|
||||||
|
```bash
|
||||||
|
# Creating new versions instead of fixing originals
|
||||||
|
app_new.py
|
||||||
|
app_v2.py
|
||||||
|
component_refactored.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```bash
|
||||||
|
# Always modify the original file
|
||||||
|
app.py # Fixed in place
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Formatter Not Run
|
||||||
|
**❌ Wrong:** Committing without formatting (main cause of CI failures)
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```bash
|
||||||
|
# Python
|
||||||
|
make format
|
||||||
|
black . -l 79
|
||||||
|
|
||||||
|
# React
|
||||||
|
npm run lint -- --fix
|
||||||
|
npx prettier --write .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment Variables
|
||||||
|
**❌ Wrong:**
|
||||||
|
```javascript
|
||||||
|
// React env vars without REACT_APP_ prefix
|
||||||
|
const API_URL = process.env.API_URL; // Won't work!
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```javascript
|
||||||
|
// Use config file pattern instead
|
||||||
|
import config from './config/environment';
|
||||||
|
const API_URL = config.API_URL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Using Wrong Python Version
|
||||||
|
**❌ Wrong:** Downgrading to Python 3.10 or older
|
||||||
|
|
||||||
|
**✅ Correct:** Use Python 3.13 as specified in project requirements
|
||||||
|
|
||||||
|
### 5. Manual Changelog Updates
|
||||||
|
**❌ Wrong:** Running `make changelog` and committing `CHANGELOG.md`
|
||||||
|
|
||||||
|
**✅ Correct:** Only create `changelog_entry.yaml` in PR
|
||||||
|
|
||||||
|
## Repository Setup Patterns
|
||||||
|
|
||||||
|
### Python Package Structure
|
||||||
|
```
|
||||||
|
policyengine-package/
|
||||||
|
├── policyengine_package/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── core/
|
||||||
|
│ ├── calculations/
|
||||||
|
│ └── utils/
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_calculations.py
|
||||||
|
│ └── test_core.py
|
||||||
|
├── pyproject.toml
|
||||||
|
├── Makefile
|
||||||
|
├── CLAUDE.md
|
||||||
|
├── CHANGELOG.md
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### React App Structure
|
||||||
|
```
|
||||||
|
policyengine-app/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ ├── pages/
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── environment.js
|
||||||
|
│ └── App.jsx
|
||||||
|
├── public/
|
||||||
|
├── package.json
|
||||||
|
├── .eslintrc.json
|
||||||
|
├── .prettierrc
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Makefile Commands
|
||||||
|
|
||||||
|
Standard commands across PolicyEngine repos:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install # Install dependencies
|
||||||
|
make test # Run tests
|
||||||
|
make format # Format code
|
||||||
|
make changelog # Update changelog (automation only, not manual)
|
||||||
|
make debug # Start dev server (apps)
|
||||||
|
make build # Production build (apps)
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI Stability
|
||||||
|
|
||||||
|
### Common CI Issues
|
||||||
|
|
||||||
|
**1. Fork PRs Fail**
|
||||||
|
- **Problem**: PRs from forks don't have access to repository secrets
|
||||||
|
- **Solution**: Create branches directly on PolicyEngine repos
|
||||||
|
|
||||||
|
**2. GitHub API Rate Limits**
|
||||||
|
- **Problem**: Smoke tests fail with 403 errors
|
||||||
|
- **Solution**: Re-run failed jobs (different runners have different limits)
|
||||||
|
|
||||||
|
**3. Linting Failures**
|
||||||
|
- **Problem**: Code not formatted before commit
|
||||||
|
- **Solution**: Always run `make format` before committing
|
||||||
|
|
||||||
|
**4. Test Failures in CI but Pass Locally**
|
||||||
|
- **Problem**: Missing `uv run` prefix
|
||||||
|
- **Solution**: Use `uv run pytest` instead of `pytest`
|
||||||
|
|
||||||
|
## Best Practices Checklist
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Code formatted with Black (Python) or Prettier (JS)
|
||||||
|
- [ ] No linting errors
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Type hints added (Python, where applicable)
|
||||||
|
- [ ] Docstrings for public functions/classes
|
||||||
|
- [ ] Error handling with specific exceptions
|
||||||
|
|
||||||
|
### Version Control
|
||||||
|
- [ ] Only `changelog_entry.yaml` created (not CHANGELOG.md)
|
||||||
|
- [ ] Commit message references issue number
|
||||||
|
- [ ] Branch created on PolicyEngine repo (not fork)
|
||||||
|
- [ ] No secrets or .env files committed
|
||||||
|
- [ ] Original files modified (no _v2 or _new files)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Tests written for new functionality
|
||||||
|
- [ ] Tests pass locally with `make test`
|
||||||
|
- [ ] Coverage maintained or improved
|
||||||
|
- [ ] Edge cases handled
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] README updated if needed
|
||||||
|
- [ ] Code comments for complex logic
|
||||||
|
- [ ] API documentation updated if needed
|
||||||
|
- [ ] Examples provided for new features
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Format Commands by Language
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
```bash
|
||||||
|
make format # Format code
|
||||||
|
black . -l 79 --check # Check formatting
|
||||||
|
uv run pytest tests/ -v # Run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**React:**
|
||||||
|
```bash
|
||||||
|
make format # Format code
|
||||||
|
npm run lint -- --max-warnings=0 # Check linting
|
||||||
|
npm test # Run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-Commit Checklist
|
||||||
|
```bash
|
||||||
|
# 1. Format
|
||||||
|
make format
|
||||||
|
|
||||||
|
# 2. Test
|
||||||
|
make test
|
||||||
|
|
||||||
|
# 3. Check linting
|
||||||
|
# Python: black . -l 79 --check
|
||||||
|
# React: npm run lint -- --max-warnings=0
|
||||||
|
|
||||||
|
# 4. Stage and commit
|
||||||
|
git add .
|
||||||
|
git commit -m "Description
|
||||||
|
|
||||||
|
Fixes #123"
|
||||||
|
|
||||||
|
# 5. Push and watch CI
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Main CLAUDE.md**: `/PolicyEngine/CLAUDE.md`
|
||||||
|
- **Python Style**: PEP 8, Black documentation
|
||||||
|
- **React Style**: Airbnb React/JSX Style Guide
|
||||||
|
- **Testing**: pytest documentation, Jest/RTL documentation
|
||||||
|
- **Writing Style**: See policyengine-writing-skill for blog posts, PR descriptions, and documentation
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See PolicyEngine repositories for examples of standard-compliant code:
|
||||||
|
- **policyengine-us**: Python package standards
|
||||||
|
- **policyengine-app**: React app standards
|
||||||
|
- **givecalc**: Streamlit app standards
|
||||||
|
- **crfb-tob-impacts**: Analysis repository standards
|
||||||
660
skills/policyengine-uk-skill/SKILL.md
Normal file
660
skills/policyengine-uk-skill/SKILL.md
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
---
|
||||||
|
name: policyengine-uk
|
||||||
|
description: PolicyEngine-UK tax and benefit microsimulation patterns, situation creation, and common workflows
|
||||||
|
---
|
||||||
|
|
||||||
|
# PolicyEngine-UK
|
||||||
|
|
||||||
|
PolicyEngine-UK models the UK tax and benefit system, including devolved variations for Scotland and Wales.
|
||||||
|
|
||||||
|
## For Users 👥
|
||||||
|
|
||||||
|
### What is PolicyEngine-UK?
|
||||||
|
|
||||||
|
PolicyEngine-UK is the "calculator" for UK taxes and benefits. When you use policyengine.org/uk, PolicyEngine-UK runs behind the scenes.
|
||||||
|
|
||||||
|
**What it models:**
|
||||||
|
|
||||||
|
**Direct taxes:**
|
||||||
|
- Income tax (UK-wide, Scottish, and Welsh variations)
|
||||||
|
- National Insurance (Classes 1, 2, 4)
|
||||||
|
- Capital gains tax
|
||||||
|
- Dividend tax
|
||||||
|
|
||||||
|
**Property and transaction taxes:**
|
||||||
|
- Council Tax
|
||||||
|
- Stamp Duty Land Tax (England/NI)
|
||||||
|
- Land and Buildings Transaction Tax (Scotland)
|
||||||
|
- Land Transaction Tax (Wales)
|
||||||
|
|
||||||
|
**Universal Credit:**
|
||||||
|
- Standard allowance
|
||||||
|
- Child elements
|
||||||
|
- Housing cost element
|
||||||
|
- Childcare costs element
|
||||||
|
- Carer element
|
||||||
|
- Work capability elements
|
||||||
|
|
||||||
|
**Legacy benefits (being phased out):**
|
||||||
|
- Working Tax Credit
|
||||||
|
- Child Tax Credit
|
||||||
|
- Income Support
|
||||||
|
- Income-based JSA/ESA
|
||||||
|
- Housing Benefit
|
||||||
|
|
||||||
|
**Other benefits:**
|
||||||
|
- Child Benefit
|
||||||
|
- Pension Credit
|
||||||
|
- Personal Independence Payment (PIP)
|
||||||
|
- Disability Living Allowance (DLA)
|
||||||
|
- Attendance Allowance
|
||||||
|
- State Pension
|
||||||
|
|
||||||
|
**See full list:** https://policyengine.org/uk/parameters
|
||||||
|
|
||||||
|
### Understanding Variables
|
||||||
|
|
||||||
|
When you see results in PolicyEngine, these are variables:
|
||||||
|
|
||||||
|
**Income variables:**
|
||||||
|
- `employment_income` - Gross employment earnings/salary
|
||||||
|
- `self_employment_income` - Self-employment profits
|
||||||
|
- `pension_income` - Private pension income
|
||||||
|
- `property_income` - Rental income
|
||||||
|
- `savings_interest_income` - Interest from savings
|
||||||
|
- `dividend_income` - Dividend income
|
||||||
|
|
||||||
|
**Tax variables:**
|
||||||
|
- `income_tax` - Total income tax liability
|
||||||
|
- `national_insurance` - Total NI contributions
|
||||||
|
- `council_tax` - Council tax liability
|
||||||
|
|
||||||
|
**Benefit variables:**
|
||||||
|
- `universal_credit` - Universal Credit amount
|
||||||
|
- `child_benefit` - Child Benefit amount
|
||||||
|
- `pension_credit` - Pension Credit amount
|
||||||
|
- `working_tax_credit` - Working Tax Credit (legacy)
|
||||||
|
- `child_tax_credit` - Child Tax Credit (legacy)
|
||||||
|
|
||||||
|
**Summary variables:**
|
||||||
|
- `household_net_income` - Income after taxes and benefits
|
||||||
|
- `disposable_income` - Income after taxes
|
||||||
|
- `equivalised_household_net_income` - Adjusted for household size
|
||||||
|
|
||||||
|
## For Analysts 📊
|
||||||
|
|
||||||
|
### Installation and Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install PolicyEngine-UK
|
||||||
|
pip install policyengine-uk
|
||||||
|
|
||||||
|
# Or with uv (recommended)
|
||||||
|
uv pip install policyengine-uk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_uk import Simulation
|
||||||
|
|
||||||
|
# Create a household
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"person": {
|
||||||
|
"age": {2025: 30},
|
||||||
|
"employment_income": {2025: 30000}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"benunits": {
|
||||||
|
"benunit": {
|
||||||
|
"members": ["person"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["person"],
|
||||||
|
"region": {2025: "LONDON"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate taxes and benefits
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
income_tax = sim.calculate("income_tax", 2025)[0]
|
||||||
|
universal_credit = sim.calculate("universal_credit", 2025)[0]
|
||||||
|
|
||||||
|
print(f"Income tax: £{income_tax:,.0f}")
|
||||||
|
print(f"Universal Credit: £{universal_credit:,.0f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web App to Python
|
||||||
|
|
||||||
|
**Web app URL:**
|
||||||
|
```
|
||||||
|
policyengine.org/uk/household?household=12345
|
||||||
|
```
|
||||||
|
|
||||||
|
**Equivalent Python (conceptually):**
|
||||||
|
The household ID represents a situation dictionary. To replicate in Python, you'd create a similar situation.
|
||||||
|
|
||||||
|
### When to Use This Skill
|
||||||
|
|
||||||
|
- Creating household situations for tax/benefit calculations
|
||||||
|
- Running microsimulations with PolicyEngine-UK
|
||||||
|
- Analyzing policy reforms and their impacts
|
||||||
|
- Building tools that use PolicyEngine-UK (calculators, analysis notebooks)
|
||||||
|
- Debugging PolicyEngine-UK calculations
|
||||||
|
|
||||||
|
## For Contributors 💻
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
**Location:** PolicyEngine/policyengine-uk
|
||||||
|
|
||||||
|
**To see current implementation:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/PolicyEngine/policyengine-uk
|
||||||
|
cd policyengine-uk
|
||||||
|
|
||||||
|
# Explore structure
|
||||||
|
tree policyengine_uk/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key directories:**
|
||||||
|
```bash
|
||||||
|
ls policyengine_uk/
|
||||||
|
# - variables/ - Tax and benefit calculations
|
||||||
|
# - parameters/ - Policy rules (YAML)
|
||||||
|
# - reforms/ - Pre-defined reforms
|
||||||
|
# - tests/ - Test cases
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### 1. Situation Dictionary Structure
|
||||||
|
|
||||||
|
PolicyEngine UK requires a nested dictionary defining household composition:
|
||||||
|
|
||||||
|
```python
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"person_id": {
|
||||||
|
"age": {2025: 35},
|
||||||
|
"employment_income": {2025: 30000},
|
||||||
|
# ... other person attributes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"benunits": {
|
||||||
|
"benunit_id": {
|
||||||
|
"members": ["person_id", ...]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"households": {
|
||||||
|
"household_id": {
|
||||||
|
"members": ["person_id", ...],
|
||||||
|
"region": {2025: "SOUTH_EAST"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Rules:**
|
||||||
|
- All entities must have consistent member lists
|
||||||
|
- Use year keys for all values: `{2025: value}`
|
||||||
|
- Region must be one of the ITL 1 regions (see below)
|
||||||
|
- All monetary values in pounds (not pence)
|
||||||
|
- UK tax year runs April 6 to April 5 (but use calendar year in code)
|
||||||
|
|
||||||
|
**Important Entity Difference:**
|
||||||
|
- UK uses **benunits** (benefit units): a single adult OR couple + dependent children
|
||||||
|
- This is the assessment unit for most means-tested benefits
|
||||||
|
- Unlike US which uses families/marital_units/tax_units/spm_units
|
||||||
|
|
||||||
|
### 2. Creating Simulations
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_uk import Simulation
|
||||||
|
|
||||||
|
# Create simulation from situation
|
||||||
|
simulation = Simulation(situation=situation)
|
||||||
|
|
||||||
|
# Calculate variables
|
||||||
|
income_tax = simulation.calculate("income_tax", 2025)
|
||||||
|
universal_credit = simulation.calculate("universal_credit", 2025)
|
||||||
|
household_net_income = simulation.calculate("household_net_income", 2025)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Variables:**
|
||||||
|
|
||||||
|
**Income:**
|
||||||
|
- `employment_income` - Gross employment earnings
|
||||||
|
- `self_employment_income` - Self-employment profits
|
||||||
|
- `pension_income` - Private pension income
|
||||||
|
- `property_income` - Rental income
|
||||||
|
- `savings_interest_income` - Interest income
|
||||||
|
- `dividend_income` - Dividend income
|
||||||
|
- `miscellaneous_income` - Other income sources
|
||||||
|
|
||||||
|
**Tax Outputs:**
|
||||||
|
- `income_tax` - Total income tax liability
|
||||||
|
- `national_insurance` - Total NI contributions
|
||||||
|
- `council_tax` - Council tax liability
|
||||||
|
- `VAT` - Value Added Tax paid
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- `universal_credit` - Universal Credit
|
||||||
|
- `child_benefit` - Child Benefit
|
||||||
|
- `pension_credit` - Pension Credit
|
||||||
|
- `working_tax_credit` - Working Tax Credit (legacy)
|
||||||
|
- `child_tax_credit` - Child Tax Credit (legacy)
|
||||||
|
- `personal_independence_payment` - PIP
|
||||||
|
- `attendance_allowance` - Attendance Allowance
|
||||||
|
- `state_pension` - State Pension
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
- `household_net_income` - Income after taxes and benefits
|
||||||
|
- `disposable_income` - Income after taxes
|
||||||
|
- `equivalised_household_net_income` - Adjusted for household size
|
||||||
|
|
||||||
|
### 3. Using Axes for Parameter Sweeps
|
||||||
|
|
||||||
|
To vary a parameter across multiple values:
|
||||||
|
|
||||||
|
```python
|
||||||
|
situation = {
|
||||||
|
# ... normal situation setup ...
|
||||||
|
"axes": [[{
|
||||||
|
"name": "employment_income",
|
||||||
|
"count": 1001,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100000,
|
||||||
|
"period": 2025
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
simulation = Simulation(situation=situation)
|
||||||
|
# Now calculate() returns arrays of 1001 values
|
||||||
|
incomes = simulation.calculate("employment_income", 2025) # Array of 1001 values
|
||||||
|
taxes = simulation.calculate("income_tax", 2025) # Array of 1001 values
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Remove axes before creating single-point simulations:
|
||||||
|
```python
|
||||||
|
situation_single = situation.copy()
|
||||||
|
situation_single.pop("axes", None)
|
||||||
|
simulation = Simulation(situation=situation_single)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Policy Reforms
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_uk import Simulation
|
||||||
|
|
||||||
|
# Define a reform (modifies parameters)
|
||||||
|
reform = {
|
||||||
|
"gov.hmrc.income_tax.rates.uk.brackets[0].rate": {
|
||||||
|
"2025-01-01.2100-12-31": 0.25 # Increase basic rate to 25%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create simulation with reform
|
||||||
|
simulation = Simulation(situation=situation, reform=reform)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Single Person Household Calculation
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_uk import Simulation
|
||||||
|
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"person": {
|
||||||
|
"age": {2025: 30},
|
||||||
|
"employment_income": {2025: 30000}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"benunits": {
|
||||||
|
"benunit": {
|
||||||
|
"members": ["person"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["person"],
|
||||||
|
"region": {2025: "LONDON"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
income_tax = sim.calculate("income_tax", 2025)[0]
|
||||||
|
national_insurance = sim.calculate("national_insurance", 2025)[0]
|
||||||
|
universal_credit = sim.calculate("universal_credit", 2025)[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Couple with Children
|
||||||
|
|
||||||
|
```python
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"parent_1": {
|
||||||
|
"age": {2025: 35},
|
||||||
|
"employment_income": {2025: 35000}
|
||||||
|
},
|
||||||
|
"parent_2": {
|
||||||
|
"age": {2025: 33},
|
||||||
|
"employment_income": {2025: 25000}
|
||||||
|
},
|
||||||
|
"child_1": {
|
||||||
|
"age": {2025: 8}
|
||||||
|
},
|
||||||
|
"child_2": {
|
||||||
|
"age": {2025: 5}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"benunits": {
|
||||||
|
"benunit": {
|
||||||
|
"members": ["parent_1", "parent_2", "child_1", "child_2"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["parent_1", "parent_2", "child_1", "child_2"],
|
||||||
|
"region": {2025: "NORTH_WEST"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
child_benefit = sim.calculate("child_benefit", 2025)[0]
|
||||||
|
universal_credit = sim.calculate("universal_credit", 2025)[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Marginal Tax Rate Analysis
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create baseline with axes varying income
|
||||||
|
situation_with_axes = {
|
||||||
|
"people": {
|
||||||
|
"person": {
|
||||||
|
"age": {2025: 30}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"benunits": {"benunit": {"members": ["person"]}},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["person"],
|
||||||
|
"region": {2025: "LONDON"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"axes": [[{
|
||||||
|
"name": "employment_income",
|
||||||
|
"count": 1001,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100000,
|
||||||
|
"period": 2025
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation_with_axes)
|
||||||
|
incomes = sim.calculate("employment_income", 2025)
|
||||||
|
net_incomes = sim.calculate("household_net_income", 2025)
|
||||||
|
|
||||||
|
# Calculate marginal tax rate
|
||||||
|
import numpy as np
|
||||||
|
mtr = 1 - (np.gradient(net_incomes) / np.gradient(incomes))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Regional Comparison
|
||||||
|
|
||||||
|
```python
|
||||||
|
regions = ["LONDON", "SCOTLAND", "WALES", "NORTH_EAST"]
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for region in regions:
|
||||||
|
situation = create_situation(region=region, income=30000)
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
results[region] = {
|
||||||
|
"income_tax": sim.calculate("income_tax", 2025)[0],
|
||||||
|
"national_insurance": sim.calculate("national_insurance", 2025)[0],
|
||||||
|
"total_tax": sim.calculate("income_tax", 2025)[0] +
|
||||||
|
sim.calculate("national_insurance", 2025)[0]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 5: Policy Reform Impact
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_uk import Microsimulation, Reform
|
||||||
|
|
||||||
|
# Define reform: Increase basic rate to 25%
|
||||||
|
class IncreaseBasicRate(Reform):
|
||||||
|
def apply(self):
|
||||||
|
def modify_parameters(parameters):
|
||||||
|
parameters.gov.hmrc.income_tax.rates.uk.brackets[0].rate.update(
|
||||||
|
period="year:2025:10", value=0.25
|
||||||
|
)
|
||||||
|
return parameters
|
||||||
|
self.modify_parameters(modify_parameters)
|
||||||
|
|
||||||
|
# Run microsimulation
|
||||||
|
baseline = Microsimulation()
|
||||||
|
reformed = Microsimulation(reform=IncreaseBasicRate)
|
||||||
|
|
||||||
|
# Calculate revenue impact
|
||||||
|
baseline_revenue = baseline.calc("income_tax", 2025).sum()
|
||||||
|
reformed_revenue = reformed.calc("income_tax", 2025).sum()
|
||||||
|
revenue_change = (reformed_revenue - baseline_revenue) / 1e9 # in billions
|
||||||
|
|
||||||
|
# Calculate household impact
|
||||||
|
baseline_net_income = baseline.calc("household_net_income", 2025)
|
||||||
|
reformed_net_income = reformed.calc("household_net_income", 2025)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Scripts
|
||||||
|
|
||||||
|
This skill includes helper scripts in the `scripts/` directory:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_uk_skills.situation_helpers import (
|
||||||
|
create_single_person,
|
||||||
|
create_couple,
|
||||||
|
create_family_with_children,
|
||||||
|
add_region
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quick situation creation
|
||||||
|
situation = create_single_person(
|
||||||
|
income=30000,
|
||||||
|
region="LONDON",
|
||||||
|
age=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create couple
|
||||||
|
situation = create_couple(
|
||||||
|
income_1=35000,
|
||||||
|
income_2=25000,
|
||||||
|
region="SCOTLAND"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls and Solutions
|
||||||
|
|
||||||
|
### Pitfall 1: Member Lists Out of Sync
|
||||||
|
|
||||||
|
**Problem:** Different entities have different members
|
||||||
|
```python
|
||||||
|
# WRONG
|
||||||
|
"benunits": {"benunit": {"members": ["parent"]}},
|
||||||
|
"households": {"household": {"members": ["parent", "child"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Keep all entity member lists consistent:
|
||||||
|
```python
|
||||||
|
# CORRECT
|
||||||
|
all_members = ["parent", "child"]
|
||||||
|
"benunits": {"benunit": {"members": all_members}},
|
||||||
|
"households": {"household": {"members": all_members}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 2: Forgetting Year Keys
|
||||||
|
|
||||||
|
**Problem:** `"age": 35` instead of `"age": {2025: 35}`
|
||||||
|
|
||||||
|
**Solution:** Always use year dictionary:
|
||||||
|
```python
|
||||||
|
"age": {2025: 35},
|
||||||
|
"employment_income": {2025: 30000}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 3: Wrong Region Format
|
||||||
|
|
||||||
|
**Problem:** Using lowercase or incorrect region names
|
||||||
|
|
||||||
|
**Solution:** Use uppercase ITL 1 region codes:
|
||||||
|
```python
|
||||||
|
# CORRECT regions:
|
||||||
|
"region": {2025: "LONDON"}
|
||||||
|
"region": {2025: "SCOTLAND"}
|
||||||
|
"region": {2025: "WALES"}
|
||||||
|
"region": {2025: "NORTH_EAST"}
|
||||||
|
"region": {2025: "SOUTH_EAST"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 4: Axes Persistence
|
||||||
|
|
||||||
|
**Problem:** Axes remain in situation when creating single-point simulation
|
||||||
|
|
||||||
|
**Solution:** Remove axes before single-point simulation:
|
||||||
|
```python
|
||||||
|
situation_single = situation.copy()
|
||||||
|
situation_single.pop("axes", None)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 5: Missing Benunits
|
||||||
|
|
||||||
|
**Problem:** Forgetting to include benunits (benefit units)
|
||||||
|
|
||||||
|
**Solution:** Always include benunits in UK simulations:
|
||||||
|
```python
|
||||||
|
# UK requires benunits
|
||||||
|
situation = {
|
||||||
|
"people": {...},
|
||||||
|
"benunits": {"benunit": {"members": [...]}}, # Required!
|
||||||
|
"households": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regions in PolicyEngine UK
|
||||||
|
|
||||||
|
UK uses ITL 1 (International Territorial Level 1, formerly NUTS 1) regions:
|
||||||
|
|
||||||
|
**Regions:**
|
||||||
|
- `NORTH_EAST` - North East England
|
||||||
|
- `NORTH_WEST` - North West England
|
||||||
|
- `YORKSHIRE` - Yorkshire and the Humber
|
||||||
|
- `EAST_MIDLANDS` - East Midlands
|
||||||
|
- `WEST_MIDLANDS` - West Midlands
|
||||||
|
- `EAST_OF_ENGLAND` - East of England
|
||||||
|
- `LONDON` - London
|
||||||
|
- `SOUTH_EAST` - South East England
|
||||||
|
- `SOUTH_WEST` - South West England
|
||||||
|
- `WALES` - Wales
|
||||||
|
- `SCOTLAND` - Scotland
|
||||||
|
- `NORTHERN_IRELAND` - Northern Ireland
|
||||||
|
|
||||||
|
**Regional Tax Variations:**
|
||||||
|
|
||||||
|
**Scotland:**
|
||||||
|
- Has devolved income tax with 6 bands (starter 19%, basic 20%, intermediate 21%, higher 42%, advanced 45%, top 47%)
|
||||||
|
- Scottish residents automatically calculated with Scottish rates
|
||||||
|
|
||||||
|
**Wales:**
|
||||||
|
- Has Welsh Rate of Income Tax (WRIT)
|
||||||
|
- Currently maintains parity with England/NI rates
|
||||||
|
|
||||||
|
**England/Northern Ireland:**
|
||||||
|
- Standard UK rates: basic 20%, higher 40%, additional 45%
|
||||||
|
|
||||||
|
## Key Parameters and Values (2025/26)
|
||||||
|
|
||||||
|
### Income Tax
|
||||||
|
- **Personal Allowance:** £12,570
|
||||||
|
- **Basic rate threshold:** £50,270
|
||||||
|
- **Higher rate threshold:** £125,140
|
||||||
|
- **Rates:** 20% (basic), 40% (higher), 45% (additional)
|
||||||
|
- **Personal allowance tapering:** £1 reduction for every £2 over £100,000
|
||||||
|
|
||||||
|
### National Insurance (Class 1)
|
||||||
|
- **Lower Earnings Limit:** £6,396/year
|
||||||
|
- **Primary Threshold:** £12,570/year
|
||||||
|
- **Upper Earnings Limit:** £50,270/year
|
||||||
|
- **Rates:** 12% (between primary and upper), 2% (above upper)
|
||||||
|
|
||||||
|
### Universal Credit
|
||||||
|
- **Standard allowance:** Varies by single/couple and age
|
||||||
|
- **Taper rate:** 55% (rate at which UC reduced as income increases)
|
||||||
|
- **Work allowance:** Amount you can earn before UC reduced
|
||||||
|
|
||||||
|
### Child Benefit
|
||||||
|
- **First child:** Higher rate
|
||||||
|
- **Subsequent children:** Lower rate
|
||||||
|
- **High Income Charge:** Tapered withdrawal starting at £60,000
|
||||||
|
|
||||||
|
## Version Compatibility
|
||||||
|
|
||||||
|
- Use `policyengine-uk>=1.0.0` for 2025 calculations
|
||||||
|
- Check version: `import policyengine_uk; print(policyengine_uk.__version__)`
|
||||||
|
- Different years may require different package versions
|
||||||
|
|
||||||
|
## Debugging Tips
|
||||||
|
|
||||||
|
1. **Enable tracing:**
|
||||||
|
```python
|
||||||
|
simulation.trace = True
|
||||||
|
result = simulation.calculate("variable_name", 2025)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check intermediate calculations:**
|
||||||
|
```python
|
||||||
|
gross_income = simulation.calculate("gross_income", 2025)
|
||||||
|
disposable_income = simulation.calculate("disposable_income", 2025)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify situation structure:**
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
print(json.dumps(situation, indent=2))
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Test with PolicyEngine web app:**
|
||||||
|
- Go to policyengine.org/uk/household
|
||||||
|
- Enter same inputs
|
||||||
|
- Compare results
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- **Documentation:** https://policyengine.org/uk/docs
|
||||||
|
- **API Reference:** https://github.com/PolicyEngine/policyengine-uk
|
||||||
|
- **Variable Explorer:** https://policyengine.org/uk/variables
|
||||||
|
- **Parameter Explorer:** https://policyengine.org/uk/parameters
|
||||||
|
|
||||||
|
## Examples Directory
|
||||||
|
|
||||||
|
See `examples/` for complete working examples:
|
||||||
|
- `single_person.yaml` - Single person household
|
||||||
|
- `couple.yaml` - Couple without children
|
||||||
|
- `family_with_children.yaml` - Family with dependents
|
||||||
|
- `universal_credit_sweep.yaml` - Analyzing UC with axes
|
||||||
|
|
||||||
|
## Key Differences from US System
|
||||||
|
|
||||||
|
1. **Benefit Units:** UK uses `benunits` (single/couple + children) instead of US multiple entity types
|
||||||
|
2. **Universal Credit:** Consolidated means-tested benefit (vs separate SNAP, TANF, etc. in US)
|
||||||
|
3. **National Insurance:** Separate from income tax with own thresholds (vs US Social Security tax)
|
||||||
|
4. **Devolved Taxes:** Scotland and Wales have different income tax rates
|
||||||
|
5. **Tax Year:** April 6 to April 5 (vs calendar year in US)
|
||||||
|
6. **No State Variation:** Council Tax is local, but most taxes/benefits are national (vs 50 US states)
|
||||||
29
skills/policyengine-uk-skill/examples/couple.yaml
Normal file
29
skills/policyengine-uk-skill/examples/couple.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Example: Couple without children in Scotland
|
||||||
|
# Person 1: £35,000, Age: 35
|
||||||
|
# Person 2: £25,000, Age: 33
|
||||||
|
|
||||||
|
people:
|
||||||
|
person_1:
|
||||||
|
age:
|
||||||
|
2025: 35
|
||||||
|
employment_income:
|
||||||
|
2025: 35000
|
||||||
|
person_2:
|
||||||
|
age:
|
||||||
|
2025: 33
|
||||||
|
employment_income:
|
||||||
|
2025: 25000
|
||||||
|
|
||||||
|
benunits:
|
||||||
|
benunit:
|
||||||
|
members:
|
||||||
|
- person_1
|
||||||
|
- person_2
|
||||||
|
|
||||||
|
households:
|
||||||
|
household:
|
||||||
|
members:
|
||||||
|
- person_1
|
||||||
|
- person_2
|
||||||
|
region:
|
||||||
|
2025: SCOTLAND
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Example: Family with children in Wales
|
||||||
|
# Parent 1: £35,000, Age: 35
|
||||||
|
# Parent 2: £25,000, Age: 33
|
||||||
|
# Child 1: Age 8
|
||||||
|
# Child 2: Age 5
|
||||||
|
|
||||||
|
people:
|
||||||
|
parent_1:
|
||||||
|
age:
|
||||||
|
2025: 35
|
||||||
|
employment_income:
|
||||||
|
2025: 35000
|
||||||
|
parent_2:
|
||||||
|
age:
|
||||||
|
2025: 33
|
||||||
|
employment_income:
|
||||||
|
2025: 25000
|
||||||
|
child_1:
|
||||||
|
age:
|
||||||
|
2025: 8
|
||||||
|
child_2:
|
||||||
|
age:
|
||||||
|
2025: 5
|
||||||
|
|
||||||
|
benunits:
|
||||||
|
benunit:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
|
||||||
|
households:
|
||||||
|
household:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
region:
|
||||||
|
2025: WALES
|
||||||
21
skills/policyengine-uk-skill/examples/single_person.yaml
Normal file
21
skills/policyengine-uk-skill/examples/single_person.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Example: Single person household in London
|
||||||
|
# Income: £30,000, Age: 30
|
||||||
|
|
||||||
|
people:
|
||||||
|
person:
|
||||||
|
age:
|
||||||
|
2025: 30
|
||||||
|
employment_income:
|
||||||
|
2025: 30000
|
||||||
|
|
||||||
|
benunits:
|
||||||
|
benunit:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
|
||||||
|
households:
|
||||||
|
household:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
region:
|
||||||
|
2025: LONDON
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Example: Analyzing Universal Credit with income variation
|
||||||
|
# Single parent with 2 children in North West
|
||||||
|
# Sweeps employment income from £0 to £50,000
|
||||||
|
|
||||||
|
people:
|
||||||
|
parent:
|
||||||
|
age:
|
||||||
|
2025: 30
|
||||||
|
child_1:
|
||||||
|
age:
|
||||||
|
2025: 8
|
||||||
|
child_2:
|
||||||
|
age:
|
||||||
|
2025: 5
|
||||||
|
|
||||||
|
benunits:
|
||||||
|
benunit:
|
||||||
|
members:
|
||||||
|
- parent
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
|
||||||
|
households:
|
||||||
|
household:
|
||||||
|
members:
|
||||||
|
- parent
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
region:
|
||||||
|
2025: NORTH_WEST
|
||||||
|
|
||||||
|
# Axes: Vary employment income from £0 to £50,000
|
||||||
|
axes:
|
||||||
|
- - name: employment_income
|
||||||
|
count: 1001
|
||||||
|
min: 0
|
||||||
|
max: 50000
|
||||||
|
period: 2025
|
||||||
339
skills/policyengine-uk-skill/scripts/situation_helpers.py
Normal file
339
skills/policyengine-uk-skill/scripts/situation_helpers.py
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
"""
|
||||||
|
Helper functions for creating PolicyEngine-UK situations.
|
||||||
|
|
||||||
|
These utilities simplify the creation of situation dictionaries
|
||||||
|
for common household configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CURRENT_YEAR = 2025
|
||||||
|
|
||||||
|
# UK ITL 1 regions
|
||||||
|
VALID_REGIONS = [
|
||||||
|
"NORTH_EAST",
|
||||||
|
"NORTH_WEST",
|
||||||
|
"YORKSHIRE",
|
||||||
|
"EAST_MIDLANDS",
|
||||||
|
"WEST_MIDLANDS",
|
||||||
|
"EAST_OF_ENGLAND",
|
||||||
|
"LONDON",
|
||||||
|
"SOUTH_EAST",
|
||||||
|
"SOUTH_WEST",
|
||||||
|
"WALES",
|
||||||
|
"SCOTLAND",
|
||||||
|
"NORTHERN_IRELAND"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_single_person(income, region="LONDON", age=30, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a situation for a single person household.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
income (float): Employment income
|
||||||
|
region (str): ITL 1 region (e.g., "LONDON", "SCOTLAND")
|
||||||
|
age (int): Person's age
|
||||||
|
**kwargs: Additional person attributes (e.g., self_employment_income)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
if region not in VALID_REGIONS:
|
||||||
|
raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}")
|
||||||
|
|
||||||
|
person_attrs = {
|
||||||
|
"age": {CURRENT_YEAR: age},
|
||||||
|
"employment_income": {CURRENT_YEAR: income},
|
||||||
|
}
|
||||||
|
person_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": {"person": person_attrs},
|
||||||
|
"benunits": {"benunit": {"members": ["person"]}},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["person"],
|
||||||
|
"region": {CURRENT_YEAR: region}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_couple(
|
||||||
|
income_1, income_2=0, region="LONDON", age_1=35, age_2=35, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a situation for a couple without children.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
income_1 (float): First person's employment income
|
||||||
|
income_2 (float): Second person's employment income
|
||||||
|
region (str): ITL 1 region
|
||||||
|
age_1 (int): First person's age
|
||||||
|
age_2 (int): Second person's age
|
||||||
|
**kwargs: Additional household attributes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
if region not in VALID_REGIONS:
|
||||||
|
raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}")
|
||||||
|
|
||||||
|
members = ["person_1", "person_2"]
|
||||||
|
|
||||||
|
household_attrs = {
|
||||||
|
"members": members,
|
||||||
|
"region": {CURRENT_YEAR: region}
|
||||||
|
}
|
||||||
|
household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": {
|
||||||
|
"person_1": {
|
||||||
|
"age": {CURRENT_YEAR: age_1},
|
||||||
|
"employment_income": {CURRENT_YEAR: income_1}
|
||||||
|
},
|
||||||
|
"person_2": {
|
||||||
|
"age": {CURRENT_YEAR: age_2},
|
||||||
|
"employment_income": {CURRENT_YEAR: income_2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"benunits": {"benunit": {"members": members}},
|
||||||
|
"households": {"household": household_attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_family_with_children(
|
||||||
|
parent_income,
|
||||||
|
num_children=1,
|
||||||
|
child_ages=None,
|
||||||
|
region="LONDON",
|
||||||
|
parent_age=35,
|
||||||
|
couple=False,
|
||||||
|
partner_income=0,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a situation for a family with children.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent_income (float): Primary parent's employment income
|
||||||
|
num_children (int): Number of children
|
||||||
|
child_ages (list): List of child ages (defaults to [5, 8, 12, ...])
|
||||||
|
region (str): ITL 1 region
|
||||||
|
parent_age (int): Parent's age
|
||||||
|
couple (bool): Whether this is a couple household
|
||||||
|
partner_income (float): Partner's income if couple
|
||||||
|
**kwargs: Additional household attributes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
if region not in VALID_REGIONS:
|
||||||
|
raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}")
|
||||||
|
|
||||||
|
if child_ages is None:
|
||||||
|
child_ages = [5 + i * 3 for i in range(num_children)]
|
||||||
|
elif len(child_ages) != num_children:
|
||||||
|
raise ValueError("Length of child_ages must match num_children")
|
||||||
|
|
||||||
|
people = {
|
||||||
|
"parent": {
|
||||||
|
"age": {CURRENT_YEAR: parent_age},
|
||||||
|
"employment_income": {CURRENT_YEAR: parent_income}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
members = ["parent"]
|
||||||
|
|
||||||
|
if couple:
|
||||||
|
people["partner"] = {
|
||||||
|
"age": {CURRENT_YEAR: parent_age},
|
||||||
|
"employment_income": {CURRENT_YEAR: partner_income}
|
||||||
|
}
|
||||||
|
members.append("partner")
|
||||||
|
|
||||||
|
for i, age in enumerate(child_ages):
|
||||||
|
child_id = f"child_{i+1}"
|
||||||
|
people[child_id] = {"age": {CURRENT_YEAR: age}}
|
||||||
|
members.append(child_id)
|
||||||
|
|
||||||
|
household_attrs = {
|
||||||
|
"members": members,
|
||||||
|
"region": {CURRENT_YEAR: region}
|
||||||
|
}
|
||||||
|
household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": people,
|
||||||
|
"benunits": {"benunit": {"members": members}},
|
||||||
|
"households": {"household": household_attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_income_sources(
|
||||||
|
situation,
|
||||||
|
person_id=None,
|
||||||
|
self_employment_income=0,
|
||||||
|
pension_income=0,
|
||||||
|
property_income=0,
|
||||||
|
savings_interest_income=0,
|
||||||
|
dividend_income=0,
|
||||||
|
miscellaneous_income=0
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Add additional income sources to a person in an existing situation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
situation (dict): Existing PolicyEngine situation
|
||||||
|
person_id (str): Person ID to add income to (defaults to first person)
|
||||||
|
self_employment_income (float): Self-employment income
|
||||||
|
pension_income (float): Private pension income
|
||||||
|
property_income (float): Rental income
|
||||||
|
savings_interest_income (float): Interest income
|
||||||
|
dividend_income (float): Dividend income
|
||||||
|
miscellaneous_income (float): Other income
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated situation with additional income
|
||||||
|
"""
|
||||||
|
# Get person ID
|
||||||
|
if person_id is None:
|
||||||
|
person_id = list(situation["people"].keys())[0]
|
||||||
|
|
||||||
|
# Add income sources
|
||||||
|
if self_employment_income > 0:
|
||||||
|
situation["people"][person_id]["self_employment_income"] = {
|
||||||
|
CURRENT_YEAR: self_employment_income
|
||||||
|
}
|
||||||
|
|
||||||
|
if pension_income > 0:
|
||||||
|
situation["people"][person_id]["pension_income"] = {
|
||||||
|
CURRENT_YEAR: pension_income
|
||||||
|
}
|
||||||
|
|
||||||
|
if property_income > 0:
|
||||||
|
situation["people"][person_id]["property_income"] = {
|
||||||
|
CURRENT_YEAR: property_income
|
||||||
|
}
|
||||||
|
|
||||||
|
if savings_interest_income > 0:
|
||||||
|
situation["people"][person_id]["savings_interest_income"] = {
|
||||||
|
CURRENT_YEAR: savings_interest_income
|
||||||
|
}
|
||||||
|
|
||||||
|
if dividend_income > 0:
|
||||||
|
situation["people"][person_id]["dividend_income"] = {
|
||||||
|
CURRENT_YEAR: dividend_income
|
||||||
|
}
|
||||||
|
|
||||||
|
if miscellaneous_income > 0:
|
||||||
|
situation["people"][person_id]["miscellaneous_income"] = {
|
||||||
|
CURRENT_YEAR: miscellaneous_income
|
||||||
|
}
|
||||||
|
|
||||||
|
return situation
|
||||||
|
|
||||||
|
|
||||||
|
def add_axes(situation, variable_name, min_val, max_val, count=1001):
|
||||||
|
"""
|
||||||
|
Add axes to a situation for parameter sweeps.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
situation (dict): Existing PolicyEngine situation
|
||||||
|
variable_name (str): Variable to vary (e.g., "employment_income")
|
||||||
|
min_val (float): Minimum value
|
||||||
|
max_val (float): Maximum value
|
||||||
|
count (int): Number of points (default: 1001)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated situation with axes
|
||||||
|
"""
|
||||||
|
situation["axes"] = [[{
|
||||||
|
"name": variable_name,
|
||||||
|
"count": count,
|
||||||
|
"min": min_val,
|
||||||
|
"max": max_val,
|
||||||
|
"period": CURRENT_YEAR
|
||||||
|
}]]
|
||||||
|
|
||||||
|
return situation
|
||||||
|
|
||||||
|
|
||||||
|
def set_region(situation, region):
|
||||||
|
"""
|
||||||
|
Set or change the region for a household.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
situation (dict): Existing PolicyEngine situation
|
||||||
|
region (str): ITL 1 region (e.g., "LONDON", "SCOTLAND")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated situation
|
||||||
|
"""
|
||||||
|
if region not in VALID_REGIONS:
|
||||||
|
raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}")
|
||||||
|
|
||||||
|
household_id = list(situation["households"].keys())[0]
|
||||||
|
situation["households"][household_id]["region"] = {CURRENT_YEAR: region}
|
||||||
|
|
||||||
|
return situation
|
||||||
|
|
||||||
|
|
||||||
|
def create_pensioner_household(
|
||||||
|
pension_income,
|
||||||
|
state_pension_income=0,
|
||||||
|
region="LONDON",
|
||||||
|
age=70,
|
||||||
|
couple=False,
|
||||||
|
partner_pension_income=0,
|
||||||
|
partner_age=68,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a situation for a pensioner household.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pension_income (float): Private pension income
|
||||||
|
state_pension_income (float): State pension income
|
||||||
|
region (str): ITL 1 region
|
||||||
|
age (int): Pensioner's age
|
||||||
|
couple (bool): Whether this is a couple household
|
||||||
|
partner_pension_income (float): Partner's pension income if couple
|
||||||
|
partner_age (int): Partner's age if couple
|
||||||
|
**kwargs: Additional household attributes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
if region not in VALID_REGIONS:
|
||||||
|
raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}")
|
||||||
|
|
||||||
|
people = {
|
||||||
|
"pensioner": {
|
||||||
|
"age": {CURRENT_YEAR: age},
|
||||||
|
"pension_income": {CURRENT_YEAR: pension_income},
|
||||||
|
"state_pension": {CURRENT_YEAR: state_pension_income}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
members = ["pensioner"]
|
||||||
|
|
||||||
|
if couple:
|
||||||
|
people["partner"] = {
|
||||||
|
"age": {CURRENT_YEAR: partner_age},
|
||||||
|
"pension_income": {CURRENT_YEAR: partner_pension_income},
|
||||||
|
"state_pension": {CURRENT_YEAR: 0}
|
||||||
|
}
|
||||||
|
members.append("partner")
|
||||||
|
|
||||||
|
household_attrs = {
|
||||||
|
"members": members,
|
||||||
|
"region": {CURRENT_YEAR: region}
|
||||||
|
}
|
||||||
|
household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": people,
|
||||||
|
"benunits": {"benunit": {"members": members}},
|
||||||
|
"households": {"household": household_attrs}
|
||||||
|
}
|
||||||
524
skills/policyengine-us-skill/SKILL.md
Normal file
524
skills/policyengine-us-skill/SKILL.md
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
---
|
||||||
|
name: policyengine-us
|
||||||
|
description: PolicyEngine-US tax and benefit microsimulation patterns, situation creation, and common workflows
|
||||||
|
---
|
||||||
|
|
||||||
|
# PolicyEngine-US
|
||||||
|
|
||||||
|
PolicyEngine-US models the US federal and state tax and benefit system.
|
||||||
|
|
||||||
|
## For Users 👥
|
||||||
|
|
||||||
|
### What is PolicyEngine-US?
|
||||||
|
|
||||||
|
PolicyEngine-US is the "calculator" for US taxes and benefits. When you use policyengine.org/us, PolicyEngine-US runs behind the scenes.
|
||||||
|
|
||||||
|
**What it models:**
|
||||||
|
|
||||||
|
**Federal taxes:**
|
||||||
|
- Income tax (with standard/itemized deductions)
|
||||||
|
- Payroll tax (Social Security, Medicare)
|
||||||
|
- Capital gains tax
|
||||||
|
|
||||||
|
**Federal benefits:**
|
||||||
|
- Earned Income Tax Credit (EITC)
|
||||||
|
- Child Tax Credit (CTC)
|
||||||
|
- SNAP (food stamps)
|
||||||
|
- WIC, ACA premium tax credits
|
||||||
|
- Social Security, SSI, TANF
|
||||||
|
|
||||||
|
**State programs (varies by state):**
|
||||||
|
- State income tax (all 50 states + DC)
|
||||||
|
- State EITC, CTC
|
||||||
|
- State-specific benefits
|
||||||
|
|
||||||
|
**See full list:** https://policyengine.org/us/parameters
|
||||||
|
|
||||||
|
### Understanding Variables
|
||||||
|
|
||||||
|
When you see results in PolicyEngine, these are variables:
|
||||||
|
|
||||||
|
**Income variables:**
|
||||||
|
- `employment_income` - W-2 wages
|
||||||
|
- `self_employment_income` - 1099 income
|
||||||
|
- `qualified_dividend_income` - Dividends
|
||||||
|
- `capital_gains` - Capital gains
|
||||||
|
|
||||||
|
**Tax variables:**
|
||||||
|
- `income_tax` - Federal income tax
|
||||||
|
- `state_income_tax` - State income tax
|
||||||
|
- `payroll_tax` - FICA taxes
|
||||||
|
|
||||||
|
**Benefit variables:**
|
||||||
|
- `eitc` - Earned Income Tax Credit
|
||||||
|
- `ctc` - Child Tax Credit
|
||||||
|
- `snap` - SNAP benefits
|
||||||
|
|
||||||
|
**Summary variables:**
|
||||||
|
- `household_net_income` - Income after taxes and benefits
|
||||||
|
- `household_tax` - Total taxes
|
||||||
|
- `household_benefits` - Total benefits
|
||||||
|
|
||||||
|
## For Analysts 📊
|
||||||
|
|
||||||
|
### Installation and Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install PolicyEngine-US
|
||||||
|
pip install policyengine-us
|
||||||
|
|
||||||
|
# Or with uv (recommended)
|
||||||
|
uv pip install policyengine-us
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_us import Simulation
|
||||||
|
|
||||||
|
# Create a household
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"you": {
|
||||||
|
"age": {2024: 30},
|
||||||
|
"employment_income": {2024: 50000}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"families": {"family": {"members": ["you"]}},
|
||||||
|
"marital_units": {"marital_unit": {"members": ["you"]}},
|
||||||
|
"tax_units": {"tax_unit": {"members": ["you"]}},
|
||||||
|
"spm_units": {"spm_unit": {"members": ["you"]}},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["you"],
|
||||||
|
"state_name": {2024: "CA"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate taxes and benefits
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
income_tax = sim.calculate("income_tax", 2024)[0]
|
||||||
|
eitc = sim.calculate("eitc", 2024)[0]
|
||||||
|
|
||||||
|
print(f"Income tax: ${income_tax:,.0f}")
|
||||||
|
print(f"EITC: ${eitc:,.0f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web App to Python
|
||||||
|
|
||||||
|
**Web app URL:**
|
||||||
|
```
|
||||||
|
policyengine.org/us/household?household=12345
|
||||||
|
```
|
||||||
|
|
||||||
|
**Equivalent Python (conceptually):**
|
||||||
|
The household ID represents a situation dictionary. To replicate in Python, you'd create a similar situation.
|
||||||
|
|
||||||
|
### When to Use This Skill
|
||||||
|
|
||||||
|
- Creating household situations for tax/benefit calculations
|
||||||
|
- Running microsimulations with PolicyEngine-US
|
||||||
|
- Analyzing policy reforms and their impacts
|
||||||
|
- Building tools that use PolicyEngine-US (calculators, analysis notebooks)
|
||||||
|
- Debugging PolicyEngine-US calculations
|
||||||
|
|
||||||
|
## For Contributors 💻
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
**Location:** PolicyEngine/policyengine-us
|
||||||
|
|
||||||
|
**To see current implementation:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/PolicyEngine/policyengine-us
|
||||||
|
cd policyengine-us
|
||||||
|
|
||||||
|
# Explore structure
|
||||||
|
tree policyengine_us/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key directories:**
|
||||||
|
```bash
|
||||||
|
ls policyengine_us/
|
||||||
|
# - variables/ - Tax and benefit calculations
|
||||||
|
# - parameters/ - Policy rules (YAML)
|
||||||
|
# - reforms/ - Pre-defined reforms
|
||||||
|
# - tests/ - Test cases
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### 1. Situation Dictionary Structure
|
||||||
|
|
||||||
|
PolicyEngine requires a nested dictionary defining household composition and characteristics:
|
||||||
|
|
||||||
|
```python
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"person_id": {
|
||||||
|
"age": {2024: 35},
|
||||||
|
"employment_income": {2024: 50000},
|
||||||
|
# ... other person attributes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"families": {
|
||||||
|
"family_id": {"members": ["person_id", ...]}
|
||||||
|
},
|
||||||
|
"marital_units": {
|
||||||
|
"marital_unit_id": {"members": ["person_id", ...]}
|
||||||
|
},
|
||||||
|
"tax_units": {
|
||||||
|
"tax_unit_id": {"members": ["person_id", ...]}
|
||||||
|
},
|
||||||
|
"spm_units": {
|
||||||
|
"spm_unit_id": {"members": ["person_id", ...]}
|
||||||
|
},
|
||||||
|
"households": {
|
||||||
|
"household_id": {
|
||||||
|
"members": ["person_id", ...],
|
||||||
|
"state_name": {2024: "CA"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Rules:**
|
||||||
|
- All entities must have consistent member lists
|
||||||
|
- Use year keys for all values: `{2024: value}`
|
||||||
|
- State must be two-letter code (e.g., "CA", "NY", "TX")
|
||||||
|
- All monetary values in dollars (not cents)
|
||||||
|
|
||||||
|
### 2. Creating Simulations
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_us import Simulation
|
||||||
|
|
||||||
|
# Create simulation from situation
|
||||||
|
simulation = Simulation(situation=situation)
|
||||||
|
|
||||||
|
# Calculate variables
|
||||||
|
income_tax = simulation.calculate("income_tax", 2024)
|
||||||
|
eitc = simulation.calculate("eitc", 2024)
|
||||||
|
household_net_income = simulation.calculate("household_net_income", 2024)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Variables:**
|
||||||
|
|
||||||
|
**Income:**
|
||||||
|
- `employment_income` - W-2 wages
|
||||||
|
- `self_employment_income` - 1099/business income
|
||||||
|
- `qualified_dividend_income` - Qualified dividends
|
||||||
|
- `capital_gains` - Capital gains
|
||||||
|
- `interest_income` - Interest income
|
||||||
|
- `social_security` - Social Security benefits
|
||||||
|
- `pension_income` - Pension/retirement income
|
||||||
|
|
||||||
|
**Deductions:**
|
||||||
|
- `charitable_cash_donations` - Cash charitable giving
|
||||||
|
- `real_estate_taxes` - State and local property taxes
|
||||||
|
- `mortgage_interest` - Mortgage interest deduction
|
||||||
|
- `medical_expense` - Medical and dental expenses
|
||||||
|
- `casualty_loss` - Casualty and theft losses
|
||||||
|
|
||||||
|
**Tax Outputs:**
|
||||||
|
- `income_tax` - Total federal income tax
|
||||||
|
- `payroll_tax` - FICA taxes
|
||||||
|
- `state_income_tax` - State income tax
|
||||||
|
- `household_tax` - Total taxes (federal + state + local)
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- `eitc` - Earned Income Tax Credit
|
||||||
|
- `ctc` - Child Tax Credit
|
||||||
|
- `snap` - SNAP benefits
|
||||||
|
- `household_benefits` - Total benefits
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
- `household_net_income` - Income minus taxes plus benefits
|
||||||
|
|
||||||
|
### 3. Using Axes for Parameter Sweeps
|
||||||
|
|
||||||
|
To vary a parameter across multiple values:
|
||||||
|
|
||||||
|
```python
|
||||||
|
situation = {
|
||||||
|
# ... normal situation setup ...
|
||||||
|
"axes": [[{
|
||||||
|
"name": "employment_income",
|
||||||
|
"count": 1001,
|
||||||
|
"min": 0,
|
||||||
|
"max": 200000,
|
||||||
|
"period": 2024
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
simulation = Simulation(situation=situation)
|
||||||
|
# Now calculate() returns arrays of 1001 values
|
||||||
|
incomes = simulation.calculate("employment_income", 2024) # Array of 1001 values
|
||||||
|
taxes = simulation.calculate("income_tax", 2024) # Array of 1001 values
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Remove axes before creating single-point simulations:
|
||||||
|
```python
|
||||||
|
situation_single = situation.copy()
|
||||||
|
situation_single.pop("axes", None)
|
||||||
|
simulation = Simulation(situation=situation_single)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Policy Reforms
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_us import Simulation
|
||||||
|
|
||||||
|
# Define a reform (modifies parameters)
|
||||||
|
reform = {
|
||||||
|
"gov.irs.credits.ctc.amount.base_amount": {
|
||||||
|
"2024-01-01.2100-12-31": 5000 # Increase CTC to $5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create simulation with reform
|
||||||
|
simulation = Simulation(situation=situation, reform=reform)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Single Household Calculation
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_us import Simulation
|
||||||
|
|
||||||
|
situation = {
|
||||||
|
"people": {
|
||||||
|
"parent": {
|
||||||
|
"age": {2024: 35},
|
||||||
|
"employment_income": {2024: 60000}
|
||||||
|
},
|
||||||
|
"child": {
|
||||||
|
"age": {2024: 5}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"families": {"family": {"members": ["parent", "child"]}},
|
||||||
|
"marital_units": {"marital_unit": {"members": ["parent"]}},
|
||||||
|
"tax_units": {"tax_unit": {"members": ["parent", "child"]}},
|
||||||
|
"spm_units": {"spm_unit": {"members": ["parent", "child"]}},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["parent", "child"],
|
||||||
|
"state_name": {2024: "NY"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
income_tax = sim.calculate("income_tax", 2024)[0]
|
||||||
|
ctc = sim.calculate("ctc", 2024)[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Marginal Tax Rate Analysis
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create baseline with axes varying income
|
||||||
|
situation_with_axes = {
|
||||||
|
# ... situation setup ...
|
||||||
|
"axes": [[{
|
||||||
|
"name": "employment_income",
|
||||||
|
"count": 1001,
|
||||||
|
"min": 0,
|
||||||
|
"max": 200000,
|
||||||
|
"period": 2024
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
sim = Simulation(situation=situation_with_axes)
|
||||||
|
incomes = sim.calculate("employment_income", 2024)
|
||||||
|
taxes = sim.calculate("income_tax", 2024)
|
||||||
|
|
||||||
|
# Calculate marginal tax rate
|
||||||
|
import numpy as np
|
||||||
|
mtr = np.gradient(taxes) / np.gradient(incomes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Charitable Donation Impact
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Baseline (no donation)
|
||||||
|
situation_baseline = create_situation(income=100000, donation=0)
|
||||||
|
sim_baseline = Simulation(situation=situation_baseline)
|
||||||
|
tax_baseline = sim_baseline.calculate("income_tax", 2024)[0]
|
||||||
|
|
||||||
|
# With donation
|
||||||
|
situation_donation = create_situation(income=100000, donation=5000)
|
||||||
|
sim_donation = Simulation(situation=situation_donation)
|
||||||
|
tax_donation = sim_donation.calculate("income_tax", 2024)[0]
|
||||||
|
|
||||||
|
# Tax savings from donation
|
||||||
|
tax_savings = tax_baseline - tax_donation
|
||||||
|
effective_discount = tax_savings / 5000 # e.g., 0.24 = 24% discount
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: State Comparison
|
||||||
|
|
||||||
|
```python
|
||||||
|
states = ["CA", "NY", "TX", "FL"]
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for state in states:
|
||||||
|
situation = create_situation(state=state, income=75000)
|
||||||
|
sim = Simulation(situation=situation)
|
||||||
|
results[state] = {
|
||||||
|
"state_income_tax": sim.calculate("state_income_tax", 2024)[0],
|
||||||
|
"total_tax": sim.calculate("household_tax", 2024)[0]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Scripts
|
||||||
|
|
||||||
|
This skill includes helper scripts in the `scripts/` directory:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from policyengine_skills.situation_helpers import (
|
||||||
|
create_single_filer,
|
||||||
|
create_married_couple,
|
||||||
|
create_family_with_children,
|
||||||
|
add_itemized_deductions
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quick situation creation
|
||||||
|
situation = create_single_filer(
|
||||||
|
income=50000,
|
||||||
|
state="CA",
|
||||||
|
age=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add deductions
|
||||||
|
situation = add_itemized_deductions(
|
||||||
|
situation,
|
||||||
|
charitable_donations=5000,
|
||||||
|
mortgage_interest=10000,
|
||||||
|
real_estate_taxes=8000
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls and Solutions
|
||||||
|
|
||||||
|
### Pitfall 1: Member Lists Out of Sync
|
||||||
|
**Problem:** Different entities have different members
|
||||||
|
```python
|
||||||
|
# WRONG
|
||||||
|
"tax_units": {"tax_unit": {"members": ["parent"]}},
|
||||||
|
"households": {"household": {"members": ["parent", "child"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Keep all entity member lists consistent:
|
||||||
|
```python
|
||||||
|
# CORRECT
|
||||||
|
all_members = ["parent", "child"]
|
||||||
|
"families": {"family": {"members": all_members}},
|
||||||
|
"tax_units": {"tax_unit": {"members": all_members}},
|
||||||
|
"households": {"household": {"members": all_members}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 2: Forgetting Year Keys
|
||||||
|
**Problem:** `"age": 35` instead of `"age": {2024: 35}`
|
||||||
|
|
||||||
|
**Solution:** Always use year dictionary:
|
||||||
|
```python
|
||||||
|
"age": {2024: 35},
|
||||||
|
"employment_income": {2024: 50000}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 3: Net Taxes vs Gross Taxes
|
||||||
|
**Problem:** Forgetting to subtract benefits from taxes
|
||||||
|
|
||||||
|
**Solution:** Use proper calculation:
|
||||||
|
```python
|
||||||
|
# Net taxes (what household actually pays)
|
||||||
|
net_tax = sim.calculate("household_tax", 2024) - \
|
||||||
|
sim.calculate("household_benefits", 2024)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 4: Axes Persistence
|
||||||
|
**Problem:** Axes remain in situation when creating single-point simulation
|
||||||
|
|
||||||
|
**Solution:** Remove axes before single-point simulation:
|
||||||
|
```python
|
||||||
|
situation_single = situation.copy()
|
||||||
|
situation_single.pop("axes", None)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 5: State-Specific Variables
|
||||||
|
**Problem:** Using NYC-specific variables without `in_nyc: True`
|
||||||
|
|
||||||
|
**Solution:** Set NYC flag for NY residents in NYC:
|
||||||
|
```python
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"state_name": {2024: "NY"},
|
||||||
|
"in_nyc": {2024: True} # Required for NYC taxes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## NYC Handling
|
||||||
|
|
||||||
|
For New York City residents:
|
||||||
|
```python
|
||||||
|
situation = {
|
||||||
|
# ... people setup ...
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["person"],
|
||||||
|
"state_name": {2024: "NY"},
|
||||||
|
"in_nyc": {2024: True} # Enable NYC tax calculations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Compatibility
|
||||||
|
|
||||||
|
- Always use `policyengine-us>=1.155.0` for 2024 calculations
|
||||||
|
- Check version: `import policyengine_us; print(policyengine_us.__version__)`
|
||||||
|
- Different years may require different package versions
|
||||||
|
|
||||||
|
## Debugging Tips
|
||||||
|
|
||||||
|
1. **Enable tracing:**
|
||||||
|
```python
|
||||||
|
simulation.trace = True
|
||||||
|
result = simulation.calculate("variable_name", 2024)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check intermediate calculations:**
|
||||||
|
```python
|
||||||
|
agi = simulation.calculate("adjusted_gross_income", 2024)
|
||||||
|
taxable_income = simulation.calculate("taxable_income", 2024)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify situation structure:**
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
print(json.dumps(situation, indent=2))
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Test with PolicyEngine web app:**
|
||||||
|
- Go to policyengine.org/us/household
|
||||||
|
- Enter same inputs
|
||||||
|
- Compare results
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- **Documentation:** https://policyengine.org/us/docs
|
||||||
|
- **API Reference:** https://github.com/PolicyEngine/policyengine-us
|
||||||
|
- **Example Notebooks:** https://github.com/PolicyEngine/analysis-notebooks
|
||||||
|
- **Variable Explorer:** https://policyengine.org/us/variables
|
||||||
|
|
||||||
|
## Examples Directory
|
||||||
|
|
||||||
|
See `examples/` for complete working examples:
|
||||||
|
- `single_filer.yaml` - Single person household
|
||||||
|
- `married_couple.yaml` - Married filing jointly
|
||||||
|
- `family_with_children.yaml` - Family with dependents
|
||||||
|
- `itemized_deductions.yaml` - Using itemized deductions
|
||||||
|
- `donation_sweep.yaml` - Analyzing donation impacts with axes
|
||||||
71
skills/policyengine-us-skill/examples/donation_sweep.yaml
Normal file
71
skills/policyengine-us-skill/examples/donation_sweep.yaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Example: Analyzing charitable donation impacts using axes
|
||||||
|
# Married couple with 2 children in New York
|
||||||
|
# Sweeps charitable donations from $0 to $50,000
|
||||||
|
|
||||||
|
people:
|
||||||
|
parent_1:
|
||||||
|
age:
|
||||||
|
2024: 35
|
||||||
|
employment_income:
|
||||||
|
2024: 100000
|
||||||
|
parent_2:
|
||||||
|
age:
|
||||||
|
2024: 35
|
||||||
|
employment_income:
|
||||||
|
2024: 50000
|
||||||
|
child_1:
|
||||||
|
age:
|
||||||
|
2024: 8
|
||||||
|
child_2:
|
||||||
|
age:
|
||||||
|
2024: 5
|
||||||
|
|
||||||
|
families:
|
||||||
|
family:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
|
||||||
|
marital_units:
|
||||||
|
marital_unit:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
|
||||||
|
tax_units:
|
||||||
|
tax_unit:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
|
||||||
|
spm_units:
|
||||||
|
spm_unit:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
|
||||||
|
households:
|
||||||
|
household:
|
||||||
|
members:
|
||||||
|
- parent_1
|
||||||
|
- parent_2
|
||||||
|
- child_1
|
||||||
|
- child_2
|
||||||
|
state_name:
|
||||||
|
2024: NY
|
||||||
|
|
||||||
|
# Axes: Vary charitable donations from $0 to $50,000
|
||||||
|
axes:
|
||||||
|
- - name: charitable_cash_donations
|
||||||
|
count: 1001
|
||||||
|
min: 0
|
||||||
|
max: 50000
|
||||||
|
period: 2024
|
||||||
38
skills/policyengine-us-skill/examples/single_filer.yaml
Normal file
38
skills/policyengine-us-skill/examples/single_filer.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Example: Single tax filer in California
|
||||||
|
# Income: $60,000, Age: 30, with charitable donations
|
||||||
|
|
||||||
|
people:
|
||||||
|
person:
|
||||||
|
age:
|
||||||
|
2024: 30
|
||||||
|
employment_income:
|
||||||
|
2024: 60000
|
||||||
|
charitable_cash_donations:
|
||||||
|
2024: 5000
|
||||||
|
|
||||||
|
families:
|
||||||
|
family:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
|
||||||
|
marital_units:
|
||||||
|
marital_unit:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
|
||||||
|
tax_units:
|
||||||
|
tax_unit:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
|
||||||
|
spm_units:
|
||||||
|
spm_unit:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
|
||||||
|
households:
|
||||||
|
household:
|
||||||
|
members:
|
||||||
|
- person
|
||||||
|
state_name:
|
||||||
|
2024: CA
|
||||||
257
skills/policyengine-us-skill/scripts/situation_helpers.py
Normal file
257
skills/policyengine-us-skill/scripts/situation_helpers.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
"""
|
||||||
|
Helper functions for creating PolicyEngine-US situations.
|
||||||
|
|
||||||
|
These utilities simplify the creation of situation dictionaries
|
||||||
|
for common household configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CURRENT_YEAR = 2024
|
||||||
|
|
||||||
|
|
||||||
|
def create_single_filer(income, state="CA", age=35, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a situation for a single tax filer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
income (float): Employment income
|
||||||
|
state (str): Two-letter state code (e.g., "CA", "NY")
|
||||||
|
age (int): Person's age
|
||||||
|
**kwargs: Additional person attributes (e.g., self_employment_income)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
person_attrs = {
|
||||||
|
"age": {CURRENT_YEAR: age},
|
||||||
|
"employment_income": {CURRENT_YEAR: income},
|
||||||
|
}
|
||||||
|
person_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": {"person": person_attrs},
|
||||||
|
"families": {"family": {"members": ["person"]}},
|
||||||
|
"marital_units": {"marital_unit": {"members": ["person"]}},
|
||||||
|
"tax_units": {"tax_unit": {"members": ["person"]}},
|
||||||
|
"spm_units": {"spm_unit": {"members": ["person"]}},
|
||||||
|
"households": {
|
||||||
|
"household": {
|
||||||
|
"members": ["person"],
|
||||||
|
"state_name": {CURRENT_YEAR: state}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_married_couple(
|
||||||
|
income_1, income_2=0, state="CA", age_1=35, age_2=35, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a situation for a married couple filing jointly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
income_1 (float): First spouse's employment income
|
||||||
|
income_2 (float): Second spouse's employment income
|
||||||
|
state (str): Two-letter state code
|
||||||
|
age_1 (int): First spouse's age
|
||||||
|
age_2 (int): Second spouse's age
|
||||||
|
**kwargs: Additional household attributes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
members = ["spouse_1", "spouse_2"]
|
||||||
|
|
||||||
|
household_attrs = {
|
||||||
|
"members": members,
|
||||||
|
"state_name": {CURRENT_YEAR: state}
|
||||||
|
}
|
||||||
|
household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": {
|
||||||
|
"spouse_1": {
|
||||||
|
"age": {CURRENT_YEAR: age_1},
|
||||||
|
"employment_income": {CURRENT_YEAR: income_1}
|
||||||
|
},
|
||||||
|
"spouse_2": {
|
||||||
|
"age": {CURRENT_YEAR: age_2},
|
||||||
|
"employment_income": {CURRENT_YEAR: income_2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"families": {"family": {"members": members}},
|
||||||
|
"marital_units": {"marital_unit": {"members": members}},
|
||||||
|
"tax_units": {"tax_unit": {"members": members}},
|
||||||
|
"spm_units": {"spm_unit": {"members": members}},
|
||||||
|
"households": {"household": household_attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_family_with_children(
|
||||||
|
parent_income,
|
||||||
|
num_children=1,
|
||||||
|
child_ages=None,
|
||||||
|
state="CA",
|
||||||
|
parent_age=35,
|
||||||
|
married=False,
|
||||||
|
spouse_income=0,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a situation for a family with children.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent_income (float): Primary parent's employment income
|
||||||
|
num_children (int): Number of children
|
||||||
|
child_ages (list): List of child ages (defaults to [5, 8, 12, ...])
|
||||||
|
state (str): Two-letter state code
|
||||||
|
parent_age (int): Parent's age
|
||||||
|
married (bool): Whether parents are married
|
||||||
|
spouse_income (float): Spouse's income if married
|
||||||
|
**kwargs: Additional household attributes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: PolicyEngine situation dictionary
|
||||||
|
"""
|
||||||
|
if child_ages is None:
|
||||||
|
child_ages = [5 + i * 3 for i in range(num_children)]
|
||||||
|
elif len(child_ages) != num_children:
|
||||||
|
raise ValueError("Length of child_ages must match num_children")
|
||||||
|
|
||||||
|
people = {
|
||||||
|
"parent": {
|
||||||
|
"age": {CURRENT_YEAR: parent_age},
|
||||||
|
"employment_income": {CURRENT_YEAR: parent_income}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
members = ["parent"]
|
||||||
|
|
||||||
|
if married:
|
||||||
|
people["spouse"] = {
|
||||||
|
"age": {CURRENT_YEAR: parent_age},
|
||||||
|
"employment_income": {CURRENT_YEAR: spouse_income}
|
||||||
|
}
|
||||||
|
members.append("spouse")
|
||||||
|
|
||||||
|
for i, age in enumerate(child_ages):
|
||||||
|
child_id = f"child_{i+1}"
|
||||||
|
people[child_id] = {"age": {CURRENT_YEAR: age}}
|
||||||
|
members.append(child_id)
|
||||||
|
|
||||||
|
household_attrs = {
|
||||||
|
"members": members,
|
||||||
|
"state_name": {CURRENT_YEAR: state}
|
||||||
|
}
|
||||||
|
household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"people": people,
|
||||||
|
"families": {"family": {"members": members}},
|
||||||
|
"marital_units": {
|
||||||
|
"marital_unit": {
|
||||||
|
"members": members if married else ["parent"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tax_units": {"tax_unit": {"members": members}},
|
||||||
|
"spm_units": {"spm_unit": {"members": members}},
|
||||||
|
"households": {"household": household_attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_itemized_deductions(
|
||||||
|
situation,
|
||||||
|
charitable_donations=0,
|
||||||
|
mortgage_interest=0,
|
||||||
|
real_estate_taxes=0,
|
||||||
|
medical_expenses=0,
|
||||||
|
casualty_losses=0
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Add itemized deductions to an existing situation.
|
||||||
|
|
||||||
|
Adds deductions to the first person in the situation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
situation (dict): Existing PolicyEngine situation
|
||||||
|
charitable_donations (float): Cash charitable contributions
|
||||||
|
mortgage_interest (float): Mortgage interest paid
|
||||||
|
real_estate_taxes (float): State and local property taxes
|
||||||
|
medical_expenses (float): Medical and dental expenses
|
||||||
|
casualty_losses (float): Casualty and theft losses
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated situation with deductions
|
||||||
|
"""
|
||||||
|
# Get first person ID
|
||||||
|
first_person = list(situation["people"].keys())[0]
|
||||||
|
|
||||||
|
# Add deductions
|
||||||
|
if charitable_donations > 0:
|
||||||
|
situation["people"][first_person]["charitable_cash_donations"] = {
|
||||||
|
CURRENT_YEAR: charitable_donations
|
||||||
|
}
|
||||||
|
|
||||||
|
if mortgage_interest > 0:
|
||||||
|
situation["people"][first_person]["mortgage_interest"] = {
|
||||||
|
CURRENT_YEAR: mortgage_interest
|
||||||
|
}
|
||||||
|
|
||||||
|
if real_estate_taxes > 0:
|
||||||
|
situation["people"][first_person]["real_estate_taxes"] = {
|
||||||
|
CURRENT_YEAR: real_estate_taxes
|
||||||
|
}
|
||||||
|
|
||||||
|
if medical_expenses > 0:
|
||||||
|
situation["people"][first_person]["medical_expense"] = {
|
||||||
|
CURRENT_YEAR: medical_expenses
|
||||||
|
}
|
||||||
|
|
||||||
|
if casualty_losses > 0:
|
||||||
|
situation["people"][first_person]["casualty_loss"] = {
|
||||||
|
CURRENT_YEAR: casualty_losses
|
||||||
|
}
|
||||||
|
|
||||||
|
return situation
|
||||||
|
|
||||||
|
|
||||||
|
def add_axes(situation, variable_name, min_val, max_val, count=1001):
|
||||||
|
"""
|
||||||
|
Add axes to a situation for parameter sweeps.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
situation (dict): Existing PolicyEngine situation
|
||||||
|
variable_name (str): Variable to vary (e.g., "employment_income")
|
||||||
|
min_val (float): Minimum value
|
||||||
|
max_val (float): Maximum value
|
||||||
|
count (int): Number of points (default: 1001)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated situation with axes
|
||||||
|
"""
|
||||||
|
situation["axes"] = [[{
|
||||||
|
"name": variable_name,
|
||||||
|
"count": count,
|
||||||
|
"min": min_val,
|
||||||
|
"max": max_val,
|
||||||
|
"period": CURRENT_YEAR
|
||||||
|
}]]
|
||||||
|
|
||||||
|
return situation
|
||||||
|
|
||||||
|
|
||||||
|
def set_state_nyc(situation, in_nyc=True):
|
||||||
|
"""
|
||||||
|
Set state to NY and configure NYC residence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
situation (dict): Existing PolicyEngine situation
|
||||||
|
in_nyc (bool): Whether household is in NYC
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated situation
|
||||||
|
"""
|
||||||
|
household_id = list(situation["households"].keys())[0]
|
||||||
|
situation["households"][household_id]["state_name"] = {CURRENT_YEAR: "NY"}
|
||||||
|
situation["households"][household_id]["in_nyc"] = {CURRENT_YEAR: in_nyc}
|
||||||
|
|
||||||
|
return situation
|
||||||
526
skills/policyengine-writing-skill/SKILL.md
Normal file
526
skills/policyengine-writing-skill/SKILL.md
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
---
|
||||||
|
name: policyengine-writing
|
||||||
|
description: PolicyEngine writing style for blog posts, documentation, PR descriptions, and research reports - emphasizing active voice, quantitative precision, and neutral tone
|
||||||
|
---
|
||||||
|
|
||||||
|
# PolicyEngine Writing Skill
|
||||||
|
|
||||||
|
Use this skill when writing blog posts, documentation, PR descriptions, research reports, or any public-facing PolicyEngine content.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Writing blog posts about policy analysis
|
||||||
|
- Creating PR descriptions
|
||||||
|
- Drafting documentation
|
||||||
|
- Writing research reports
|
||||||
|
- Composing social media posts
|
||||||
|
- Creating newsletters
|
||||||
|
- Writing README files
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
PolicyEngine's writing emphasizes clarity, precision, and objectivity.
|
||||||
|
|
||||||
|
1. **Active voice** - Prefer active constructions over passive
|
||||||
|
2. **Direct and quantitative** - Use specific numbers, avoid vague adjectives/adverbs
|
||||||
|
3. **Sentence case** - Use sentence case for headings, not title case
|
||||||
|
4. **Neutral tone** - Describe what policies do, not whether they're good or bad
|
||||||
|
5. **Precise language** - Choose exact verbs over vague modifiers
|
||||||
|
|
||||||
|
## Active Voice
|
||||||
|
|
||||||
|
Active voice makes writing clearer and more direct.
|
||||||
|
|
||||||
|
**✅ Correct (Active):**
|
||||||
|
```
|
||||||
|
Harris proposes expanding the Earned Income Tax Credit
|
||||||
|
The reform reduces poverty by 3.2%
|
||||||
|
PolicyEngine projects higher costs than other organizations
|
||||||
|
We estimate the ten-year costs
|
||||||
|
The bill lowers the state's top income tax rate
|
||||||
|
Montana raises the EITC from 10% to 20%
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong (Passive):**
|
||||||
|
```
|
||||||
|
The Earned Income Tax Credit is proposed to be expanded by Harris
|
||||||
|
Poverty is reduced by 3.2% by the reform
|
||||||
|
Higher costs are projected by PolicyEngine
|
||||||
|
The ten-year costs are estimated
|
||||||
|
The state's top income tax rate is lowered by the bill
|
||||||
|
The EITC is raised from 10% to 20% by Montana
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quantitative and Precise
|
||||||
|
|
||||||
|
Replace vague modifiers with specific numbers and measurements.
|
||||||
|
|
||||||
|
**✅ Correct (Quantitative):**
|
||||||
|
```
|
||||||
|
Costs the state $245 million
|
||||||
|
Benefits 77% of Montana residents
|
||||||
|
Lowers the Supplemental Poverty Measure by 0.8%
|
||||||
|
Raises net income by $252 in 2026
|
||||||
|
The reform affects 14.3 million households
|
||||||
|
Hours worked falls by 0.27%, or 411,000 full-time equivalent jobs
|
||||||
|
The top decile receives an average benefit of $1,033
|
||||||
|
PolicyEngine projects costs 40% higher than the Tax Foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong (Vague adjectives/adverbs):**
|
||||||
|
```
|
||||||
|
Significantly costs the state
|
||||||
|
Benefits most Montana residents
|
||||||
|
Greatly lowers poverty
|
||||||
|
Substantially raises net income
|
||||||
|
The reform affects many households
|
||||||
|
Hours worked falls considerably
|
||||||
|
High earners receive large benefits
|
||||||
|
PolicyEngine projects much higher costs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sentence Case for Headings
|
||||||
|
|
||||||
|
Use sentence case (capitalize only the first word and proper nouns) for all headings.
|
||||||
|
|
||||||
|
**✅ Correct (Sentence case):**
|
||||||
|
```
|
||||||
|
## The proposal
|
||||||
|
## Nationwide impacts
|
||||||
|
## Household impacts
|
||||||
|
## Statewide impacts 2026
|
||||||
|
## Case study: the End Child Poverty Act
|
||||||
|
## Key findings
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong (Title case):**
|
||||||
|
```
|
||||||
|
## The Proposal
|
||||||
|
## Nationwide Impacts
|
||||||
|
## Household Impacts
|
||||||
|
## Statewide Impacts 2026
|
||||||
|
## Case Study: The End Child Poverty Act
|
||||||
|
## Key Findings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Neutral, Objective Tone
|
||||||
|
|
||||||
|
Describe what policies do without value judgments. Let readers draw their own conclusions from the data.
|
||||||
|
|
||||||
|
**✅ Correct (Neutral):**
|
||||||
|
```
|
||||||
|
The reform reduces poverty by 3.2% and raises inequality by 0.16%
|
||||||
|
Single filers with earnings between $8,000 and $37,000 see their net incomes increase
|
||||||
|
The tax changes raise the net income of 75.9% of residents
|
||||||
|
PolicyEngine projects higher costs than other organizations
|
||||||
|
The top income decile receives 42% of total benefits
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong (Value judgments):**
|
||||||
|
```
|
||||||
|
The reform successfully reduces poverty by 3.2% but unfortunately raises inequality
|
||||||
|
Low-income workers finally see their net incomes increase
|
||||||
|
The tax changes benefit most residents
|
||||||
|
PolicyEngine provides more accurate cost estimates
|
||||||
|
The wealthiest households receive a disproportionate share of benefits
|
||||||
|
```
|
||||||
|
|
||||||
|
## Precise Verbs Over Adverbs
|
||||||
|
|
||||||
|
Choose specific verbs instead of generic verbs modified by adverbs.
|
||||||
|
|
||||||
|
**✅ Correct (Precise verbs):**
|
||||||
|
```
|
||||||
|
The bill lowers the top rate from 5.9% to 5.4%
|
||||||
|
The policy raises the maximum credit from $632 to $1,774
|
||||||
|
The reform increases the phase-in rate from 7.65% to 15.3%
|
||||||
|
This doubles Montana's EITC from 10% to 20%
|
||||||
|
The change eliminates the age cap
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong (Vague verbs + adverbs):**
|
||||||
|
```
|
||||||
|
The bill significantly changes the top rate
|
||||||
|
The policy substantially increases the maximum credit
|
||||||
|
The reform greatly boosts the phase-in rate
|
||||||
|
This dramatically expands Montana's EITC
|
||||||
|
The change completely removes the age cap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Concrete Examples
|
||||||
|
|
||||||
|
Always include specific household examples with precise numbers.
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
For a single adult with no children and $10,000 of earnings, the tax provisions
|
||||||
|
increase their net income by $69 in 2026 and $68 in 2027, solely from the
|
||||||
|
doubled EITC match.
|
||||||
|
|
||||||
|
A single parent of two kids with an annual income of $50,000 will see a $252
|
||||||
|
increase to their net income: $179 from the expanded EITC, and $73 from the
|
||||||
|
lower bracket threshold.
|
||||||
|
|
||||||
|
A married couple with no dependents and $200,000 of earnings will see their
|
||||||
|
liability drop by $1,306 in 2027.
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
Low-income workers see modest increases to their net income from the
|
||||||
|
expanded EITC.
|
||||||
|
|
||||||
|
Families with children benefit substantially from the tax changes.
|
||||||
|
|
||||||
|
High earners also see significant reductions in their tax liability.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables and Data
|
||||||
|
|
||||||
|
Use tables liberally to present data clearly. Always include units and context.
|
||||||
|
|
||||||
|
**Example 1: Tax parameters over time**
|
||||||
|
|
||||||
|
| Year | Phase-in rate | Max credit | Phase-out start | Phase-out rate |
|
||||||
|
| ---- | ------------- | ---------- | --------------- | -------------- |
|
||||||
|
| 2025 | 15.3% | $1,774 | $13,706 | 15.3% |
|
||||||
|
| 2026 | 15.3% | $1,815 | $14,022 | 15.3% |
|
||||||
|
| 2027 | 15.3% | $1,852 | $14,306 | 15.3% |
|
||||||
|
|
||||||
|
**Example 2: Household impacts**
|
||||||
|
|
||||||
|
| Household composition | 2026 net income change | 2027 net income change |
|
||||||
|
| ------------------------------ | ---------------------- | ---------------------- |
|
||||||
|
| Single, no children, $10,000 | $66 | $68 |
|
||||||
|
| Single, two children, $50,000 | $252 | $266 |
|
||||||
|
| Married, no children, $200,000 | $853 | $1,306 |
|
||||||
|
|
||||||
|
**Example 3: Ten-year costs**
|
||||||
|
|
||||||
|
| Year | Federal cost ($ billions) |
|
||||||
|
| ------- | ------------------------- |
|
||||||
|
| 2025 | 14.3 |
|
||||||
|
| 2026 | 14.4 |
|
||||||
|
| 2027 | 14.7 |
|
||||||
|
| 2025-34 | 143.7 |
|
||||||
|
|
||||||
|
## Avoid Superlatives
|
||||||
|
|
||||||
|
Replace superlative claims with specific comparisons.
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
PolicyEngine projects costs 40% higher than the Tax Foundation
|
||||||
|
The top decile receives an average benefit of $1,033
|
||||||
|
The reform reduces child poverty by 3.2 percentage points
|
||||||
|
This represents Montana's largest income tax cut since 2021
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
PolicyEngine provides the most accurate cost projections
|
||||||
|
The wealthiest households receive massive benefits
|
||||||
|
The reform dramatically slashes child poverty
|
||||||
|
This is Montana's largest income tax cut in history
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure and Organization
|
||||||
|
|
||||||
|
Follow a clear hierarchical structure with key findings up front.
|
||||||
|
|
||||||
|
**Standard blog post structure:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Title (H1)
|
||||||
|
|
||||||
|
Opening paragraph states what happened and when, with a link to PolicyEngine.
|
||||||
|
|
||||||
|
Key results in [year]:
|
||||||
|
- Cost: $245 million
|
||||||
|
- Benefits: 77% of residents
|
||||||
|
- Poverty impact: Reduces SPM by 0.8%
|
||||||
|
- Inequality impact: Raises Gini by 0.16%
|
||||||
|
|
||||||
|
## The proposal (H2)
|
||||||
|
|
||||||
|
Detailed description of the policy changes, often with a table showing
|
||||||
|
the specific parameter values.
|
||||||
|
|
||||||
|
## Household impacts (H2)
|
||||||
|
|
||||||
|
Specific examples for representative household types.
|
||||||
|
|
||||||
|
### Example 1: Single filer (H3)
|
||||||
|
Detailed calculation...
|
||||||
|
|
||||||
|
### Example 2: Family with children (H3)
|
||||||
|
Detailed calculation...
|
||||||
|
|
||||||
|
## Statewide impacts (H2)
|
||||||
|
|
||||||
|
Population-level analysis with charts and tables.
|
||||||
|
|
||||||
|
### Budgetary impact (H3)
|
||||||
|
Cost/revenue estimates...
|
||||||
|
|
||||||
|
### Distributional impact (H3)
|
||||||
|
Winners/losers by income decile...
|
||||||
|
|
||||||
|
### Poverty and inequality (H3)
|
||||||
|
Impact on poverty rates and inequality measures...
|
||||||
|
|
||||||
|
## Methodology (H2)
|
||||||
|
|
||||||
|
Explanation of data sources, modeling approach, and caveats.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Opening Paragraphs
|
||||||
|
|
||||||
|
State the facts directly with dates, actors, and actions:
|
||||||
|
|
||||||
|
```
|
||||||
|
[On April 28, 2025], Governor Greg Gianforte (R-MT) signed House Bill 337,
|
||||||
|
a bill that amends Montana's individual income tax code.
|
||||||
|
|
||||||
|
Vice President Harris proposes expanding the Earned Income Tax Credit (EITC)
|
||||||
|
for filers without qualifying dependents.
|
||||||
|
|
||||||
|
In her economic plan, Harris proposes to restore the expanded Earned Income
|
||||||
|
Tax Credit for workers without children to its level under the American
|
||||||
|
Rescue Plan Act in 2021.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Findings Format
|
||||||
|
|
||||||
|
Lead with bullet points of quantitative results:
|
||||||
|
|
||||||
|
```
|
||||||
|
Key results in 2027:
|
||||||
|
- Costs the state $245 million
|
||||||
|
- Benefits 77% of Montana residents
|
||||||
|
- Lowers the Supplemental Poverty Measure by 0.8%
|
||||||
|
- Raises the Gini index by 0.16%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Methodological Transparency
|
||||||
|
|
||||||
|
Always specify the model, version, and assumptions:
|
||||||
|
|
||||||
|
```
|
||||||
|
Based on static microsimulation modeling with PolicyEngine US (version 1.103.0),
|
||||||
|
we project the following economic impacts for 2025.
|
||||||
|
|
||||||
|
Assuming no behavioral responses, we project that the EITC expansion will cost
|
||||||
|
the federal government $14.3 billion in 2025.
|
||||||
|
|
||||||
|
Incorporating elasticities of labor supply used by the Congressional Budget Office
|
||||||
|
increases the reform's cost.
|
||||||
|
|
||||||
|
Over the ten-year budget window, this amounts to $143.7 billion.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Household Examples
|
||||||
|
|
||||||
|
Always include the household composition, income, and specific dollar impacts:
|
||||||
|
|
||||||
|
```
|
||||||
|
For a single adult with no children and $10,000 of earnings, the tax provisions
|
||||||
|
increase their net income by $69 in 2026 and $68 in 2027.
|
||||||
|
|
||||||
|
A single parent of two kids with an annual income of $50,000 will see a $252
|
||||||
|
increase to their net income due to House Bill 337: $179 from the expanded EITC,
|
||||||
|
and $73 from the lower bracket threshold.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples in Context
|
||||||
|
|
||||||
|
### Blog Post Opening
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
On April 28, 2025, Governor Gianforte signed House Bill 337, which lowers
|
||||||
|
Montana's top income tax rate from 5.9% to 5.4% and doubles the state EITC
|
||||||
|
from 10% to 20% of the federal credit.
|
||||||
|
|
||||||
|
Key results in 2027:
|
||||||
|
- Costs the state $245 million
|
||||||
|
- Benefits 77% of Montana residents
|
||||||
|
- Lowers the Supplemental Poverty Measure by 0.8%
|
||||||
|
- Raises the Gini index by 0.16%
|
||||||
|
|
||||||
|
Use PolicyEngine to view the full results or calculate the effect on your
|
||||||
|
household.
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
On April 28, 2025, Governor Gianforte made history by signing an amazing new
|
||||||
|
tax cut bill that will dramatically help Montana families. House Bill 337
|
||||||
|
significantly reduces tax rates and greatly expands the EITC.
|
||||||
|
|
||||||
|
This groundbreaking reform will:
|
||||||
|
- Cost the state money
|
||||||
|
- Help most residents
|
||||||
|
- Reduce poverty substantially
|
||||||
|
- Impact inequality
|
||||||
|
|
||||||
|
Check out PolicyEngine to see how much you could save!
|
||||||
|
```
|
||||||
|
|
||||||
|
### PR Description
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This PR adds Claude Code plugin configuration to enable automated installation
|
||||||
|
of agents and skills for PolicyEngine development.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add plugin auto-install configuration in .claude/settings.json
|
||||||
|
- Configure auto-install of country-models plugin from PolicyEngine/policyengine-claude
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- Access to 15 specialized agents
|
||||||
|
- 3 slash commands (/encode-policy, /review-pr, /fix-pr)
|
||||||
|
- 2 skills (policyengine-us-skill, policyengine-standards-skill)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
After merging, team members trust the repo and the plugin auto-installs.
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This amazing PR adds incredible new Claude Code plugin support that will
|
||||||
|
revolutionize PolicyEngine development!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Adds some configuration files
|
||||||
|
- Sets up plugins and stuff
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- Gets you lots of cool new features
|
||||||
|
- Makes development much easier
|
||||||
|
- Provides great new tools
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Should work great once merged!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install PolicyEngine-US from PyPI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install policyengine-us
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs version 1.103.0 or later, which includes support for 2025
|
||||||
|
tax parameters.
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Simply install PolicyEngine-US:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install policyengine-us
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install the latest version with all the newest features!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Cases
|
||||||
|
|
||||||
|
### Comparisons to Other Organizations
|
||||||
|
|
||||||
|
State facts neutrally without claiming superiority:
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
PolicyEngine projects higher costs than other organizations when considering
|
||||||
|
behavioral responses.
|
||||||
|
|
||||||
|
| Organization | Cost, 2025-2034 ($ billions) |
|
||||||
|
| ------------------------- | ---------------------------- |
|
||||||
|
| PolicyEngine (static) | 144 |
|
||||||
|
| PolicyEngine (dynamic) | 201 |
|
||||||
|
| Tax Foundation | 157 |
|
||||||
|
| Penn Wharton Budget Model | 135 |
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
PolicyEngine provides more accurate estimates than other organizations.
|
||||||
|
|
||||||
|
Unlike other models that underestimate costs, PolicyEngine correctly accounts
|
||||||
|
for behavioral responses to project a more realistic $201 billion cost.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discussing Limitations
|
||||||
|
|
||||||
|
Acknowledge limitations directly without hedging:
|
||||||
|
|
||||||
|
**✅ Correct:**
|
||||||
|
```
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
The Current Population Survey has several limitations for tax microsimulation:
|
||||||
|
|
||||||
|
- Truncates high incomes for privacy, underestimating tax impacts on high earners
|
||||||
|
- Underestimates benefit receipt compared to administrative totals
|
||||||
|
- Reflects 2020 data with 2025 policy parameters
|
||||||
|
- Lacks detail for specific income types (assumes all capital gains are long-term)
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Wrong:**
|
||||||
|
```
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
While our model is highly sophisticated, like all models it has some potential
|
||||||
|
limitations that users should be aware of:
|
||||||
|
|
||||||
|
- The data might not perfectly capture high incomes
|
||||||
|
- Benefits may be slightly underestimated
|
||||||
|
- We do our best to extrapolate older data to current years
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Checklist
|
||||||
|
|
||||||
|
Before publishing, verify:
|
||||||
|
|
||||||
|
- [ ] Use active voice throughout
|
||||||
|
- [ ] Include specific numbers for all claims
|
||||||
|
- [ ] Use sentence case for all headings
|
||||||
|
- [ ] Maintain neutral, objective tone
|
||||||
|
- [ ] Choose precise verbs over vague adverbs
|
||||||
|
- [ ] Include concrete household examples
|
||||||
|
- [ ] Present data in tables
|
||||||
|
- [ ] Avoid all superlatives
|
||||||
|
- [ ] Structure with clear hierarchy
|
||||||
|
- [ ] Open with key quantitative findings
|
||||||
|
- [ ] Specify model version and assumptions
|
||||||
|
- [ ] Link to PolicyEngine when relevant
|
||||||
|
- [ ] Acknowledge limitations directly
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Example posts**: See `policyengine-app/src/posts/articles/` for reference implementations
|
||||||
|
- **PolicyEngine app**: https://policyengine.org for linking to analyses
|
||||||
|
- **Microsimulation docs**: https://policyengine.org/us/docs for methodology details
|
||||||
Reference in New Issue
Block a user