diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..d56093d80 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "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)", + "Bash(pip show *)", + "PowerShell(Get-Command *)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..43891ca9c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# 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. + +## 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/config/default.yml b/config/default.yml index 864ff0d2b..785e37cba 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 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/input/EUROMODoutput/training/DatabaseCountryYear.xlsx b/input/EUROMODoutput/training/DatabaseCountryYear.xlsx index f6173d4ad..71e701416 100644 Binary files a/input/EUROMODoutput/training/DatabaseCountryYear.xlsx and b/input/EUROMODoutput/training/DatabaseCountryYear.xlsx differ diff --git a/input/EUROMODpolicySchedule.xlsx b/input/EUROMODpolicySchedule.xlsx index e19a0ca8d..2b162e6f2 100644 Binary files a/input/EUROMODpolicySchedule.xlsx and b/input/EUROMODpolicySchedule.xlsx differ diff --git a/src/main/java/simpaths/data/Parameters.java b/src/main/java/simpaths/data/Parameters.java index 114eb468c..4d249ca3b 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..2b67b503e 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.*; @@ -153,21 +154,25 @@ public void evaluate() { // The candidate pool is organised in increasing order of original income // ordering is controlled by database query in SimPathsModel.populateTaxdbReferences // normalised income is monthly in BASE_PRICE_YEAR prices (same as EUROMOD) + // Search here uses the golden search algorithm targetNormalisedOriginalIncome = Parameters.normaliseWeeklyIncome(keys.getPriceYear(), keys.getOriginalIncomePerWeek()); 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) { @@ -175,8 +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); - testOrigInc = Parameters.getDonorPool().get(candidatePool.get(testInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(); - + testOrigInc = Parameters.getDonorPool().get(candidatePool.get(testInd)).getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(targetWagesYear); if (testOrigInc > targetNormalisedOriginalIncome) { upperInd = testInd; upperOrigInc = testOrigInc; @@ -190,11 +194,16 @@ public void evaluate() { } } iiTarget = upperInd; + if (Math.abs(upperOrigInc-targetNormalisedOriginalIncome) < Math.abs(lowerOrigInc-targetNormalisedOriginalIncome)) { + iiNearest = upperInd; + } else { + iiNearest = lowerInd; + } } - DonorTaxUnit targetCandidate = Parameters.getDonorPool().get(candidatePool.get(iiTarget)); + // note that actual donor is identified by focussed search below + DonorTaxUnit targetCandidate = Parameters.getDonorPool().get(candidatePool.get(iiNearest)); donorID = targetCandidate.getId(); - double targetIncomeDifference = Math.abs(targetNormalisedOriginalIncome - - targetCandidate.getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth()); + double targetIncomeDifference = Math.abs(targetNormalisedOriginalIncome - targetCandidate.getPolicyBySystemYear(systemYear).getNormalisedOriginalIncomePerMonth(targetWagesYear)); if (!keys.isLowIncome(matchRegime)) { targetIncomeDifference /= Math.abs(targetNormalisedOriginalIncome); targetIncomeDifference *= 100; @@ -206,16 +215,14 @@ 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(), oi, flagSecondIncome, si, flagChildcareCost, cc); + int bracketPts = (!flagChildcareCost && !flagSecondIncome) ? 2 : 50; + double[] targetVector = getMeasurementVector(keys.getPriceYear(), Parameters.BASE_PRICE_YEAR, keys.getSimYear(), keys.getSimYear(), + keys.getOriginalIncomePerWeek(), flagSecondIncome, keys.getSecondIncomePerWeek(), flagChildcareCost, keys.getChildcareCostPerWeek()); 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; @@ -227,17 +234,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; @@ -247,7 +257,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; @@ -270,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(); } @@ -294,7 +304,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 @@ -332,30 +346,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)); @@ -425,33 +435,51 @@ 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) { - - double oiAdj = Parameters.normaliseWeeklyIncome(priceYear, originalIncomePerWeek); - double siAdj = Parameters.normaliseWeeklyIncome(priceYear, secondIncomePerWeek); - double ccAdj = Parameters.normaliseWeeklyIncome(priceYear, childcareCostPerWeek); + /** + * 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 currentPriceYear, int targetPriceYear, + int currentWagesYear, int targetWagesYear, + double originalIncomePerWeek, boolean flagSecondIncome, + double secondIncomePerWeek, boolean flagChildcareCost, double childcareCostPerWeek) { + + 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) { 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 currentPriceYear, int targetPriceYear, + int currentWagesYear, int targetWagesYear, + 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(currentPriceYear, targetPriceYear, currentWagesYear, targetWagesYear, + oiWeekly, flagSecondIncome, siWeekly, flagChildcareCost, ccWeekly); } private double evaluateDistance(double[] targetVector, double[] candidateVector, boolean flagSecondIncome, boolean flagChildcareCost) { if (flagSecondIncome && 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); 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) {