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, + ), + } +}