|
| 1 | +# Design Note — Time Step vs Investment Decisions and OpenFisca Integration |
| 2 | + |
| 3 | +**Project:** ReformLab |
| 4 | +**Author:** Lucas |
| 5 | +**Date:** 2026-04-01 |
| 6 | +**Status:** Draft |
| 7 | +**Purpose:** Clarify the architectural distinction between simulation time-step semantics and the independent investment decision model, and define how both connect to OpenFisca without violating existing adapter boundaries. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## 1. Decision Summary |
| 12 | + |
| 13 | +ReformLab should treat these as two separate but composable concerns: |
| 14 | + |
| 15 | +1. **Simulation time step / simulation mode** |
| 16 | + Controls how the model moves through time. |
| 17 | + |
| 18 | +2. **Investment decision model** |
| 19 | + Controls whether and how households switch technologies. |
| 20 | + |
| 21 | +They are not the same feature and must not be implemented as one UI or one backend concept. |
| 22 | + |
| 23 | +OpenFisca remains a **single-period computation backend** behind the existing [`ComputationAdapter`](/Users/lucas/Workspace/reformlab/src/reformlab/computation/adapter.py#L11). ReformLab adds orchestration and behavioral logic above that boundary. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## 2. Core Distinction |
| 28 | + |
| 29 | +### 2.1 Simulation Time Step |
| 30 | + |
| 31 | +This defines the temporal execution contract. |
| 32 | + |
| 33 | +Examples: |
| 34 | + |
| 35 | +- `annual_iterative` |
| 36 | + Run year by year. |
| 37 | + |
| 38 | +- `horizon_step` |
| 39 | + Simulate an `X`-year policy horizon as one modeled step and return endpoint outputs. |
| 40 | + |
| 41 | +This is about **time aggregation and execution semantics**. |
| 42 | + |
| 43 | +### 2.2 Investment Decision Model |
| 44 | + |
| 45 | +This defines behavioral dynamics. |
| 46 | + |
| 47 | +Examples: |
| 48 | + |
| 49 | +- no behavior model: households keep baseline technologies, |
| 50 | +- discrete-choice model: households choose between alternatives based on costs, subsidies, taxes, and preferences. |
| 51 | + |
| 52 | +This is about **technology adoption / switching behavior**. |
| 53 | + |
| 54 | +### 2.3 Resulting Matrix |
| 55 | + |
| 56 | +These features can exist independently: |
| 57 | + |
| 58 | +| Simulation mode | Investment decisions | Meaning | |
| 59 | +|---|---|---| |
| 60 | +| `annual_iterative` | off | Standard yearly fiscal/distributional simulation | |
| 61 | +| `annual_iterative` | on | Year-by-year adoption dynamics | |
| 62 | +| `horizon_step` | off | Endpoint fiscal/distributional effect after `X` years | |
| 63 | +| `horizon_step` | on | Endpoint technology distribution after `X` years of sustained policy exposure | |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## 3. Existing Architectural Boundary |
| 68 | + |
| 69 | +The current backend already provides the right separation of concerns: |
| 70 | + |
| 71 | +- OpenFisca is isolated behind [`ComputationAdapter.compute(population, policy, period)`](/Users/lucas/Workspace/reformlab/src/reformlab/computation/adapter.py#L26). |
| 72 | +- Yearly execution is orchestrated above that boundary in [`Orchestrator.run()`](/Users/lucas/Workspace/reformlab/src/reformlab/orchestrator/runner.py#L81). |
| 73 | +- Behavioral choice logic already lives above that boundary in [`DiscreteChoiceStep`](/Users/lucas/Workspace/reformlab/src/reformlab/discrete_choice/step.py#L43). |
| 74 | + |
| 75 | +This means: |
| 76 | + |
| 77 | +- **Do not modify OpenFisca to “understand 5 years at once.”** |
| 78 | +- **Do not collapse time-step logic into the discrete-choice model.** |
| 79 | +- **Do not collapse discrete choice into UI-only configuration.** |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## 4. Proposed Execution Model |
| 84 | + |
| 85 | +### 4.1 Annual Iterative Mode |
| 86 | + |
| 87 | +This is the current model. |
| 88 | + |
| 89 | +Execution: |
| 90 | + |
| 91 | +1. Start with population state at year `t`. |
| 92 | +2. Run `ComputationStep` for year `t`. |
| 93 | +3. Optionally run `DiscreteChoiceStep`. |
| 94 | +4. Apply carry-forward / vintage updates. |
| 95 | +5. Repeat for year `t+1`. |
| 96 | + |
| 97 | +Output: |
| 98 | + |
| 99 | +- yearly states, |
| 100 | +- yearly indicators, |
| 101 | +- optional yearly decision records. |
| 102 | + |
| 103 | +### 4.2 Horizon Step Mode |
| 104 | + |
| 105 | +This is the proposed new model. |
| 106 | + |
| 107 | +Execution: |
| 108 | + |
| 109 | +1. Start with population state at year `t`. |
| 110 | +2. Define horizon length `X`. |
| 111 | +3. Compute or derive `X`-year policy exposure for each household / alternative. |
| 112 | +4. If the investment model is enabled, evaluate alternatives using aggregated `X`-year economics. |
| 113 | +5. Produce endpoint state at year `t + X`. |
| 114 | + |
| 115 | +Output: |
| 116 | + |
| 117 | +- endpoint household state, |
| 118 | +- endpoint technology distribution, |
| 119 | +- optional aggregated indicators over the horizon, |
| 120 | +- no requirement for yearly intermediate outputs. |
| 121 | + |
| 122 | +This is **not** the same as: |
| 123 | + |
| 124 | +- running annual simulation and filtering to year `t + X`, |
| 125 | +- or skipping years in the current yearly loop. |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## 5. How Horizon Step Connects to OpenFisca |
| 130 | + |
| 131 | +### 5.1 What OpenFisca Should Continue Doing |
| 132 | + |
| 133 | +OpenFisca should continue to provide: |
| 134 | + |
| 135 | +- annual taxes/transfers, |
| 136 | +- household-level policy calculations, |
| 137 | +- alternative-specific economics when population expansion is used. |
| 138 | + |
| 139 | +### 5.2 What ReformLab Must Add Above It |
| 140 | + |
| 141 | +ReformLab must add a horizon aggregation layer that can transform annual economics into `X`-year decision signals. |
| 142 | + |
| 143 | +Examples of aggregated features: |
| 144 | + |
| 145 | +- cumulative subsidy over `X` years, |
| 146 | +- cumulative tax burden over `X` years, |
| 147 | +- discounted net cost, |
| 148 | +- simple payback period, |
| 149 | +- expected operating-cost difference, |
| 150 | +- total ownership cost over `X` years. |
| 151 | + |
| 152 | +These are not native OpenFisca outputs. They are **derived orchestration-layer constructs** built from repeated or parameterized use of OpenFisca results. |
| 153 | + |
| 154 | +### 5.3 Clean Integration Pattern |
| 155 | + |
| 156 | +Recommended pattern: |
| 157 | + |
| 158 | +1. Expand population by alternatives, as already done by `DiscreteChoiceStep`. |
| 159 | +2. Use OpenFisca via `ComputationAdapter` to compute annual household economics for each alternative. |
| 160 | +3. Aggregate those annual results into horizon features for each household-alternative pair. |
| 161 | +4. Feed those horizon features into the choice model. |
| 162 | +5. Write the chosen endpoint technology/state back into ReformLab state. |
| 163 | + |
| 164 | +This preserves the existing architectural rule: |
| 165 | + |
| 166 | +- OpenFisca computes, |
| 167 | +- ReformLab orchestrates and decides. |
| 168 | + |
| 169 | +--- |
| 170 | + |
| 171 | +## 6. New Backend Concepts Required |
| 172 | + |
| 173 | +### 6.1 Scenario Execution Mode |
| 174 | + |
| 175 | +Add a scenario-level execution mode concept: |
| 176 | + |
| 177 | +```text |
| 178 | +simulation_mode = "annual_iterative" | "horizon_step" |
| 179 | +``` |
| 180 | + |
| 181 | +### 6.2 Horizon Configuration |
| 182 | + |
| 183 | +Add explicit horizon-step configuration: |
| 184 | + |
| 185 | +```text |
| 186 | +horizon_step_years: int |
| 187 | +``` |
| 188 | + |
| 189 | +Optional future extensions: |
| 190 | + |
| 191 | +- discounting strategy, |
| 192 | +- exogenous-price series, |
| 193 | +- technology cost trajectories, |
| 194 | +- income uprating assumptions, |
| 195 | +- domain-specific aggregation rules. |
| 196 | + |
| 197 | +### 6.3 Horizon Aggregator |
| 198 | + |
| 199 | +Introduce a new orchestration/domain component, conceptually: |
| 200 | + |
| 201 | +```text |
| 202 | +HorizonAggregator |
| 203 | +``` |
| 204 | + |
| 205 | +Responsibility: |
| 206 | + |
| 207 | +- derive `X`-year endpoint economics from annual policy computations, |
| 208 | +- support multiple policy domains without changing `ComputationAdapter`. |
| 209 | + |
| 210 | +### 6.4 Endpoint Output Contract |
| 211 | + |
| 212 | +For `horizon_step`, outputs should include: |
| 213 | + |
| 214 | +- endpoint year, |
| 215 | +- simulation mode, |
| 216 | +- horizon length, |
| 217 | +- endpoint household states, |
| 218 | +- endpoint decision records if behavior is enabled. |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## 7. Validation and Governance Implications |
| 223 | + |
| 224 | +The preflight layer must validate more than it does today. |
| 225 | + |
| 226 | +New checks needed: |
| 227 | + |
| 228 | +- `horizon_step_years > 0`, |
| 229 | +- simulation mode is explicitly set, |
| 230 | +- selected policy domain supports `horizon_step`, |
| 231 | +- required exogenous assumptions are available for the full horizon, |
| 232 | +- investment decisions are either disabled or fully configured, |
| 233 | +- output semantics are compatible with the requested comparison/reporting view. |
| 234 | + |
| 235 | +Governance and manifests must also record: |
| 236 | + |
| 237 | +- simulation mode, |
| 238 | +- horizon length, |
| 239 | +- aggregation assumptions, |
| 240 | +- discounting assumptions, |
| 241 | +- exogenous series used, |
| 242 | +- adapter version, |
| 243 | +- whether results are yearly or endpoint-only. |
| 244 | + |
| 245 | +This is required so analysts do not compare annual runs and horizon-step runs as if they were identical artifacts. |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +## 8. Frontend Implications |
| 250 | + |
| 251 | +### 8.1 Workspace Structure |
| 252 | + |
| 253 | +Recommended top-level stages: |
| 254 | + |
| 255 | +1. Policies & Portfolio |
| 256 | +2. Population |
| 257 | +3. Investment Decisions |
| 258 | +4. Scenario |
| 259 | +5. Run / Results / Compare |
| 260 | + |
| 261 | +### 8.2 Stage Ownership |
| 262 | + |
| 263 | +**Investment Decisions** |
| 264 | + |
| 265 | +- optional stage, |
| 266 | +- enable/disable behavior, |
| 267 | +- model choice, |
| 268 | +- parameters, |
| 269 | +- calibration/review. |
| 270 | + |
| 271 | +**Scenario** |
| 272 | + |
| 273 | +- simulation mode, |
| 274 | +- annual horizon or horizon-step size, |
| 275 | +- seed, |
| 276 | +- inherited primary population, |
| 277 | +- optional sensitivity population, |
| 278 | +- final validation. |
| 279 | + |
| 280 | +### 8.3 Skip Logic |
| 281 | + |
| 282 | +- If investment decisions are disabled, the user can skip that stage. |
| 283 | +- If simulation mode is `annual_iterative`, existing yearly output surfaces remain valid. |
| 284 | +- If simulation mode is `horizon_step`, result surfaces must make clear that outputs are endpoint-only unless additional decomposition is available. |
| 285 | + |
| 286 | +--- |
| 287 | + |
| 288 | +## 9. Recommended Implementation Strategy |
| 289 | + |
| 290 | +### Phase A: Product and Architecture Alignment |
| 291 | + |
| 292 | +Clarify artifacts first: |
| 293 | + |
| 294 | +- separate `time_step / simulation mode` from `investment decisions`, |
| 295 | +- define top-level stage ownership, |
| 296 | +- define annual vs horizon-step output semantics. |
| 297 | + |
| 298 | +### Phase B: Backend Contract |
| 299 | + |
| 300 | +Implement: |
| 301 | + |
| 302 | +- scenario execution mode, |
| 303 | +- horizon-step validation contract, |
| 304 | +- manifest fields, |
| 305 | +- endpoint result metadata. |
| 306 | + |
| 307 | +### Phase C: Domain Logic |
| 308 | + |
| 309 | +Implement: |
| 310 | + |
| 311 | +- horizon aggregation layer, |
| 312 | +- first supported domain for `horizon_step`, |
| 313 | +- integration with discrete choice. |
| 314 | + |
| 315 | +### Phase D: UI |
| 316 | + |
| 317 | +Implement: |
| 318 | + |
| 319 | +- dedicated Investment Decisions stage, |
| 320 | +- Scenario-stage simulation-mode controls, |
| 321 | +- result-surface differentiation between yearly and endpoint outputs. |
| 322 | + |
| 323 | +--- |
| 324 | + |
| 325 | +## 10. Recommended First Slice |
| 326 | + |
| 327 | +The best first implementation slice is: |
| 328 | + |
| 329 | +1. support `horizon_step` **without** investment decisions, |
| 330 | +2. produce endpoint fiscal/distributional outputs, |
| 331 | +3. then integrate the investment model on top. |
| 332 | + |
| 333 | +Reason: |
| 334 | + |
| 335 | +- it validates the execution contract independently, |
| 336 | +- it reduces ambiguity in result semantics, |
| 337 | +- it avoids coupling two new sources of complexity in the first slice. |
| 338 | + |
| 339 | +Then the second slice is: |
| 340 | + |
| 341 | +4. enable `horizon_step + investment decisions`, |
| 342 | +5. return endpoint technology distribution. |
| 343 | + |
| 344 | +--- |
| 345 | + |
| 346 | +## 11. BMAD Implementation Path |
| 347 | + |
| 348 | +Recommended BMAD sequence: |
| 349 | + |
| 350 | +1. `bmad-correct-course` |
| 351 | + Approve the sprint change proposal and the stage-boundary correction. |
| 352 | + |
| 353 | +2. `bmad-edit-prd` |
| 354 | + Add explicit requirements for: |
| 355 | + - simulation mode, |
| 356 | + - `horizon_step`, |
| 357 | + - separation from investment decisions. |
| 358 | + |
| 359 | +3. `bmad-create-ux-design` |
| 360 | + Update the workspace IA to a five-stage shell and define skip logic. |
| 361 | + |
| 362 | +4. `bmad-create-architecture` |
| 363 | + Fold this design note into the canonical architecture decision set. |
| 364 | + |
| 365 | +5. `bmad-create-epics-and-stories` |
| 366 | + Revise existing stories and add backend work for: |
| 367 | + - execution contract, |
| 368 | + - horizon aggregation, |
| 369 | + - endpoint outputs. |
| 370 | + |
| 371 | +6. `bmad-check-implementation-readiness` |
| 372 | + Ensure PRD, UX, architecture, and epics are aligned before development. |
| 373 | + |
| 374 | +7. `bmad-sprint-planning` |
| 375 | + Plan the first implementation slice: |
| 376 | + - `horizon_step` without behavior first, |
| 377 | + - then `horizon_step` with investment decisions. |
| 378 | + |
| 379 | +--- |
| 380 | + |
| 381 | +## 12. Final Recommendation |
| 382 | + |
| 383 | +Proceed with this principle: |
| 384 | + |
| 385 | +> **Time-step semantics and investment decisions are independent features that meet at the orchestration layer, not inside OpenFisca.** |
| 386 | +
|
| 387 | +That gives ReformLab a clean architecture: |
| 388 | + |
| 389 | +- OpenFisca stays a single-period calculator, |
| 390 | +- ReformLab owns temporal orchestration, |
| 391 | +- ReformLab owns behavioral adoption logic, |
| 392 | +- the frontend reflects those as separate analyst decisions. |
0 commit comments