diff --git a/src/main/java/simpaths/model/FertilityAlignment.java b/src/main/java/simpaths/model/FertilityAlignment.java index 3362d835c..8c57c87e4 100644 --- a/src/main/java/simpaths/model/FertilityAlignment.java +++ b/src/main/java/simpaths/model/FertilityAlignment.java @@ -4,8 +4,6 @@ import simpaths.data.IEvaluation; import simpaths.data.Parameters; import simpaths.data.filters.FertileFilter; -import simpaths.model.enums.Dcpst; -import simpaths.model.enums.TimeSeriesVariable; import java.util.Set; @@ -25,6 +23,8 @@ public class FertilityAlignment implements IEvaluation { private double targetFertilityRate; private Set persons; private SimPathsModel model; + private FertileFilter fertileFilter; + private long numFertile; // CONSTRUCTOR @@ -32,49 +32,36 @@ public FertilityAlignment(Set persons) { this.model = (SimPathsModel) SimulationEngine.getInstance().getManager(SimPathsModel.class.getCanonicalName()); this.persons = persons; targetFertilityRate = Parameters.getFertilityRateByYear(model.getYear()); + fertileFilter = new FertileFilter(); + numFertile = persons.stream() + .filter(person -> fertileFilter.evaluate(person)) + .count(); } /** - * Evaluates the discrepancy between the simulated and target total fertility rates adjusts probabilities if necessary. - * - * This method focuses on the influence of the adjustment parameter 'args[0]' on the difference between the target and - * simulated fertility rates (error). + * Evaluates the discrepancy between the expected (smooth) fertility rate at a candidate adjustment + * and the target rate. Uses sum of probit probabilities rather than stochastic realisations, + * yielding a smooth, deterministic objective for the root search. * - * The error value is returned and serves as the stopping condition in root search routines. - * - * @param args An array of parameters, where args[0] represents the adjustment parameter. - * @return The error in the target aggregate share of partnered persons after potential adjustments. + * @param args An array of parameters, where args[0] represents the probit intercept adjustment. + * @return target fertility rate minus expected fertility rate at the given adjustment. */ @Override public double evaluate(double[] args) { - persons.parallelStream() - .forEach(person -> person.fertility(args[0])); - - return targetFertilityRate - evalFertilityRate(); - } - - - /** - * Evaluates the aggregate share of persons with partners assigned in a test run of union matching among those eligible for partnership. - * - * This method uses Java streams to count the number of persons who meet the age criteria for cohabitation - * and the number of persons who currently have a test partner. The aggregate share is calculated as the - * ratio of successfully partnered persons to those eligible for partnership, with consideration for potential division by zero. - * - * @return The aggregate share of partnered persons among those eligible, or 0.0 if no eligible persons are found. - */ - private double evalFertilityRate() { - - FertileFilter filter = new FertileFilter(); - long numFertilePersons = model.getPersons().stream() - .filter(person -> filter.evaluate(person)) - .count(); - long numBirths = model.getPersons().stream() - .filter(person -> (person.isToGiveBirth())) - .count(); + if (numFertile == 0) return targetFertilityRate; - return (numFertilePersons > 0) ? (double) numBirths / numFertilePersons : 0.0; + double expectedBirths = persons.parallelStream() + .filter(person -> fertileFilter.evaluate(person)) + .mapToDouble(person -> { + double score = Parameters.getRegFertilityF1() + .getScore(person, Person.DoublesVariables.class); + return Parameters.getRegFertilityF1() + .getProbability(score + args[0]); + }) + .sum(); + double expectedRate = expectedBirths / numFertile; + return targetFertilityRate - expectedRate; } } diff --git a/src/main/java/simpaths/model/InSchoolAlignment.java b/src/main/java/simpaths/model/InSchoolAlignment.java index af5fa63e0..88c18a84e 100644 --- a/src/main/java/simpaths/model/InSchoolAlignment.java +++ b/src/main/java/simpaths/model/InSchoolAlignment.java @@ -21,9 +21,15 @@ */ public class InSchoolAlignment implements IEvaluation { - private final double targetStudentShare; - private final Set persons; - private final SimPathsModel model; + private static final int MIN_STUDENT_AGE = Parameters.MIN_AGE_TO_LEAVE_EDUCATION; + private static final int MAX_STUDENT_AGE = Parameters.MAX_AGE_TO_STAY_IN_CONTINUOUS_EDUCATION; + + private double targetStudentShare; + private Set persons; + private SimPathsModel model; + private long numEligible; + private long numDeterministicStudents; + private double baseReEntryExpected; // CONSTRUCTOR @@ -31,61 +37,74 @@ public InSchoolAlignment(Set persons) { this.model = (SimPathsModel) SimulationEngine.getInstance().getManager(SimPathsModel.class.getCanonicalName()); this.persons = persons; targetStudentShare = Parameters.getTargetShare(model.getYear(), TargetShares.Students); + + numEligible = persons.stream() + .filter(person -> person.getLabC4() != null + && person.getDemAge() >= MIN_STUDENT_AGE + && person.getDemAge() <= MAX_STUDENT_AGE) + .count(); + + // Lagged students below minimum quitting age: deterministically remain students + numDeterministicStudents = persons.stream() + .filter(person -> person.getLabC4() != null + && person.getDemAge() >= MIN_STUDENT_AGE + && person.getDemAge() <= MAX_STUDENT_AGE + && Les_c4.Student.equals(person.getLabC4L1()) + && person.getDemAge() < Parameters.MIN_AGE_TO_LEAVE_EDUCATION) + .count(); + + // Non-students who are not retired: E1b probability of re-entering (no adjustment applied) + baseReEntryExpected = persons.parallelStream() + .filter(person -> person.getLabC4() != null + && person.getDemAge() >= MIN_STUDENT_AGE + && person.getDemAge() <= MAX_STUDENT_AGE + && !Les_c4.Student.equals(person.getLabC4L1()) + && !Les_c4.Retired.equals(person.getLabC4L1())) + .mapToDouble(person -> { + double score = Parameters.getRegEducationE1b().getScore(person, Person.DoublesVariables.class); + return Parameters.getRegEducationE1b().getProbability(score); + }) + .sum(); } /** - * Evaluates the discrepancy between the simulated and target total student share and adjusts probabilities if necessary. - * - * This method focuses on the influence of the adjustment parameter 'args[0]' on the difference between the target and - * simulated student share (error). + * Evaluates the discrepancy between the expected (smooth) student share at a candidate adjustment + * and the target share. Uses sum of probit probabilities rather than stochastic realisations, + * yielding a smooth, deterministic objective for the root search. * - * The error value is returned and serves as the stopping condition in root search routines. + * The adjustment only affects the E1a model (continuing students deciding whether to stay). + * The E1b model (re-entry) does not use the adjustment. * - * @param args An array of parameters, where args[0] represents the adjustment parameter. - * @return The error in the target aggregate share of students after potential adjustments. + * @param args An array of parameters, where args[0] represents the probit intercept adjustment. + * @return target student share minus expected student share at the given adjustment. */ @Override public double evaluate(double[] args) { - // Ensure each trial point is evaluated from lagged status (pure function for root search). - persons.parallelStream().forEach(person -> { - if (person.getLabC4L1() != null) { - person.setLabC4(person.getLabC4L1()); - } - person.inSchool(args[0]); - }); + if (numEligible == 0) return targetStudentShare; - return targetStudentShare - evalStudentShare(); + // Lagged students within quitting age range: E1a probability of remaining (adjustment applies) + // Students above MAX_AGE_TO_STAY_IN_CONTINUOUS_EDUCATION deterministically leave → contribute 0 + double expectedContinuing = persons.parallelStream() + .filter(person -> person.getLabC4() != null + && person.getDemAge() >= MIN_STUDENT_AGE + && person.getDemAge() <= MAX_STUDENT_AGE + && Les_c4.Student.equals(person.getLabC4L1()) + && person.getDemAge() >= Parameters.MIN_AGE_TO_LEAVE_EDUCATION + && person.getDemAge() <= Parameters.MAX_AGE_TO_STAY_IN_CONTINUOUS_EDUCATION) + .mapToDouble(person -> { + double score = Parameters.getRegEducationE1a().getScore(person, Person.DoublesVariables.class); + return Parameters.getRegEducationE1a().getProbability(score + args[0]); + }) + .sum(); + + double expectedStudents = numDeterministicStudents + expectedContinuing + baseReEntryExpected; + double expectedStudentShare = expectedStudents / numEligible; + return targetStudentShare - expectedStudentShare; } public double getTargetStudentShare() { return targetStudentShare; } - - /** - * Evaluates the aggregate share of students. - * - * This method uses Java streams to count the number of students over the total number of individuals. - * - * @return The aggregate share of partnered persons among those eligible, or 0.0 if no eligible persons are found. - */ - private double evalStudentShare() { - - // Counts aligned students within education age range: 16-29 (range is defined in Model) - long numStudents = model.getPersons().stream() - .filter(person -> person.getDemAge() >= Parameters.MIN_AGE_TO_LEAVE_EDUCATION - && person.getDemAge() <= Parameters.MAX_AGE_TO_STAY_IN_CONTINUOUS_EDUCATION - && !person.isToLeaveSchool() - && Les_c4.Student.equals(person.getLabC4())) // count aligned student group only - .count(); - // Counts individuals within education age range: 16-29 (range is defined in Model) - long numPeople = model.getPersons().stream() - .filter(person -> person.getDemAge() >= Parameters.MIN_AGE_TO_LEAVE_EDUCATION - && person.getDemAge() <= Parameters.MAX_AGE_TO_STAY_IN_CONTINUOUS_EDUCATION - && person.getLabC4() != null) - .count(); - - return (numStudents > 0) ? (double) numStudents / numPeople : 0.0; - } } diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index b93f938c2..f376bc08a 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -1754,8 +1754,10 @@ public boolean inSchool(double probitAdjustment) { // No --> Process E1b else { + // The InSchool alignment adjustment is applied to E1a (continuing students) only, + // consistent with the student-share target definition. E1b (re-entry) uses the raw score. double score = Parameters.getRegEducationE1b().getScore(this, Person.DoublesVariables.class); - double prob = Parameters.getRegEducationE1b().getProbability(score + probitAdjustment); + double prob = Parameters.getRegEducationE1b().getProbability(score); if (labourInnov < prob) { // Become a student *OUTCOME E* diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 173cbdd89..8f427056e 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -522,11 +522,11 @@ public void buildSchedule() { yearlySchedule.addCollectionEvent(persons, Person.Processes.ConsiderRetirement, false); // EDUCATION MODULE + // In School alignment — runs before InSchool decisions so the solved adjustment applies in the same year + yearlySchedule.addEvent(this, Processes.InSchoolAlignment); + // Check In School - check whether still in education, and if leaving school, reset Education Level yearlySchedule.addCollectionEvent(persons, Person.Processes.InSchool); - - // In School alignment - yearlySchedule.addEvent(this, Processes.InSchoolAlignment); yearlySchedule.addCollectionEvent(persons, Person.Processes.LeavingSchool); // Align the level of education if required @@ -2319,11 +2319,10 @@ private void inSchoolAlignment() { } lastInSchoolAdjustment = search.getTarget()[0]; - // update and exit - if (search.isTargetAltered()) { - Parameters.putTimeSeriesValue(getYear(), search.getTarget()[0], TimeSeriesVariable.InSchoolAdjustment); // If adjustment is altered from the initial value, update the map - System.out.println("InSchool adjustment value was " + search.getTarget()[0]); - } + // Always persist the solved value for the current year so the InSchool process (which runs next in the + // schedule and reads timeseries[year] via getInSchoolAdjustment) applies exactly this adjustment. + Parameters.putTimeSeriesValue(getYear(), search.getTarget()[0], TimeSeriesVariable.InSchoolAdjustment); + System.out.println("InSchool adjustment value was " + search.getTarget()[0]); }