Skip to content

Conversation

SakshiKekre
Copy link
Collaborator

@SakshiKekre SakshiKekre commented Oct 9, 2025

Fixes #226

Problem

Household calculation outputs were incorrectly stored in Report.output due to economy calculation limitations being applied to household calculations. Ideally, each Simulation should store its own output.


Solution Overview

Implemented a callback-based flow where the Handler manages per-simulation execution and the Manager orchestrates storage via callbacks.


Architecture Changes

Added simulationIds to CalculationMeta

Why: The handler needs to know which simulations to run calculations for. Critical for the waterfall pattern (direct URL navigation/page refresh) where metadata must be reconstructed from the report.

Handler Executes Multiple Simulations

Why: Each simulation needs its own calculation with its own policy (baseline vs reform).

  • Unique tracking keys: Uses ${reportId}-sim-${simulationId} to avoid handler collisions
  • Positional mapping: simulationIds[0] → baseline, simulationIds[1] → reform (matches report structure)
  • Synthetic progress: Aggregates progress across all simulations for a report
  • Callback coordination: Wraps onSimulationComplete callback to detect when all simulations are done and trigger onComplete

Manager Provides Storage Callbacks

Why: Separation of concerns - Handler handles calculation logic, Manager handles storage/side effects (dependency inversion).

  • Dependency injection via callbacks: Standard pattern (used in React, Express, event emitters)
  • Progressive updates: Each simulation output stored immediately via onSimulationComplete
  • Report completion: Final report status updated via onComplete after all simulations finish

Related Backend Changes (policyengine-api)

  • Service: Added update_simulation_output() to SimulationService
  • Routes: Added PATCH /{country}/simulation endpoint (ID in payload, not URL)
  • Database: Updated initialise.sql and initialise_local.sql with output_json column

The Complete Flow

  1. Manager calls Service's executeCalculation() with callbacks
  2. Service routes to Handler based on calculation type
  3. Handler loops through meta.simulationIds
  4. For each simulation:
    - Handler executes calculation → calls onSimulationComplete → Manager stores in Simulation
  5. After all simulations complete:
    - Handler calls onComplete → Manager stores Report with output={}

Copy link

vercel bot commented Oct 9, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
policyengine-app-v2 Ready Ready Preview Comment Oct 15, 2025 5:36am

Copy link
Collaborator

@anth-volk anth-volk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, Sakshi. TL;DR: I'd move the executeCalculation code to the household-level handler instead.

// Simulation IDs needed for storing household calculation outputs
simulationIds: [
String(report.simulation_1_id),
...(report.simulation_2_id ? [String(report.simulation_2_id)] : []),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Make this less hacky; I was about to write an issue asking if this didn't nest sim 2's ID inside an array, inside another array, until I re-read the manipulation code

Comment on lines 138 to 154
for (let index = 0; index < meta.simulationIds.length; index++) {
const simulationId = meta.simulationIds[index];
const policyId = index === 0 ? meta.policyIds.baseline : meta.policyIds.reform;

if (!policyId) {
continue;
}

const singleSimMeta: CalculationMeta = {
...meta,
policyIds: { baseline: policyId, reform: undefined },
simulationIds: [simulationId],
};

await this.householdHandler.execute(
`${reportId}-sim-${simulationId}`,
singleSimMeta,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue, blocking: I'd highly recommend modifying the household handler instead.

The aim of executeCalculation is to occur at the report level, executing an entire report's worth of calculation. In the long-run, with API v2, ideally we'd just move the functionality to the simulation level, which would make this easier. For the time being, with households having a sim-level calculation, it'd be more effective to add the onSimulationComplete function to the handler itself and return the calculation service code back to what it was, I think.

Copy link
Collaborator

@anth-volk anth-volk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshiKekre left some commentary on this.

Comment on lines +117 to +120
const { data: simulationData } = useQuery({
queryKey: ['simulation', reformSimId || 'none'],
queryFn: () => fetchSimulationById(metadata!.countryId, reformSimId!),
enabled: !!baseReportId && outputType === 'household' && !!reformSimId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Are you doing only the reform because in a next PR, you'll add the code for the comparison?

Comment on lines +160 to +174
if (parsed.countryId && parsed.householdData) {
// Already a Household object, just add the ID
output = {
...parsed,
id: baseReportId,
};
} else {
// Raw HouseholdData, need to wrap it
const wrappedOutput: Household = {
id: baseReportId,
countryId: metadata!.countryId,
householdData: parsed,
};
output = wrappedOutput;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why is this necessary?

Comment on lines +299 to +301
const allCompleted = simulations.every((sim) => sim.completed);
const anyError = simulations.some((sim) => sim.error);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: If a user runs, say, 4 household calculations within 5 minutes, won't this require all 4 to be complete to show any household report as complete?

meta.type === 'household' ? onComplete : undefined
);
// Execute calculation with callbacks
const result = await this.service.executeCalculation(reportId, meta, callbacks);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Are you sure this and the above section don't break society-wide calculations?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix household calculations to properly map a simulation run to a simulation record

2 participants