33
44Architecture:
55 - Rust binary is compiled at image build time (baked into the image layer).
6- - A Modal Volume (`policyengine-uk-frs`) holds the clean FRS microdata CSVs.
7- Upload once with: python api/upload_frs.py <path-to-frs-clean-dir>
6+ - Modal Volume (`policyengine-uk-frs`) holds per-year clean FRS CSVs (1994/-2023/) .
7+ Upload with: python api/upload_frs.py data/frs_clean_all
88 - FastAPI app is served via modal.asgi_app().
99
1010Deploy:
1111 modal deploy api/modal_app.py
1212
1313Serve locally (with hot-reload):
1414 modal serve api/modal_app.py
15-
16- Upload FRS data to the volume:
17- python api/upload_frs.py data/frs_clean
1815"""
1916
2017import modal
2118
2219# ---------------------------------------------------------------------------
23- # Volume for FRS clean microdata (persons.csv / benunits.csv / households.csv)
20+ # Volumes
2421# ---------------------------------------------------------------------------
2522frs_volume = modal .Volume .from_name ("policyengine-uk-frs" , create_if_missing = True )
26- FRS_MOUNT_PATH = "/data/frs_clean"
23+ FRS_MOUNT = "/data/frs_clean"
2724
2825# ---------------------------------------------------------------------------
2926# Image — Debian base, install Rust toolchain, clone repo, compile binary
3835 )
3936 .pip_install ("fastapi>=0.115" , "uvicorn[standard]>=0.30" , "pydantic>=2.0" )
4037 # Copy the repo source into the image (exclude FRS data — it stays on the Volume)
41- # The .dockerignore / ignore list ensures data/frs_clean never enters the image.
4238 .add_local_dir ("." , remote_path = "/app" , copy = True ,
4339 ignore = ["data/" , "target/" , ".git/" , "app/node_modules/" , "app/.next/" ])
4440 .run_commands (
5753
5854
5955# ---------------------------------------------------------------------------
60- # FastAPI application (identical logic to api/main.py but paths adjusted)
56+ # FastAPI application
6157# ---------------------------------------------------------------------------
6258def _make_fastapi_app ():
6359 import json
@@ -70,9 +66,8 @@ def _make_fastapi_app():
7066 from pydantic import BaseModel
7167
7268 RUST_BINARY = "policyengine-uk"
73- CLEAN_FRS_DIR = FRS_MOUNT_PATH
74- PARAMETERS_DIR = "/app/parameters"
75- AVAILABLE_YEARS = [2023 , 2024 , 2025 , 2026 , 2027 , 2028 , 2029 ]
69+ FRS_BASE_DIR = FRS_MOUNT
70+ AVAILABLE_YEARS = list (range (1994 , 2030 ))
7671
7772 fastapi_app = FastAPI (title = "PolicyEngine UK API" )
7873
@@ -87,8 +82,8 @@ def _make_fastapi_app():
8782 params_cache : dict [int , dict ] = {}
8883
8984 def _data_args () -> list [str ]:
90- if os .path .isdir (CLEAN_FRS_DIR ) and os .listdir (CLEAN_FRS_DIR ):
91- return ["--clean-frs" , CLEAN_FRS_DIR ]
85+ if os .path .isdir (FRS_BASE_DIR ) and os .listdir (FRS_BASE_DIR ):
86+ return ["--clean-frs-base " , FRS_BASE_DIR ]
9287 return []
9388
9489 def run_simulation (year : int , reform_json : Optional [str ] = None ) -> dict :
@@ -133,7 +128,7 @@ async def cache_baselines():
133128 REFORM_SECTIONS = [
134129 "income_tax" , "national_insurance" , "universal_credit" ,
135130 "child_benefit" , "benefit_cap" , "housing_benefit" ,
136- "tax_credits" , "scottish_child_payment" ,
131+ "tax_credits" , "council_tax_reduction" , " scottish_child_payment" ,
137132 "pension_credit" , "state_pension" ,
138133 ]
139134
@@ -146,6 +141,7 @@ class SimulateRequest(BaseModel):
146141 benefit_cap : Optional [dict [str , Any ]] = None
147142 housing_benefit : Optional [dict [str , Any ]] = None
148143 tax_credits : Optional [dict [str , Any ]] = None
144+ council_tax_reduction : Optional [dict [str , Any ]] = None
149145 scottish_child_payment : Optional [dict [str , Any ]] = None
150146 pension_credit : Optional [dict [str , Any ]] = None
151147 state_pension : Optional [dict [str , Any ]] = None
@@ -159,6 +155,7 @@ class SimulateMultiYearRequest(BaseModel):
159155 benefit_cap : Optional [dict [str , Any ]] = None
160156 housing_benefit : Optional [dict [str , Any ]] = None
161157 tax_credits : Optional [dict [str , Any ]] = None
158+ council_tax_reduction : Optional [dict [str , Any ]] = None
162159 scottish_child_payment : Optional [dict [str , Any ]] = None
163160 pension_credit : Optional [dict [str , Any ]] = None
164161 state_pension : Optional [dict [str , Any ]] = None
@@ -219,19 +216,19 @@ async def health():
219216 return {
220217 "status" : "ok" ,
221218 "binary" : bool (shutil .which ("policyengine-uk" )),
222- "frs_data" : os .path .isdir (CLEAN_FRS_DIR ) and bool (os .listdir (CLEAN_FRS_DIR )),
223- "frs_path " : CLEAN_FRS_DIR ,
219+ "frs_data" : os .path .isdir (FRS_BASE_DIR ) and bool (os .listdir (FRS_BASE_DIR )),
220+ "cached_years " : sorted ( baseline_cache . keys ()) ,
224221 }
225222
226223 return fastapi_app
227224
228225
229226@app .function (
230- volumes = {FRS_MOUNT_PATH : frs_volume },
231- # Startup caches all 7 year baselines — give it enough RAM
232- memory = 4096 ,
227+ volumes = {FRS_MOUNT : frs_volume },
228+ # Startup caches 36 year baselines — needs time and memory
229+ memory = 8192 ,
233230 cpu = 4 ,
234- timeout = 300 ,
231+ timeout = 600 ,
235232 # EU West (Ireland) for lower latency from UK callers
236233 region = "eu-west-1" ,
237234)
0 commit comments