From 416a5922985bf95286ccb658bfb8b1b35544bd90 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:04:55 +0200 Subject: [PATCH 1/6] Add wage-growth uprating option for tax donor income matching Introduces a new model parameter `taxDonorUpratingByWage` that, when enabled, scales simulated household income by the real wage growth index (TimeSeriesVariable.WageGrowth) before nearest-neighbour matching against the tax donor database. When disabled (default), the existing price-growth-only behaviour is preserved. In both cases, imputed financial flows are deflated back to BASE_PRICE_YEAR (2015) using price inflation only (TimeSeriesVariable.Inflation). The option is exposed as a JAS-mine @GUIparameter checkbox in SimPathsModel, is configurable via YAML (taxDonorUpratingByWage), and is written to the per-run config output file. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 107 ++++++++++++++++++ config/default.yml | 5 + src/main/java/simpaths/data/Parameters.java | 11 +- .../java/simpaths/model/SimPathsModel.java | 13 ++- .../model/taxes/DonorTaxImputation.java | 16 ++- 5 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..071fe2c4c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Important: Directory Path Contains Spaces + +The project root is `C:\MyFiles\99 DEV ENV\JAS-MINE\SimPaths`. The path contains spaces, so always wrap paths in double quotes in shell/Bash commands, e.g.: + +```bash +cd "C:\MyFiles\99 DEV ENV\JAS-MINE\SimPaths" +mvn -f "C:\MyFiles\99 DEV ENV\JAS-MINE\SimPaths\pom.xml" compile +``` + +## Build & Run Commands + +```bash +# Compile only +mvn compile + +# Run unit tests (excludes integration tests) +mvn test + +# Run a single test class +mvn test -Dtest=MahalanobisDistanceTest + +# Run a single test method +mvn test -Dtest=PersonTest#methodName + +# Run integration tests (requires pre-built JARs) +mvn failsafe:integration-test + +# Build shaded JARs (produces singlerun.jar and multirun.jar in project root) +mvn package + +# Run simulation headless via multirun.jar (uses config/default.yml) +java -jar multirun.jar -config config/default.yml + +# Run database setup only +java -jar multirun.jar -DBSetup -config config/default.yml + +# Run with GUI (single run) +java -jar singlerun.jar +``` + +Java 19 is required (configured in pom.xml). Dependencies are pulled from Maven Central and JitPack (for JAS-mine). + +## Architecture Overview + +SimPaths is a **dynamic microsimulation model** built on the [JAS-mine](https://github.com/jasmineRepo) simulation framework. It projects individual and household life histories year-by-year across domains: labour market, health, demographics, social care, and taxes/benefits. + +### Simulation entity hierarchy + +``` +Household — groups BenefitUnits sharing a dwelling (parent–adult-child links) + └─ BenefitUnit — the tax/benefit assessment unit (couple or single + dependent children) + └─ Person — the individual agent (all life-course state is stored here) +``` + +All three are JPA `@Entity` classes persisted via Hibernate/H2. Each carries a `PanelEntityKey` (id + simulation_time + simulation_run). + +### Simulation lifecycle + +`SimPathsModel` (extends `AbstractSimulationManager`) owns the main simulation loop. Each year it fires ordered events that update entities via `EventListener.onEvent()`. The sequence covers: mortality → union formation/dissolution → fertility → education → labour market → taxes/benefits → health → social care → statistics collection. + +### Key classes to know + +| Class | Role | +|---|---| +| `SimPathsModel` | Orchestrates yearly event schedule; holds all population lists | +| `SimPathsStart` | Entry point for single runs (GUI or headless) | +| `SimPathsMultiRun` | Entry point for multi-run / sensitivity analysis | +| `SimPathsCollector` | Collects and exports output statistics each year | +| `Parameters` | Static store for all model parameters and regression coefficients loaded from Excel | +| `ManagerRegressions` | Routes regression evaluation calls to the correct JAS-mine regression objects | +| `Person` / `BenefitUnit` | Core simulation agents — most behavioural logic lives here | + +### Tax-benefit imputation (`model/taxes/`) + +Tax and benefit outcomes are not calculated analytically. Instead, the model uses **statistical matching**: simulated households are matched to a pre-built donor database derived from EUROMOD/UKMOD output. `TaxDonorDataParser` builds this database; `DonorTaxImputation` performs the matching using a nearest-neighbour key function (`KeyFunction1`–`KeyFunction4`). + +### Intertemporal optimisation (`model/decisions/`) + +An optional backward-induction module solves for optimal lifetime consumption and labour supply. `ManagerSolveGrids` runs the solution over a multi-dimensional state-space grid (age, health, education, pension, region, etc.). `ManagerPopulateGrids` sets up the grid geometry. This is computationally expensive and disabled by default (`enableIntertemporalOptimisations: false`). + +### Regression system + +All behavioural equations are estimated externally and loaded at startup from Excel files in `input/`. `Parameters` loads them via JAS-mine's `ExcelAssistant`/`MultiKeyCoefficientMap`. `RegressionName` (enum) names every equation; `ManagerRegressions` dispatches calls by type (linear, probit, ordered probit, multinomial logit, etc.). + +### Alignment + +Several demographic processes are aligned to external targets (ONS projections, LFS shares) via `ActivityAlignmentV2`, `FertilityAlignment`, `PartnershipAlignment`, `InSchoolAlignment`, `SocialCareAlignment`. Alignment factors are exported to `AlignmentAdjustmentFactors1.csv` each run. + +### Configuration + +Runs are configured via YAML files in `config/`. `default.yml` documents all available keys with their defaults. CLI flags override YAML values. `SimPathsMultiRun` reads the YAML and reflectively sets fields on `SimPathsModel`, `SimPathsCollector`, and `Parameters`. + +### Output + +Output is written to `output//csv/` by `SimPathsCollector`. Key files: +- `Statistics1.csv` — income distribution (Gini, percentiles, S-Index) +- `Statistics2.csv` — demographic validation (partnership, employment, health by age/gender) +- `EmploymentStatistics.csv`, `HealthStatistics.csv` — domain-specific time series +- `AlignmentAdjustmentFactors1.csv` — alignment diagnostics + +### Integration tests + +`RunSimPathsIntegrationTest` runs the full simulation end-to-end using the built JARs and compares CSV output against reference files in `src/test/java/simpaths/integrationtest/expected/`. If a substantive change shifts the output, update the expected files and commit them. \ No newline at end of file diff --git a/config/default.yml b/config/default.yml index 864ff0d2b..daec23461 100644 --- a/config/default.yml +++ b/config/default.yml @@ -75,6 +75,11 @@ model_args: # --- Tax-benefit imputation --- # donorPoolAveraging: true # if true, average disposable income over k nearest-neighbour donors # rather than using the single closest donor; reduces imputation volatility +# taxDonorUpratingByWage: false # if true, scale simulated income by real wage growth (WageGrowth index) + # before matching to the tax donor database, instead of price growth only. + # Compounding WageGrowth with Inflation gives the nominal wage adjustment + # used for matching. Imputed financial flows are always deflated back to + # BASE_PRICE_YEAR (2015) using Inflation only, regardless of this setting. # --- Regression stochasticity --- # addRegressionStochasticComponent: true # include the residual draw in regression predictions diff --git a/src/main/java/simpaths/data/Parameters.java b/src/main/java/simpaths/data/Parameters.java index 114eb468c..111ad66a8 100644 --- a/src/main/java/simpaths/data/Parameters.java +++ b/src/main/java/simpaths/data/Parameters.java @@ -879,6 +879,7 @@ else if(numberOfChildren <= 5) { public static boolean flagSuppressChildcareCosts; public static boolean flagSuppressSocialCareCosts; public static boolean donorPoolAveraging; + public static boolean taxDonorUpratingByWage; public static boolean lifetimeIncomeImpute; public static double realInterestRateInnov; @@ -908,10 +909,11 @@ private static void addFixedCostRegressors(MultiKeyCoefficientMap map, List persons; @@ -380,9 +383,9 @@ public void buildObjects() { // load model parameters Parameters.loadParameters(country, maxAge, enableIntertemporalOptimisations, projectFormalChildcare, - projectSocialCare, donorPoolAveraging, fixTimeTrend, flagDefaultToTimeSeriesAverages, saveImperfectTaxDBMatches, - timeTrendStopsIn, startYear, endYear, interestRateInnov, disposableIncomeFromLabourInnov, flagSuppressChildcareCosts, - flagSuppressSocialCareCosts, lifetimeIncomeImpute); + projectSocialCare, donorPoolAveraging, taxDonorUpratingByWage, fixTimeTrend, flagDefaultToTimeSeriesAverages, + saveImperfectTaxDBMatches, timeTrendStopsIn, startYear, endYear, interestRateInnov, + disposableIncomeFromLabourInnov, flagSuppressChildcareCosts, flagSuppressSocialCareCosts, lifetimeIncomeImpute); if (lifetimeIncomeGenerate) { ManagerProjectLifetimeIncomes.run(log, lifetimeIncomeStartBirthYear, lifetimeIncomeEndBirthYear, lifetimeIncomeEndAge, lifetimeIncomeCohortSize, lifetimeIncomeWriteToCSV, @@ -722,6 +725,8 @@ private void saveRunParameters() { pw.println(line); line = "donorPoolAveraging: " + donorPoolAveraging; pw.println(line); + line = "taxDonorUpratingByWage: " + taxDonorUpratingByWage; + pw.println(line); line = "initialisePotentialEarningsFromDatabase: " + initialisePotentialEarningsFromDatabase; pw.println(line); line = "useWeights: " + useWeights; @@ -2928,6 +2933,8 @@ public void setCountry(Country country) { public void setProjectFormalChildcare(boolean projectFormalChildcare) { this.projectFormalChildcare = projectFormalChildcare; } public boolean getDonorPoolAveraging() { return donorPoolAveraging; } public void setDonorPoolAveraging(boolean val) { donorPoolAveraging = val; } + public boolean getTaxDonorUpratingByWage() { return taxDonorUpratingByWage; } + public void setTaxDonorUpratingByWage(boolean val) { taxDonorUpratingByWage = val; } public Integer getPopSize() { return popSize; diff --git a/src/main/java/simpaths/model/taxes/DonorTaxImputation.java b/src/main/java/simpaths/model/taxes/DonorTaxImputation.java index d0aa8edd1..b4d34a996 100644 --- a/src/main/java/simpaths/model/taxes/DonorTaxImputation.java +++ b/src/main/java/simpaths/model/taxes/DonorTaxImputation.java @@ -3,6 +3,7 @@ import microsim.engine.SimulationEngine; import org.apache.commons.lang3.tuple.Triple; +import simpaths.model.enums.TimeSeriesVariable; import simpaths.model.enums.UpratingCase; import java.util.*; @@ -210,7 +211,7 @@ public void evaluate() { double oi = keys.getOriginalIncomePerWeek(); double si = keys.getSecondIncomePerWeek(); double cc = keys.getChildcareCostPerWeek(); - double[] targetVector = getMeasurementVector(keys.getPriceYear(), oi, flagSecondIncome, si, flagChildcareCost, cc); + double[] targetVector = getMeasurementVector(keys.getPriceYear(), systemYear, oi, flagSecondIncome, si, flagChildcareCost, cc); for (int increment=-1; increment<2; increment=increment+2) { // search backward and then forward through candidate list @@ -425,12 +426,15 @@ public static int getCounterVal(MatchFeature targetFeature, int regime, int inde throw new RuntimeException("attempt to evaluate index of unrecognised target feature"); } - private double[] getMeasurementVector(int priceYear, double originalIncomePerWeek, boolean flagSecondIncome, double secondIncomePerWeek, - boolean flagChildcareCost, double childcareCostPerWeek) { + private double[] getMeasurementVector(int priceYear, int systemYear, double originalIncomePerWeek, boolean flagSecondIncome, + double secondIncomePerWeek, boolean flagChildcareCost, double childcareCostPerWeek) { - double oiAdj = Parameters.normaliseWeeklyIncome(priceYear, originalIncomePerWeek); - double siAdj = Parameters.normaliseWeeklyIncome(priceYear, secondIncomePerWeek); - double ccAdj = Parameters.normaliseWeeklyIncome(priceYear, childcareCostPerWeek); + double wageScale = Parameters.taxDonorUpratingByWage + ? Parameters.getTimeSeriesValue(systemYear, TimeSeriesVariable.WageGrowth) + : 1.0; + double oiAdj = Parameters.normaliseWeeklyIncome(priceYear, originalIncomePerWeek * wageScale); + double siAdj = Parameters.normaliseWeeklyIncome(priceYear, secondIncomePerWeek * wageScale); + double ccAdj = Parameters.normaliseWeeklyIncome(priceYear, childcareCostPerWeek * wageScale); if (!flagSecondIncome && !flagChildcareCost) { return new double[] {oiAdj}; } else if (flagSecondIncome && !flagChildcareCost) { From 2dda7bab71da18f71b82892189455dc9fbdd5f45 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:49:27 +0200 Subject: [PATCH 2/6] minor --- .claude/settings.local.json | 13 +++++++++++++ CLAUDE.md | 16 +++++++++++++++- .../training/DatabaseCountryYear.xlsx | Bin 6527 -> 8877 bytes input/EUROMODpolicySchedule.xlsx | Bin 8444 -> 11001 bytes 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..4c04233a5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(git checkout *)", + "Bash(mvn compile *)", + "Bash(cmd /c \"mvn compile -q 2>&1\")", + "Bash(git add *)", + "Bash(git commit -m ' *)", + "Bash(git push *)", + "mcp__github__create_pull_request" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 071fe2c4c..43891ca9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,4 +104,18 @@ Output is written to `output//csv/` by `SimPathsCollector`. Key f ### Integration tests -`RunSimPathsIntegrationTest` runs the full simulation end-to-end using the built JARs and compares CSV output against reference files in `src/test/java/simpaths/integrationtest/expected/`. If a substantive change shifts the output, update the expected files and commit them. \ No newline at end of file +`RunSimPathsIntegrationTest` runs the full simulation end-to-end using the built JARs and compares CSV output against reference files in `src/test/java/simpaths/integrationtest/expected/`. If a substantive change shifts the output, update the expected files and commit them. + +## Domain Knowledge + +This section records design principles and non-obvious rationale accumulated through development. Update it as new insights emerge. + +### Price levels: SimPaths vs. the tax donor database + +All SimPaths financial variables are stored in **real 2015 prices** (`BASE_PRICE_YEAR`). The tax donor database, however, is denominated in **nominal prices of the respective policy year**. + +Tax donor matching therefore requires a two-step price bridge: +1. Inflate SimPaths income to policy-year prices before matching. +2. Deflate the imputed financial values from the matched donor record back to 2015 prices. + +The default uprating series is `TimeSeriesVariable.Inflation`, sourced from the `UK_inflation` worksheet in `input/time_series_factor.xlsx`. An alternative option (added 2026-04) allows wage growth to be used instead of price growth for the initial matching step, controlled via a config flag. \ No newline at end of file diff --git a/input/EUROMODoutput/training/DatabaseCountryYear.xlsx b/input/EUROMODoutput/training/DatabaseCountryYear.xlsx index f6173d4ad57f30d60749c00687b9c776c1b1d0ac..71e701416ee4b8158e50c45445f7e5b55246b6a6 100644 GIT binary patch literal 8877 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 literal 6527 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 Date: Mon, 27 Apr 2026 12:28:10 +0200 Subject: [PATCH 3/6] minor --- config/default.yml | 10 +++++----- src/main/java/simpaths/model/SimPathsModel.java | 2 +- .../model/taxes/database/TaxDonorDataParser.java | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/default.yml b/config/default.yml index daec23461..785e37cba 100644 --- a/config/default.yml +++ b/config/default.yml @@ -75,11 +75,11 @@ model_args: # --- Tax-benefit imputation --- # donorPoolAveraging: true # if true, average disposable income over k nearest-neighbour donors # rather than using the single closest donor; reduces imputation volatility -# taxDonorUpratingByWage: false # if true, scale simulated income by real wage growth (WageGrowth index) - # before matching to the tax donor database, instead of price growth only. - # Compounding WageGrowth with Inflation gives the nominal wage adjustment - # used for matching. Imputed financial flows are always deflated back to - # BASE_PRICE_YEAR (2015) using Inflation only, regardless of this setting. +# taxDonorUpratingByWage: false # if true, scale simulated income by nominal wage growth (compound of + # WageGrowth and Inflation indices). before matching to the tax donor + # database, instead of price growth only. Imputed financial flows are + # always deflated back to BASE_PRICE_YEAR (2015) using Inflation only, + # regardless of this setting. # --- Regression stochasticity --- # addRegressionStochasticComponent: true # include the residual draw in regression predictions diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 9a9144c40..2ff12deef 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -204,7 +204,7 @@ public void setFirstRun(boolean firstRun) { @GUIparameter(description = "Average over donor pool when imputing transfer payments") public boolean donorPoolAveraging = true; - @GUIparameter(description = "Scale simulated income by real wage growth (instead of price growth) before tax donor matching") + @GUIparameter(description = "Scale simulated income by nominal wage growth (instead of price growth) before tax donor matching") public boolean taxDonorUpratingByWage = false; private int ordering = Parameters.MODEL_ORDERING; //Used in Scheduling of model events. Schedule model events at the same time as the collector and observer events, but a lower order, so will be fired before the collector and observer have updated. diff --git a/src/main/java/simpaths/model/taxes/database/TaxDonorDataParser.java b/src/main/java/simpaths/model/taxes/database/TaxDonorDataParser.java index 01ec0ae15..f422706fc 100644 --- a/src/main/java/simpaths/model/taxes/database/TaxDonorDataParser.java +++ b/src/main/java/simpaths/model/taxes/database/TaxDonorDataParser.java @@ -438,7 +438,6 @@ public static String stringAppender(Collection strings) { * output .txt files, picking up the relevant columns for each EUROMOD policy scenario, that * will eventually be parsed into the JAS-mine input database. * - * */ public static void constructAggregateTaxDonorPopulationCSVfile(Country country, boolean showGui) { From 67e58c24593effbb6076c6ae428ffd7347f9c661 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:39:41 +0200 Subject: [PATCH 4/6] minor --- input/EUROMODpolicySchedule.xlsx | Bin 11001 -> 8445 bytes .../model/taxes/DonorTaxImputation.java | 19 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/input/EUROMODpolicySchedule.xlsx b/input/EUROMODpolicySchedule.xlsx index 07c3ecdd30b8a58810050f9d0843ec620d9baf24..2b162e6f28ba20aeb27c71627f22a4d535adda2c 100644 GIT binary patch literal 8445 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 Orx)xKQLN%UfBPRlVxb5C literal 11001 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 D2 targetNormalisedOriginalIncome) { upperInd = testInd; upperOrigInc = testOrigInc; @@ -190,12 +196,15 @@ public void evaluate() { lowerOrigInc = testOrigInc; } } - iiTarget = upperInd; + if (Math.abs(targetNormalisedOriginalIncome-lowerOrigInc) < Math.abs(targetNormalisedOriginalIncome-upperOrigInc)) { + iiTarget = lowerInd; + } else { + iiTarget = upperInd; + } } DonorTaxUnit targetCandidate = Parameters.getDonorPool().get(candidatePool.get(iiTarget)); donorID = targetCandidate.getId(); - double targetIncomeDifference = Math.abs(targetNormalisedOriginalIncome - - targetCandidate.getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth()); + double targetIncomeDifference = Math.min(Math.abs(targetNormalisedOriginalIncome - lowerOrigInc), Math.abs(targetNormalisedOriginalIncome - upperOrigInc)); if (!keys.isLowIncome(matchRegime)) { targetIncomeDifference /= Math.abs(targetNormalisedOriginalIncome); targetIncomeDifference *= 100; From 176777269353bf7348ead2d69bc4b437047eedc2 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:18:41 +0200 Subject: [PATCH 5/6] first complete draft --- .claude/settings.local.json | 16 ++- src/main/java/simpaths/model/Person.java | 3 +- .../model/taxes/DonorTaxImputation.java | 122 +++++++++++------- 3 files changed, 85 insertions(+), 56 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4c04233a5..d808cab04 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,13 +1,15 @@ { "permissions": { "allow": [ - "Bash(git checkout *)", - "Bash(mvn compile *)", - "Bash(cmd /c \"mvn compile -q 2>&1\")", - "Bash(git add *)", - "Bash(git commit -m ' *)", - "Bash(git push *)", - "mcp__github__create_pull_request" + "Bash(xargs -I {} wc -l {})", + "Bash(mvn clean *)", + "Bash(python -c \"import docx; print\\('python-docx available'\\)\")", + "Bash(python3 -c \"import docx; print\\('ok'\\)\")", + "Bash(where py *)", + "Bash(py --version)", + "Bash(py -c \"import docx; print\\('ok'\\)\")", + "Bash(py -m pip install python-docx -q)", + "Bash(py generate_review.py)" ] } } diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index 3f895c713..fb66cab17 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -1466,8 +1466,9 @@ protected void evaluateSocialCareReceipt() { double recCareInnov = statInnovations.getDoubleDraw(7); double probNeedCare = Parameters.getRegNeedCareS2a().getProbability(this, Person.DoublesVariables.class); if (recCareInnov < probNeedCare) { - // need care careNeedFlag = Indicator.True; + } else { + careNeedFlag = Indicator.False; } double probRecCare = Parameters.getRegReceiveCareS2b().getProbability(this, Person.DoublesVariables.class); diff --git a/src/main/java/simpaths/model/taxes/DonorTaxImputation.java b/src/main/java/simpaths/model/taxes/DonorTaxImputation.java index 2782f4861..93a13ee91 100644 --- a/src/main/java/simpaths/model/taxes/DonorTaxImputation.java +++ b/src/main/java/simpaths/model/taxes/DonorTaxImputation.java @@ -196,15 +196,16 @@ public void evaluate() { lowerOrigInc = testOrigInc; } } - if (Math.abs(targetNormalisedOriginalIncome-lowerOrigInc) < Math.abs(targetNormalisedOriginalIncome-upperOrigInc)) { - iiTarget = lowerInd; - } else { + if (Math.abs(upperOrigInc-targetNormalisedOriginalIncome) < Math.abs(lowerOrigInc-targetNormalisedOriginalIncome)) { iiTarget = upperInd; + } else { + iiTarget = lowerInd; } } + // note that actual donor is identified by focussed search below DonorTaxUnit targetCandidate = Parameters.getDonorPool().get(candidatePool.get(iiTarget)); donorID = targetCandidate.getId(); - double targetIncomeDifference = Math.min(Math.abs(targetNormalisedOriginalIncome - lowerOrigInc), Math.abs(targetNormalisedOriginalIncome - upperOrigInc)); + double targetIncomeDifference = Math.min(Math.abs(targetNormalisedOriginalIncome - upperOrigInc), Math.abs(targetNormalisedOriginalIncome - lowerOrigInc)); if (!keys.isLowIncome(matchRegime)) { targetIncomeDifference /= Math.abs(targetNormalisedOriginalIncome); targetIncomeDifference *= 100; @@ -216,16 +217,15 @@ public void evaluate() { // focussed search around target //------------------------------------------------------------ List candidatesList = new ArrayList<>(); - int bracketPts = (!flagChildcareCost && !flagSecondIncome) ? 2 : 60; - double oi = keys.getOriginalIncomePerWeek(); - double si = keys.getSecondIncomePerWeek(); - double cc = keys.getChildcareCostPerWeek(); - double[] targetVector = getMeasurementVector(keys.getPriceYear(), systemYear, oi, flagSecondIncome, si, flagChildcareCost, cc); + int bracketPts = (!flagChildcareCost && !flagSecondIncome) ? 2 : 50; + double[] targetVector = getMeasurementVector(Parameters.BASE_PRICE_YEAR, keys.getPriceYear(), keys.getSimYear(), keys.getSimYear(), + keys.getOriginalIncomePerWeek(), flagSecondIncome, keys.getSecondIncomePerWeek(), flagChildcareCost, keys.getChildcareCostPerWeek()); + int targetWagesYear = (Parameters.taxDonorUpratingByWage) ? keys.getSimYear() : systemYear; for (int increment=-1; increment<2; increment=increment+2) { // search backward and then forward through candidate list // initialise directional search - int bracketInd = 0; + int bracketInd = 0; // number of consecutive points within bracket double localMin = 999.0; double bracketDist = -999.0; int ii; @@ -237,17 +237,20 @@ public void evaluate() { while (ii>=0 && ii 1.0E-4) { bracketDist = distance; bracketInd++; } if (distance < localMin) { + // new minimum found: reset bracket bracketInd = 0; localMin = distance; } if (bracketInd <= bracketPts) { + // add candidate to list candidatesList.add(new CandidateList(candidate, candidate.getWeight(), distance)); } else { break; @@ -257,7 +260,7 @@ public void evaluate() { } // select subset of preferred candidates - Collections.sort(candidatesList, new CandidateListComparator()); + candidatesList.sort(new CandidateListComparator()); int candidateLast = 0; int bracketInd = 0; double bracketDist = -999.0; @@ -304,7 +307,11 @@ public void evaluate() { setReceivedUC(0); setReceivedLegacyBenefit(0); if (systemYear != keys.getPriceYear()) - infAdj = Parameters.getTimeSeriesIndex(keys.getPriceYear(), UpratingCase.TaxDonor) / Parameters.getTimeSeriesIndex(systemYear, UpratingCase.TaxDonor); + infAdj = Parameters.getTimeSeriesValue(keys.getPriceYear(), TimeSeriesVariable.Inflation) / + Parameters.getTimeSeriesValue(systemYear, TimeSeriesVariable.Inflation); + if (systemYear != keys.getSimYear() && Parameters.taxDonorUpratingByWage) + infAdj = infAdj * Parameters.getTimeSeriesValue(keys.getSimYear(), TimeSeriesVariable.WageGrowth) / + Parameters.getTimeSeriesValue(systemYear, TimeSeriesVariable.WageGrowth); for (CandidateList candidateList : candidatesList) { // loop over each preferred candidate @@ -342,30 +349,26 @@ public void evaluate() { } if (Math.abs(disposableIncomePerWeek+999.0)<1.0E-5) { // Deterministic fallback: use nearest neighbour directly. - DonorTaxUnit fallback = (targetCandidate != null) ? targetCandidate : - (!candidatesList.isEmpty() ? candidatesList.get(0).getCandidate() : null); - if (fallback != null) { - donorID = fallback.getId(); - if (keys.isLowIncome(matchRegime)) { - disposableIncomePerWeek = fallback.getPolicyBySystemYear(systemYear).getDisposableIncomePerMonth() - / Parameters.WEEKS_PER_MONTH * infAdj; - benefitsReceivedPerWeek = (fallback.getPolicyBySystemYear(systemYear).getBenMeansTestPerMonth() - + fallback.getPolicyBySystemYear(systemYear).getBenNonMeansTestPerMonth()) - / Parameters.WEEKS_PER_MONTH * infAdj; + donorID = targetCandidate.getId(); + if (keys.isLowIncome(matchRegime)) { + disposableIncomePerWeek = targetCandidate.getPolicyBySystemYear(systemYear).getDisposableIncomePerMonth() + / Parameters.WEEKS_PER_MONTH * infAdj; + benefitsReceivedPerWeek = (targetCandidate.getPolicyBySystemYear(systemYear).getBenMeansTestPerMonth() + + targetCandidate.getPolicyBySystemYear(systemYear).getBenNonMeansTestPerMonth()) + / Parameters.WEEKS_PER_MONTH * infAdj; + } else { + double origIncMonth = targetCandidate.getPolicyBySystemYear(systemYear).getOriginalIncomePerMonth(); + if (Math.abs(origIncMonth) > 1.0E-9) { + disposableIncomePerWeek = targetCandidate.getPolicyBySystemYear(systemYear).getDisposableIncomePerMonth() / origIncMonth; + benefitsReceivedPerWeek = (targetCandidate.getPolicyBySystemYear(systemYear).getBenMeansTestPerMonth() + + targetCandidate.getPolicyBySystemYear(systemYear).getBenNonMeansTestPerMonth()) / origIncMonth; } else { - double origIncMonth = fallback.getPolicyBySystemYear(systemYear).getOriginalIncomePerMonth(); - if (Math.abs(origIncMonth) > 1.0E-9) { - disposableIncomePerWeek = fallback.getPolicyBySystemYear(systemYear).getDisposableIncomePerMonth() / origIncMonth; - benefitsReceivedPerWeek = (fallback.getPolicyBySystemYear(systemYear).getBenMeansTestPerMonth() - + fallback.getPolicyBySystemYear(systemYear).getBenNonMeansTestPerMonth()) / origIncMonth; - } else { - disposableIncomePerWeek = 0.0; - benefitsReceivedPerWeek = 0.0; - } + disposableIncomePerWeek = 0.0; + benefitsReceivedPerWeek = 0.0; } - setReceivedUC(fallback.getPolicyBySystemYear(systemYear).getReceivesUC()); - setReceivedLegacyBenefit(fallback.getPolicyBySystemYear(systemYear).getReceivesLegacyBenefit()); } + setReceivedUC(targetCandidate.getPolicyBySystemYear(systemYear).getReceivesUC()); + setReceivedLegacyBenefit(targetCandidate.getPolicyBySystemYear(systemYear).getReceivesLegacyBenefit()); } if (Math.abs(disposableIncomePerWeek+999.0)<1.0E-5) throw new RuntimeException("Failed to populate disposable income and benefits from donor with inner key value " + keys.getKey(0)); @@ -435,36 +438,59 @@ public static int getCounterVal(MatchFeature targetFeature, int regime, int inde throw new RuntimeException("attempt to evaluate index of unrecognised target feature"); } - private double[] getMeasurementVector(int priceYear, int systemYear, double originalIncomePerWeek, boolean flagSecondIncome, + /** + * Method to package array of metrics used to compare target against potential donors + * + * @param targetPriceYear inflation year that monetary variables should be measured in + * @param currentPriceYear inflation year that monetary variables currently measured in + * @param targetWagesYear wage growth year that monetary variables should be measured in + * @param currentWagesYear wage growth year that monetary variables currently measured in + * @param originalIncomePerWeek current original income (per week) + * @param flagSecondIncome if true, second income is also measured + * @param secondIncomePerWeek second income (per week, if measured) + * @param flagChildcareCost if true, childcare cost is also measured + * @param childcareCostPerWeek childcare cost (per week, if measured) + * @return + */ + private double[] getMeasurementVector(int targetPriceYear, int currentPriceYear, + int targetWagesYear, int currentWagesYear, + double originalIncomePerWeek, boolean flagSecondIncome, double secondIncomePerWeek, boolean flagChildcareCost, double childcareCostPerWeek) { - double wageScale = Parameters.taxDonorUpratingByWage - ? Parameters.getTimeSeriesValue(systemYear, TimeSeriesVariable.WageGrowth) - : 1.0; - double oiAdj = Parameters.normaliseWeeklyIncome(priceYear, originalIncomePerWeek * wageScale); - double siAdj = Parameters.normaliseWeeklyIncome(priceYear, secondIncomePerWeek * wageScale); - double ccAdj = Parameters.normaliseWeeklyIncome(priceYear, childcareCostPerWeek * wageScale); + double infAdj = 1.0, wageAdj = 1.0; + if (currentPriceYear != targetPriceYear) + infAdj = Parameters.getTimeSeriesValue(targetPriceYear, TimeSeriesVariable.Inflation) / + Parameters.getTimeSeriesValue(currentPriceYear, TimeSeriesVariable.Inflation); + if (currentWagesYear != targetWagesYear) + wageAdj = Parameters.getTimeSeriesValue(targetWagesYear, TimeSeriesVariable.WageGrowth) / + Parameters.getTimeSeriesValue(currentWagesYear, TimeSeriesVariable.WageGrowth); + + double oiAdj = originalIncomePerWeek * infAdj * wageAdj; + double siAdj = secondIncomePerWeek * infAdj * wageAdj; + double ccAdj = childcareCostPerWeek * infAdj * wageAdj; if (!flagSecondIncome && !flagChildcareCost) { return new double[] {oiAdj}; } else if (flagSecondIncome && !flagChildcareCost) { return new double[] {oiAdj, siAdj}; - } else if (!flagSecondIncome && flagChildcareCost) { + } else if (!flagSecondIncome) { return new double[]{oiAdj, ccAdj}; } else { return new double[]{oiAdj, siAdj, ccAdj}; } } - private double[] getCandidateMeasVector(DonorTaxUnit candidate, boolean flagSecondIncome, boolean flagChildcareCost) { + private double[] getCandidateMeasVector(int targetPriceYear, int currentPriceYear, + int targetWagesYear, int currentWagesYear, + DonorTaxUnit candidate, boolean flagSecondIncome, boolean flagChildcareCost) { - int priceYear = Parameters.BASE_PRICE_YEAR; - double oiWeekly = candidate.getPolicyBySystemYear(priceYear).getOriginalIncomePerMonth() / Parameters.WEEKS_PER_MONTH; + double oiWeekly = candidate.getPolicyBySystemYear(currentPriceYear).getOriginalIncomePerMonth() / Parameters.WEEKS_PER_MONTH; double siWeekly = 0.0; if (flagSecondIncome) - siWeekly = candidate.getPolicyBySystemYear(priceYear).getSecondIncomePerMonth() / Parameters.WEEKS_PER_MONTH; + siWeekly = candidate.getPolicyBySystemYear(currentPriceYear).getSecondIncomePerMonth() / Parameters.WEEKS_PER_MONTH; double ccWeekly = 0.0; if (flagChildcareCost) - ccWeekly = candidate.getPolicyBySystemYear(priceYear).getChildcareCostPerMonth() / Parameters.WEEKS_PER_MONTH; - return getMeasurementVector(priceYear, oiWeekly, flagSecondIncome, siWeekly, flagChildcareCost, ccWeekly); + ccWeekly = candidate.getPolicyBySystemYear(currentPriceYear).getChildcareCostPerMonth() / Parameters.WEEKS_PER_MONTH; + return getMeasurementVector(targetPriceYear, currentPriceYear, targetWagesYear, currentWagesYear, + oiWeekly, flagSecondIncome, siWeekly, flagChildcareCost, ccWeekly); } private double evaluateDistance(double[] targetVector, double[] candidateVector, boolean flagSecondIncome, boolean flagChildcareCost) { if (flagSecondIncome && flagChildcareCost) { From e81bb8b8df70823b8a1130a85a77d18f50f64c17 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:34:10 +0200 Subject: [PATCH 6/6] finalise code adjustment --- .claude/settings.local.json | 4 +- src/main/java/simpaths/data/Parameters.java | 22 +++++-- .../java/simpaths/model/SimPathsModel.java | 2 +- .../model/taxes/DonorTaxImputation.java | 61 ++++++++----------- .../model/taxes/DonorTaxUnitPolicy.java | 3 + .../simpaths/model/taxes/TestTaxRoutine.java | 28 ++++----- 6 files changed, 64 insertions(+), 56 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d808cab04..d56093d80 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,9 @@ "Bash(py --version)", "Bash(py -c \"import docx; print\\('ok'\\)\")", "Bash(py -m pip install python-docx -q)", - "Bash(py generate_review.py)" + "Bash(py generate_review.py)", + "Bash(pip show *)", + "PowerShell(Get-Command *)" ] } } diff --git a/src/main/java/simpaths/data/Parameters.java b/src/main/java/simpaths/data/Parameters.java index 111ad66a8..4d249ca3b 100644 --- a/src/main/java/simpaths/data/Parameters.java +++ b/src/main/java/simpaths/data/Parameters.java @@ -3060,11 +3060,25 @@ public static MahalanobisDistance getMdDualIncomeChildcare() { public static double normaliseWeeklyIncome(int priceYear, double weeklyFinancial) { return normaliseMonthlyIncome(priceYear, weeklyFinancial * WEEKS_PER_MONTH); } - public static double normaliseMonthlyIncome(int priceYear, double monthlyFinancial) { + public static double normaliseWeeklyIncome(int currentPriceYear, int targetWagesYear, double weeklyFinancial) { + return normaliseMonthlyIncome(currentPriceYear, targetWagesYear, weeklyFinancial * WEEKS_PER_MONTH); + } + public static double normaliseMonthlyIncome(int currentPriceYear, double monthlyFinancial) { + return normaliseMonthlyIncome(currentPriceYear, BASE_PRICE_YEAR, currentPriceYear, currentPriceYear, monthlyFinancial); + } + public static double normaliseMonthlyIncome(int currentPriceYear, int targetWagesYear, double monthlyFinancial) { + return normaliseMonthlyIncome(currentPriceYear, BASE_PRICE_YEAR, currentPriceYear, targetWagesYear, monthlyFinancial); + } + public static double normaliseWeeklyIncome(int currentPriceYear, int targetPriceYear, int currentWagesYear, int targetWagesYear, double weeklyFinancial) { + return normaliseMonthlyIncome(currentPriceYear, targetPriceYear, currentWagesYear, targetWagesYear, weeklyFinancial * WEEKS_PER_MONTH); + } + public static double normaliseMonthlyIncome(int currentPriceYear, int targetPriceYear, int currentWagesYear, int targetWagesYear, double monthlyFinancial) { double infAdj = 1.0; - if (priceYear != BASE_PRICE_YEAR) - infAdj = getTimeSeriesValue(BASE_PRICE_YEAR, TimeSeriesVariable.Inflation) / getTimeSeriesValue(priceYear, TimeSeriesVariable.Inflation); - return Parameters.asinh(monthlyFinancial * infAdj); + if (currentPriceYear != targetPriceYear) + infAdj = getTimeSeriesValue(targetPriceYear, TimeSeriesVariable.Inflation) / getTimeSeriesValue(currentPriceYear, TimeSeriesVariable.Inflation); + if (currentWagesYear != targetWagesYear) + infAdj *= getTimeSeriesValue(targetWagesYear, TimeSeriesVariable.WageGrowth) / getTimeSeriesValue(currentWagesYear, TimeSeriesVariable.WageGrowth); + return asinh(monthlyFinancial * infAdj); } public static void setTrainingFlag(boolean flag) { trainingFlag = flag; diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 2ff12deef..5554683b0 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -204,7 +204,7 @@ public void setFirstRun(boolean firstRun) { @GUIparameter(description = "Average over donor pool when imputing transfer payments") public boolean donorPoolAveraging = true; - @GUIparameter(description = "Scale simulated income by nominal wage growth (instead of price growth) before tax donor matching") + @GUIparameter(description = "Scale simulated income by wage growth when imputing taxes and benefits") public boolean taxDonorUpratingByWage = false; private int ordering = Parameters.MODEL_ORDERING; //Used in Scheduling of model events. Schedule model events at the same time as the collector and observer events, but a lower order, so will be fired before the collector and observer have updated. diff --git a/src/main/java/simpaths/model/taxes/DonorTaxImputation.java b/src/main/java/simpaths/model/taxes/DonorTaxImputation.java index 93a13ee91..2b67b503e 100644 --- a/src/main/java/simpaths/model/taxes/DonorTaxImputation.java +++ b/src/main/java/simpaths/model/taxes/DonorTaxImputation.java @@ -159,17 +159,20 @@ public void evaluate() { int lowerInd, upperInd, testInd; double lowerOrigInc, upperOrigInc, testOrigInc; final double MEAN_BIAS = 0.5; + int targetWagesYear = (Parameters.taxDonorUpratingByWage) ? keys.getSimYear() : systemYear; lowerInd = 0; - lowerOrigInc = Parameters.getDonorPool().get(candidatePool.get(lowerInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(); + lowerOrigInc = Parameters.getDonorPool().get(candidatePool.get(lowerInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(targetWagesYear); upperInd = candidatePool.size()-1; - upperOrigInc = Parameters.getDonorPool().get(candidatePool.get(upperInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(); + upperOrigInc = Parameters.getDonorPool().get(candidatePool.get(upperInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(targetWagesYear); - int iiTarget; - if (targetNormalisedOriginalIncomeupperOrigInc) { + iiNearest = lowerInd; + } else if (targetNormalisedOriginalIncome > upperOrigInc) { iiTarget = upperInd; + iiNearest = upperInd; } else { while (upperInd > lowerInd+1) { @@ -177,13 +180,7 @@ public void evaluate() { double adjFactor = 0.5 * MEAN_BIAS + (targetNormalisedOriginalIncome-lowerOrigInc) / (upperOrigInc - lowerOrigInc) * (1-MEAN_BIAS); int adjInd = (int) ((upperInd - lowerInd) * adjFactor); testInd = lowerInd + Math.max(1, adjInd); - double candidateOrigIncMonthly = Parameters.getDonorPool().get(candidatePool.get(testInd)).getPolicyBySystemYear(systemYear).getOriginalIncomePerMonth(); - if (Parameters.taxDonorUpratingByWage) { - // adjust wage growth to simulated year - candidateOrigIncMonthly = candidateOrigIncMonthly * Parameters.getTimeSeriesValue(keys.getSimYear(), TimeSeriesVariable.WageGrowth) / - Parameters.getTimeSeriesValue(systemYear, TimeSeriesVariable.WageGrowth); - } - testOrigInc = Parameters.normaliseMonthlyIncome(systemYear, candidateOrigIncMonthly); + testOrigInc = Parameters.getDonorPool().get(candidatePool.get(testInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(targetWagesYear); if (testOrigInc > targetNormalisedOriginalIncome) { upperInd = testInd; upperOrigInc = testOrigInc; @@ -196,16 +193,17 @@ public void evaluate() { lowerOrigInc = testOrigInc; } } + iiTarget = upperInd; if (Math.abs(upperOrigInc-targetNormalisedOriginalIncome) < Math.abs(lowerOrigInc-targetNormalisedOriginalIncome)) { - iiTarget = upperInd; + iiNearest = upperInd; } else { - iiTarget = lowerInd; + iiNearest = lowerInd; } } // note that actual donor is identified by focussed search below - DonorTaxUnit targetCandidate = Parameters.getDonorPool().get(candidatePool.get(iiTarget)); + DonorTaxUnit targetCandidate = Parameters.getDonorPool().get(candidatePool.get(iiNearest)); donorID = targetCandidate.getId(); - double targetIncomeDifference = Math.min(Math.abs(targetNormalisedOriginalIncome - upperOrigInc), Math.abs(targetNormalisedOriginalIncome - lowerOrigInc)); + double targetIncomeDifference = Math.abs(targetNormalisedOriginalIncome - targetCandidate.getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(targetWagesYear)); if (!keys.isLowIncome(matchRegime)) { targetIncomeDifference /= Math.abs(targetNormalisedOriginalIncome); targetIncomeDifference *= 100; @@ -218,9 +216,8 @@ public void evaluate() { //------------------------------------------------------------ List candidatesList = new ArrayList<>(); int bracketPts = (!flagChildcareCost && !flagSecondIncome) ? 2 : 50; - double[] targetVector = getMeasurementVector(Parameters.BASE_PRICE_YEAR, keys.getPriceYear(), keys.getSimYear(), keys.getSimYear(), + double[] targetVector = getMeasurementVector(keys.getPriceYear(), Parameters.BASE_PRICE_YEAR, keys.getSimYear(), keys.getSimYear(), keys.getOriginalIncomePerWeek(), flagSecondIncome, keys.getSecondIncomePerWeek(), flagChildcareCost, keys.getChildcareCostPerWeek()); - int targetWagesYear = (Parameters.taxDonorUpratingByWage) ? keys.getSimYear() : systemYear; for (int increment=-1; increment<2; increment=increment+2) { // search backward and then forward through candidate list @@ -237,7 +234,7 @@ public void evaluate() { while (ii>=0 && ii 1.0E-4) { @@ -283,7 +280,7 @@ public void evaluate() { // If focussed search produced no candidates, fall back to nearest neighbour. if (candidatesList.isEmpty()) { - DonorTaxUnit candidate = Parameters.getDonorPool().get(candidatePool.get(iiTarget)); + DonorTaxUnit candidate = Parameters.getDonorPool().get(candidatePool.get(iiNearest)); candidatesList.add(new CandidateList(candidate, candidate.getWeight(), 0.0)); weightSum = candidate.getWeight(); } @@ -452,22 +449,14 @@ public static int getCounterVal(MatchFeature targetFeature, int regime, int inde * @param childcareCostPerWeek childcare cost (per week, if measured) * @return */ - private double[] getMeasurementVector(int targetPriceYear, int currentPriceYear, - int targetWagesYear, int currentWagesYear, + private double[] getMeasurementVector(int currentPriceYear, int targetPriceYear, + int currentWagesYear, int targetWagesYear, double originalIncomePerWeek, boolean flagSecondIncome, double secondIncomePerWeek, boolean flagChildcareCost, double childcareCostPerWeek) { - double infAdj = 1.0, wageAdj = 1.0; - if (currentPriceYear != targetPriceYear) - infAdj = Parameters.getTimeSeriesValue(targetPriceYear, TimeSeriesVariable.Inflation) / - Parameters.getTimeSeriesValue(currentPriceYear, TimeSeriesVariable.Inflation); - if (currentWagesYear != targetWagesYear) - wageAdj = Parameters.getTimeSeriesValue(targetWagesYear, TimeSeriesVariable.WageGrowth) / - Parameters.getTimeSeriesValue(currentWagesYear, TimeSeriesVariable.WageGrowth); - - double oiAdj = originalIncomePerWeek * infAdj * wageAdj; - double siAdj = secondIncomePerWeek * infAdj * wageAdj; - double ccAdj = childcareCostPerWeek * infAdj * wageAdj; + double oiAdj = Parameters.normaliseWeeklyIncome(currentPriceYear, targetPriceYear, currentWagesYear, targetWagesYear, originalIncomePerWeek); + double siAdj = Parameters.normaliseWeeklyIncome(currentPriceYear, targetPriceYear, currentWagesYear, targetWagesYear, secondIncomePerWeek); + double ccAdj = Parameters.normaliseWeeklyIncome(currentPriceYear, targetPriceYear, currentWagesYear, targetWagesYear, childcareCostPerWeek); if (!flagSecondIncome && !flagChildcareCost) { return new double[] {oiAdj}; } else if (flagSecondIncome && !flagChildcareCost) { @@ -478,8 +467,8 @@ private double[] getMeasurementVector(int targetPriceYear, int currentPriceYear, return new double[]{oiAdj, siAdj, ccAdj}; } } - private double[] getCandidateMeasVector(int targetPriceYear, int currentPriceYear, - int targetWagesYear, int currentWagesYear, + private double[] getCandidateMeasVector(int currentPriceYear, int targetPriceYear, + int currentWagesYear, int targetWagesYear, DonorTaxUnit candidate, boolean flagSecondIncome, boolean flagChildcareCost) { double oiWeekly = candidate.getPolicyBySystemYear(currentPriceYear).getOriginalIncomePerMonth() / Parameters.WEEKS_PER_MONTH; @@ -489,7 +478,7 @@ private double[] getCandidateMeasVector(int targetPriceYear, int currentPriceYea double ccWeekly = 0.0; if (flagChildcareCost) ccWeekly = candidate.getPolicyBySystemYear(currentPriceYear).getChildcareCostPerMonth() / Parameters.WEEKS_PER_MONTH; - return getMeasurementVector(targetPriceYear, currentPriceYear, targetWagesYear, currentWagesYear, + return getMeasurementVector(currentPriceYear, targetPriceYear, currentWagesYear, targetWagesYear, oiWeekly, flagSecondIncome, siWeekly, flagChildcareCost, ccWeekly); } private double evaluateDistance(double[] targetVector, double[] candidateVector, boolean flagSecondIncome, boolean flagChildcareCost) { diff --git a/src/main/java/simpaths/model/taxes/DonorTaxUnitPolicy.java b/src/main/java/simpaths/model/taxes/DonorTaxUnitPolicy.java index 359458e77..6c3d33e1e 100644 --- a/src/main/java/simpaths/model/taxes/DonorTaxUnitPolicy.java +++ b/src/main/java/simpaths/model/taxes/DonorTaxUnitPolicy.java @@ -91,6 +91,9 @@ public double getOriginalIncomePerMonth() { throw new RuntimeException("attempt to get original income before instantiated"); return originalIncomePerMonth; } + public double getNormalisedOriginalIncomePerMonth(int targetWagesYear) { + return Parameters.normaliseMonthlyIncome(systemYear, targetWagesYear, getOriginalIncomePerMonth()); + } public double getNormalisedOriginalIncomePerMonth() { return Parameters.normaliseMonthlyIncome(systemYear, getOriginalIncomePerMonth()); } diff --git a/src/main/java/simpaths/model/taxes/TestTaxRoutine.java b/src/main/java/simpaths/model/taxes/TestTaxRoutine.java index c5ccc29cd..6da90a8a3 100644 --- a/src/main/java/simpaths/model/taxes/TestTaxRoutine.java +++ b/src/main/java/simpaths/model/taxes/TestTaxRoutine.java @@ -23,9 +23,9 @@ public static void run() { // disability1/2: 0, 1 // income: <946.17 <=2985.70, >2985.70 - year = 2025; - age = 78; - adults = 2; + year = 2040; + age = 80; + adults = 1; children04 = 0; children59 = 0; children1017 = 0; @@ -33,8 +33,8 @@ public static void run() { hoursWork2 = 0.0; disability1 = 0; disability2 = 0; - careProvision = 1; - originalIncomePerMonth = 0.0; + careProvision = 0; + originalIncomePerMonth = 23.75; secondIncomePerMonth = 0.0; childcarePerMonth = 0.0; evaluatedTransfers = new TaxEvaluation(year, age, adults, children04, children59, children1017, hoursWork1, hoursWork2, disability1, disability2, careProvision, originalIncomePerMonth, secondIncomePerMonth, childcarePerMonth, -2); @@ -43,7 +43,7 @@ public static void run() { disposableIncomePerMonth = evaluatedTransfers.getDisposableIncomePerMonth(); matchRegime = evaluatedTransfers.getImputedTransfers().getMatchCriterion(); - year = 2025; + year = 2040; age = 48; adults = 2; children04 = 1; @@ -63,7 +63,7 @@ public static void run() { disposableIncomePerMonth = evaluatedTransfers.getDisposableIncomePerMonth(); matchRegime = evaluatedTransfers.getImputedTransfers().getMatchCriterion(); - year = 2025; + year = 2040; age = 48; adults = 2; children04 = 0; @@ -73,7 +73,7 @@ public static void run() { hoursWork2 = 0.0; disability1 = 1; disability2 = 0; - originalIncomePerMonth = 0.0; + originalIncomePerMonth = 2100.0; secondIncomePerMonth = 0.0; childcarePerMonth = 0.0; evaluatedTransfers = new TaxEvaluation(year, age, adults, children04, children59, children1017, hoursWork1, hoursWork2, disability1, disability2, careProvision, originalIncomePerMonth, secondIncomePerMonth, childcarePerMonth, -2); @@ -82,7 +82,7 @@ public static void run() { disposableIncomePerMonth = evaluatedTransfers.getDisposableIncomePerMonth(); matchRegime = evaluatedTransfers.getImputedTransfers().getMatchCriterion(); - year = 2025; + year = 2040; age = 75; adults = 1; children04 = 0; @@ -92,7 +92,7 @@ public static void run() { hoursWork2 = 0.0; disability1 = 1; disability2 = 0; - originalIncomePerMonth = 0.0; + originalIncomePerMonth = 200.0; secondIncomePerMonth = 0.0; childcarePerMonth = 0.0; evaluatedTransfers = new TaxEvaluation(year, age, adults, children04, children59, children1017, hoursWork1, hoursWork2, disability1, disability2, careProvision, originalIncomePerMonth, secondIncomePerMonth, childcarePerMonth, -2); @@ -101,7 +101,7 @@ public static void run() { disposableIncomePerMonth = evaluatedTransfers.getDisposableIncomePerMonth(); matchRegime = evaluatedTransfers.getImputedTransfers().getMatchCriterion(); - year = 2025; + year = 2040; age = 57; adults = 1; children04 = 0; @@ -109,7 +109,7 @@ public static void run() { children1017 = 0; hoursWork1 = 20.0; hoursWork2 = 0.0; - disability1 = 1; + disability1 = 0; disability2 = 0; originalIncomePerMonth = 4500.0; secondIncomePerMonth = 0.0; @@ -120,7 +120,7 @@ public static void run() { disposableIncomePerMonth = evaluatedTransfers.getDisposableIncomePerMonth(); matchRegime = evaluatedTransfers.getImputedTransfers().getMatchCriterion(); - year = 2025; + year = 2040; age = 57; adults = 1; children04 = 0; @@ -130,7 +130,7 @@ public static void run() { hoursWork2 = 0.0; disability1 = 1; disability2 = 0; - originalIncomePerMonth = 0.0; + originalIncomePerMonth = 350.0; secondIncomePerMonth = 0.0; childcarePerMonth = 0.0; evaluatedTransfers = new TaxEvaluation(year, age, adults, children04, children59, children1017, hoursWork1, hoursWork2, disability1, disability2, careProvision, originalIncomePerMonth, secondIncomePerMonth, childcarePerMonth, -2);