diff --git a/src/lib/calculations.svelte.ts b/src/lib/calculations.svelte.ts
new file mode 100644
index 0000000000000000000000000000000000000000..549ad37a267bf307e4e84e2f6eafc8d7286e70b8
--- /dev/null
+++ b/src/lib/calculations.svelte.ts
@@ -0,0 +1,419 @@
+import type { Evaluation } from "$lib/decompositions"
+import type { ParametricReform } from "$lib/reforms"
+import type { VariableValueByCalculationName } from "$lib/variables"
+import { entityByKey } from "./entities"
+import type { SituationWithAxes } from "./situations"
+
+export type CalculationName = "amendment" | "bill" | "law" | "revaluation"
+
+export const calculationNames: CalculationName[] = [
+  "law",
+  "revaluation",
+  "bill",
+  "amendment",
+]
+
+export interface RequestedCalculations {
+  budgetCalculationNames?: Set<CalculationName>
+  budgetVariableName?: string
+  situationIndexByCalculationName: RequestedSituationIndexByCalculationName
+}
+
+export type RequestedSituationIndexByCalculationName = Partial<{
+  [calculationName in CalculationName]: number | null // `null` means "every test cases"
+}>
+
+export let requestedCalculations = $state({
+  situationIndexByCalculationName: {},
+}) as RequestedCalculations
+
+function calculateTestCases({}: {
+  situationIndexByCalculationName: RequestedSituationIndexByCalculationName
+}) {
+  const aggregatedSituation: SituationWithAxes = {}
+  const axesData: { situationIndex: number; variables: string[] } =
+    $axes.reduce(
+      (
+        result: { situationIndex?: number; variables: string[] },
+        parallelAxes,
+      ) => {
+        for (const axis of parallelAxes) {
+          result.situationIndex = axis.situationIndex
+          if (result.variables.includes(axis.name)) {
+            continue
+          }
+          result.variables.push(axis.name)
+        }
+        return result
+      },
+      { variables: [] },
+    )
+  const entities = Object.values(entityByKey)
+  if (
+    Object.values(situationIndexByCalculationName).some(
+      (situationIndex) => situationIndex === null,
+    )
+  ) {
+    let personIndex = -1
+    // Some of the calculations are requested to be done for every situations.
+    // Aggregate every situations into a single one without calculated variables.
+    for (const [situationIndex, situation] of $testCases.entries()) {
+      const inputInstantsByVariableName =
+        $inputInstantsByVariableNameArray[situationIndex]
+      const situationPrefix = `Cas type n°${situationIndex + 1} | `
+      for (const entity of entities) {
+        let entitySituation = situation[entity.key_plural as string]
+        if (entitySituation === undefined) {
+          continue
+        }
+        let aggregatedEntitySituation =
+          aggregatedSituation[entity.key_plural as string]
+        if (aggregatedEntitySituation === undefined) {
+          aggregatedEntitySituation = aggregatedSituation[
+            entity.key_plural as string
+          ] = {}
+        }
+        const reservedKeys = getPopulationReservedKeys(entity)
+        for (const [populationId, population] of Object.entries(
+          entitySituation,
+        ).sort(([populationId1], [populationId2]) =>
+          populationId1.localeCompare(populationId2),
+        )) {
+          const transformedPopulation: PopulationWithoutId = {}
+          if (!entity.is_person) {
+            for (const role of (entity as GroupEntity).roles) {
+              const personsIdKey = getRolePersonsIdKey(role)
+              const personsId = population[personsIdKey] as string[] | undefined
+              if (personsId === undefined) {
+                continue
+              }
+              transformedPopulation[personsIdKey] = personsId.map(
+                (personId) => situationPrefix + personId,
+              )
+            }
+          } else {
+            personIndex++
+          }
+          for (const [variableName, variableValueByInstant] of Object.entries(
+            population,
+          )) {
+            if (
+              reservedKeys.has(variableName) ||
+              (entity.is_person &&
+                axesData.situationIndex === personIndex &&
+                axesData.variables.includes(variableName))
+            ) {
+              continue
+            }
+            const inputVariableValueByInstant: {
+              [instant: string]: VariableValue | null
+            } = {}
+            const inputInstants =
+              inputInstantsByVariableName[variableName] ?? new Set<string>()
+            for (const [instant, variableValue] of Object.entries(
+              variableValueByInstant as {
+                [instant: string]: VariableValue | null
+              },
+            )) {
+              if (!inputInstants.has(instant)) {
+                // Ignore calculated value.
+                continue
+              }
+              inputVariableValueByInstant[instant] = variableValue
+            }
+            if (Object.keys(inputVariableValueByInstant).length > 0) {
+              transformedPopulation[variableName] = inputVariableValueByInstant
+            }
+          }
+          aggregatedEntitySituation[situationPrefix + populationId] =
+            transformedPopulation
+        }
+      }
+    }
+
+    if ($axes.length > 0) {
+      aggregatedSituation.axes = $axes
+    }
+  }
+
+  const message = {
+    period: $year.toString(),
+  }
+  const newCalculationByName: CalculationByName = {}
+
+  const lawSituationIndex = situationIndexByCalculationName.law
+  if (lawSituationIndex !== undefined) {
+    const calculation: Calculation = (newCalculationByName.law = {
+      running: true,
+    })
+    let situation: SituationWithAxes
+    if (lawSituationIndex === null) {
+      situation = aggregatedSituation
+    } else {
+      calculation.situationIndex = lawSituationIndex
+      situation = cleanSituation($testCases[lawSituationIndex], $axes)
+    }
+    calculation.input = {
+      ...message,
+      situation,
+      variables: [
+        ...summaryCalculatedVariablesName,
+        ...otherCalculatedVariablesName,
+        ...nonVirtualVariablesName,
+      ],
+    }
+    sendTestCasesSimulation("law", calculation.input) // Don't wait for result.
+  }
+
+  const revaluationSituationIndex = situationIndexByCalculationName.revaluation
+  if (
+    revaluationSituationIndex !== undefined &&
+    $revaluationName !== undefined
+  ) {
+    const calculation: Calculation = (newCalculationByName.revaluation = {
+      running: true,
+    })
+    let situation: SituationWithAxes
+    if (revaluationSituationIndex === null) {
+      situation = aggregatedSituation
+    } else {
+      calculation.situationIndex = revaluationSituationIndex
+      situation = cleanSituation($testCases[revaluationSituationIndex], $axes)
+    }
+    calculation.input = {
+      ...message,
+      reform: $revaluationName,
+      situation,
+      variables: [
+        ...summaryCalculatedVariablesName,
+        ...otherCalculatedVariablesName,
+        ...(nonVirtualVariablesNameByReformName[$revaluationName] ??
+          nonVirtualVariablesName),
+      ],
+    }
+    sendTestCasesSimulation("revaluation", calculation.input) // Don't wait for result.
+  }
+
+  const billSituationIndex = situationIndexByCalculationName.bill
+  if (billSituationIndex !== undefined && $billActive) {
+    const calculation: Calculation = (newCalculationByName.bill = {
+      running: true,
+    })
+    let situation: SituationWithAxes
+    if (billSituationIndex === null) {
+      situation = aggregatedSituation
+    } else {
+      calculation.situationIndex = billSituationIndex
+      situation = cleanSituation($testCases[billSituationIndex], $axes)
+    }
+    calculation.input = {
+      ...message,
+      reform: $billName,
+      situation,
+      variables: [
+        ...summaryCalculatedVariablesName,
+        ...otherCalculatedVariablesName,
+        ...(nonVirtualVariablesNameByReformName[$billName] ??
+          nonVirtualVariablesName),
+      ],
+    }
+    sendTestCasesSimulation("bill", calculation.input) // Don't wait for result.
+  }
+
+  const amendmentSituationIndex = situationIndexByCalculationName.amendment
+  if (amendmentSituationIndex !== undefined) {
+    if (Object.keys($parametricReform).length === 0) {
+      // Remove amendment evaluations from decompositions.
+      $evaluationByNameArray = $evaluationByNameArray.map(
+        (evaluationByName, situationIndex): EvaluationByName => {
+          const updatedEvaluationByName = Object.fromEntries(
+            Object.entries(evaluationByName).map(
+              ([variableName, evaluation]) => {
+                const calculationEvaluationByName = {
+                  ...evaluation.calculationEvaluationByName,
+                }
+                delete calculationEvaluationByName["amendment"]
+                return [
+                  variableName,
+                  { ...evaluation, calculationEvaluationByName },
+                ]
+              },
+            ),
+          )
+          return updateEvaluations(
+            $decompositionByName,
+            updatedEvaluationByName,
+            $testCases[situationIndex].slider?.vectorIndex ?? 0,
+            $vectorLength,
+            waterfalls,
+          )
+        },
+      )
+
+      // Remove amendment values.
+      $valuesByCalculationNameByVariableNameArray =
+        $valuesByCalculationNameByVariableNameArray.map(
+          (valuesByCalculationNameByVariableName) =>
+            Object.fromEntries(
+              Object.entries(valuesByCalculationNameByVariableName).map(
+                ([variableName, valuesByCalculationName]) => {
+                  const updatedValuesByCalculationName = {
+                    ...valuesByCalculationName,
+                  }
+                  delete updatedValuesByCalculationName["amendment"]
+                  return [variableName, updatedValuesByCalculationName]
+                },
+              ),
+            ),
+        )
+    } else {
+      const calculation: Calculation = (newCalculationByName.amendment = {
+        running: true,
+      })
+      let situation: SituationWithAxes
+      if (amendmentSituationIndex === null) {
+        situation = aggregatedSituation
+      } else {
+        calculation.situationIndex = amendmentSituationIndex
+        situation = cleanSituation($testCases[amendmentSituationIndex], $axes)
+      }
+      calculation.input = {
+        ...message,
+        parametric_reform: $parametricReform,
+        reform: $billName ?? $revaluationName, // Will be removed when $billName and $revaluationName are undefined.
+        situation,
+        variables: [
+          ...summaryCalculatedVariablesName,
+          ...otherCalculatedVariablesName,
+          ...(nonVirtualVariablesNameByReformName[$billName as string] ??
+            nonVirtualVariablesNameByReformName[$revaluationName as string] ??
+            nonVirtualVariablesName),
+        ],
+      }
+      sendTestCasesSimulation("amendment", calculation.input) // Don't wait for result.
+    }
+  }
+
+  $calculationByName = newCalculationByName
+}
+
+export function isNullVariableValueByCalculationName(
+  variableValueByCalculationName: VariableValueByCalculationName,
+): boolean {
+  return Object.values(variableValueByCalculationName).every(
+    (variableValue) => variableValue === undefined || variableValue === 0,
+  )
+}
+
+export function requestAllBudgetCalculations(variableName: string): void {
+  if (
+    variableName !== requestedCalculations.budgetVariableName ||
+    requestedCalculations.budgetCalculationNames === undefined ||
+    requestedCalculations.budgetCalculationNames.size < calculationNames.length
+  ) {
+    requestedCalculations.budgetCalculationNames = new Set(calculationNames)
+    requestedCalculations.budgetVariableName = variableName
+  }
+}
+
+export function requestAllTestCasesCalculations(
+  requestedSituationIndex: number | null,
+): void {
+  if (
+    calculationNames.some((calculationName) => {
+      const situationIndex =
+        requestedCalculations.situationIndexByCalculationName[calculationName]
+      return (
+        situationIndex !== null && situationIndex !== requestedSituationIndex
+      )
+    })
+  ) {
+    calculateTestCases(
+      Object.fromEntries(
+        calculationNames.map((calculationName) => {
+          const situationIndex =
+            requestedCalculations.situationIndexByCalculationName[
+              calculationName
+            ]
+          return [
+            calculationName,
+            situationIndex === null ? null : requestedSituationIndex,
+          ]
+        }),
+      ),
+    )
+  }
+}
+
+export function requestBudgetCalculation(
+  requestedCalculations: RequestedCalculations,
+  variableName: string,
+  calculationName: CalculationName,
+): RequestedCalculations {
+  const budgetCalculationNames =
+    variableName === requestedCalculations.budgetVariableName
+      ? (requestedCalculations.budgetCalculationNames ??
+        new Set<CalculationName>())
+      : new Set<CalculationName>()
+  if (!budgetCalculationNames.has(calculationName)) {
+    requestedCalculations = {
+      ...requestedCalculations,
+      budgetCalculationNames: budgetCalculationNames.add(calculationName),
+      budgetVariableName: variableName,
+    }
+  }
+  return requestedCalculations
+}
+
+export function requestTestCasesCalculation(
+  requestedCalculations: RequestedCalculations,
+  calculationName: CalculationName,
+  requestedSituationIndex: number | null,
+): RequestedCalculations {
+  const situationIndex =
+    requestedCalculations.situationIndexByCalculationName[calculationName]
+  if (situationIndex !== null && situationIndex !== requestedSituationIndex) {
+    requestedCalculations = {
+      ...requestedCalculations,
+      situationIndexByCalculationName: {
+        ...requestedCalculations.situationIndexByCalculationName,
+        [calculationName]: requestedSituationIndex,
+      },
+    }
+  }
+  return requestedCalculations
+}
+
+export function variableValueByCalculationNameFromEvaluation(
+  evaluation: Evaluation | undefined | null,
+  revaluationName: string | undefined | null,
+  billName: string | undefined | null,
+  parametricReform: ParametricReform,
+): VariableValueByCalculationName {
+  return {
+    amendment:
+      Object.keys(parametricReform).length === 0
+        ? undefined
+        : Math.abs(
+            evaluation?.calculationEvaluationByName["amendment"]
+              ?.deltaAtVectorIndex ?? 0,
+          ),
+    bill:
+      billName == null
+        ? undefined
+        : Math.abs(
+            evaluation?.calculationEvaluationByName["bill"]
+              ?.deltaAtVectorIndex ?? 0,
+          ),
+    law: Math.abs(
+      evaluation?.calculationEvaluationByName["law"]?.deltaAtVectorIndex ?? 0,
+    ),
+    revaluation:
+      revaluationName == null
+        ? undefined
+        : Math.abs(
+            evaluation?.calculationEvaluationByName["revaluation"]
+              ?.deltaAtVectorIndex ?? 0,
+          ),
+  }
+}