Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"plugins": {
"marketplaces": [
"PolicyEngine/policyengine-claude"
],
"auto_install": [
"analysis-tools@policyengine-claude"
]
}
}
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Install dependencies
run: |
uv pip install --system -e ".[dev]"

- name: Run tests
run: |
uv run pytest tests/ -v --tb=short

- name: Check formatting
if: matrix.python-version == '3.10'
run: |
pip install black isort
black --check givecalc/ tests/ ui/ --line-length 79
isort --check givecalc/ tests/ ui/
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,16 @@ __pycache__/
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
# should NOT be excluded as they contain compiler settings and other important
# information for Eclipse / Flash Builder.

# Python
*.pyc
*.egg-info/
dist/
build/
.pytest_cache/
.coverage
htmlcov/
*.so
.DS_Store
.venv/
venv/
5 changes: 1 addition & 4 deletions .streamlit/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,4 @@ toolbarMode = "minimal"
showErrorDetails = true

[browser]
gatherUsageStats = true

[theme.button]
backgroundColor = "#39C6C0"
gatherUsageStats = true
293 changes: 293 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

GiveCalc is a Streamlit web application that calculates how charitable giving affects taxes. It uses the PolicyEngine-US Python package to compute accurate federal and state tax impacts from charitable donations. Users input their income, filing status, state, and other deductions, and the app shows how donations reduce net taxes through deductions and credits.

## Running the Application

**Development:**
```bash
streamlit run app.py
```

The app will open in your browser at http://localhost:8501.

**Installation:**
```bash
# Install package in editable mode
pip install -e .

# Or use uv (recommended)
uv pip install --system -e .
```

**Testing:**
```bash
# Run tests with uv
uv run pytest tests/ -v

# Or with standard pytest
pytest tests/ -v
```

Key dependencies:
- `streamlit`: Web application framework
- `policyengine-us>=1.155.0`: Tax and benefit calculations
- `plotly`: Interactive charts
- `pandas`, `numpy`: Data manipulation
- `scipy`: Optimization for target donation calculations

## Architecture

### Package Structure

The codebase is organized into two main layers:

1. **`givecalc/` package** - Core calculation logic (installable Python package)
2. **UI layer** (`ui/`, `app.py`, `visualization.py`, `tax_info.py`) - Streamlit interface

### Application Flow

1. **User Input** (`ui/basic.py`, `ui/donations.py`)
- State selection with NYC checkbox for NY residents
- Income, filing status, number of children
- Itemized deductions (mortgage interest, real estate taxes, medical expenses, casualty losses)
- Initial donation amount

2. **Situation Creation** (`givecalc/core/situation.py`)
- Builds a PolicyEngine situation dictionary with all household members
- Creates entities: people, families, marital units, tax units, SPM units, households
- Adds an "axes" array that varies charitable donations from $0 to income in 1001 steps
- All calculations use CURRENT_YEAR (2024) from `givecalc/constants.py`

3. **Tax Calculations** (`givecalc/calculations/tax.py`)
- `calculate_donation_metrics()`: Calculates metrics at a specific donation amount
- `calculate_donation_effects()`: Runs simulation across donation range using axes
- Returns DataFrames with income tax (net of benefits) and marginal savings rate

4. **Visualization** (`visualization.py`)
- Net taxes vs donation amount line chart
- Marginal giving discount (tax savings per $1 donated)
- Net income after taxes and donations
- All charts use PolicyEngine branding (Roboto font, teal accent color)

5. **Target Donation Calculator** (`ui/target_donation.py`, `givecalc/calculations/donations.py`)
- Users can specify a target net income reduction
- Uses scipy interpolation to find the required donation amount
- Shows comparison between current and required donation

### Key Files

**Package (givecalc/):**
- `givecalc/__init__.py`: Clean package API exposing all key functions
- `givecalc/constants.py`: Shared constants (CURRENT_YEAR, colors, default age)
- `givecalc/config.py`: Configuration file loading
- `givecalc/core/situation.py`: Creates PolicyEngine situation dictionaries
- `givecalc/core/simulation.py`: Creates single-point simulations (no axes)
- `givecalc/calculations/tax.py`: Tax calculation logic
- `givecalc/calculations/donations.py`: Target donation interpolation
- `givecalc/calculations/net_income.py`: Net income calculations

**UI Layer:**
- `app.py`: Main Streamlit entry point, renders all UI sections
- `ui/basic.py`: Input widgets for income, state, filing status, deductions
- `ui/donations.py`: Donation input widgets
- `ui/tax_results.py`: Tax results display
- `ui/target_donation.py`: Target donation calculator UI
- `visualization.py`: Plotly chart generation
- `tax_info.py`: Displays federal and state tax program information
- `config.yaml`: State-specific tax program descriptions

### Code Organization

```
givecalc/
├── app.py # Main Streamlit app
├── visualization.py # Plotly chart generation
├── tax_info.py # Tax program info display
├── config.yaml # State tax program descriptions
├── requirements.txt # Python dependencies
├── requirements-dev.txt # Dev dependencies (pytest, etc.)
├── setup.py # Package installation configuration
├── pytest.ini # Pytest configuration
├── givecalc/ # Core calculation package
│ ├── __init__.py # Clean package API
│ ├── constants.py # Shared constants
│ ├── config.py # YAML config loader
│ ├── core/ # Core functionality
│ │ ├── situation.py # PolicyEngine situation builder
│ │ └── simulation.py # Single-point simulation helper
│ └── calculations/ # Calculation functions
│ ├── tax.py # Main tax calculations
│ ├── donations.py # Target donation interpolation
│ └── net_income.py # Net income calculations
├── ui/ # Streamlit UI components
│ ├── basic.py # Basic input widgets
│ ├── donations.py # Donation input widgets
│ ├── tax_results.py # Tax results display
│ └── target_donation.py # Target donation calculator
├── tests/ # Test suite (TDD)
│ ├── test_situation.py # Situation creation tests
│ ├── test_tax.py # Tax calculation tests
│ ├── test_donations.py # Donation calculation tests
│ └── test_simulation.py # Simulation tests
└── .streamlit/
└── config.toml # Streamlit theme (teal accent)
```

## PolicyEngine Integration

### Situation Dictionary Structure

PolicyEngine requires a nested dictionary with these entities:
- `people`: Individuals with income, age, deductions
- `families`: Family groupings
- `marital_units`: Marriage-based groupings
- `tax_units`: Tax filing units
- `spm_units`: Supplemental Poverty Measure units
- `households`: Geographic/state information

All entities must have consistent member lists.

### Using Axes for Donation Sweeps

The `axes` parameter creates multiple simulations varying one or more parameters:

```python
"axes": [[{
"name": "charitable_cash_donations",
"count": 1001,
"min": 0,
"max": employment_income,
"period": CURRENT_YEAR,
}]]
```

This creates 1001 simulations with donations from $0 to income. When you call `simulation.calculate()`, you get an array of 1001 values.

### Key Variables

- `household_tax`: Total taxes (federal + state + local)
- `household_benefits`: Total benefits (EITC, CTC, SNAP, etc.)
- `household_net_income`: Income minus taxes plus benefits
- Net taxes = `household_tax - household_benefits`

### State Handling

- State is specified via `state_name` in the household entity (two-letter code)
- NYC residents need `in_nyc: True` for NYC-specific taxes
- State-specific deductions/credits are automatically calculated

## Key Patterns

### Creating a Single-Point Simulation

Use `create_donation_simulation()` for calculating metrics at a specific donation:

```python
from donation_simulation import create_donation_simulation

simulation = create_donation_simulation(situation, donation_amount=5000)
net_tax = simulation.calculate("household_tax", CURRENT_YEAR) - \
simulation.calculate("household_benefits", CURRENT_YEAR)
```

### Creating a Donation Sweep

Use the situation with axes directly:

```python
from policyengine_us import Simulation

simulation = Simulation(situation=situation) # situation has axes
donations = simulation.calculate("charitable_cash_donations", map_to="household")
taxes = simulation.calculate("household_tax", map_to="household")
```

### Adding New Deductions

1. Add parameter to `create_situation()` in `situation.py`
2. Add to person's dictionary (usually "you")
3. Add input widget in `ui/basic.py` or appropriate UI module
4. Update `render_itemized_deductions()` to return the new value
5. Pass through in `app.py` when creating situation

### Marginal Tax Savings Calculation

The marginal savings rate is calculated using numpy's gradient:

```python
df["marginal_savings"] = -np.gradient(df.income_tax_after_donations) / \
np.gradient(df[donation_column])
```

This gives the tax reduction per dollar donated (e.g., 0.24 = 24¢ saved per $1 donated).

## Styling

### Colors (from constants.py)
- `TEAL_ACCENT = "#39C6C0"`: Primary UI accent color
- `BLUE_PRIMARY = "#2C6496"`: Chart color (not currently used)

### Fonts
- UI: Roboto (loaded via Google Fonts in app.py)
- Charts: Roboto Serif (set in visualization.py)

### Streamlit Theme
See `.streamlit/config.toml` for theme configuration with teal accent.

### Chart Formatting
All charts use `format_fig()` in `visualization.py` which:
- Applies PolicyEngine branding
- Adds PolicyEngine logo
- Sets Roboto Serif font
- Uses white background
- Formats axes with currency and percentage

## State Tax Programs

The `config.yaml` file contains descriptions of federal and state-specific charitable deduction programs. When adding new state programs:

1. Add entry under `state_programs` with two-letter state code
2. Include `title` and `description` fields
3. Update `tax_info.py` if new display logic is needed

States with special charitable benefits:
- **AZ**: Dollar-for-dollar tax credit (up to $400-$800)
- **MS**: Foster care charitable tax credit
- **VT**: 5% credit on first $20k in contributions
- **CO**: Subtraction for contributions over $500 with standard deduction
- **NH**: 85% education tax credit

## PolicyEngine-US Version

The app requires `policyengine-us>=1.155.0` for accurate 2024 calculations. The current version is displayed in the app footer (see `constants.py` for version detection logic).

## Development Tips

### Testing Changes Locally

1. Make code changes
2. Streamlit auto-reloads when files change
3. If auto-reload fails, refresh browser or restart `streamlit run app.py`

### Debugging PolicyEngine Issues

Add these lines to see calculation details:

```python
print(simulation.calculate("variable_name", CURRENT_YEAR))
simulation.trace = True # Enables detailed calculation tracing
```

### Common Gotchas

1. **Situation modifications**: Always copy situations before modifying to avoid side effects
2. **Member lists**: All entities (family, marital_unit, tax_unit, etc.) must have same members
3. **Year parameter**: Always use `CURRENT_YEAR` constant for consistency
4. **Axes removal**: Remove axes before creating single-point simulations
5. **Net taxes**: Remember to subtract benefits from taxes: `household_tax - household_benefits`
6. **NYC checkbox**: Only show for NY state residents (handled in `ui/basic.py`)
Loading
Loading