Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 23 additions & 36 deletions src/main/java/simpaths/model/FertilityAlignment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -25,56 +23,45 @@ public class FertilityAlignment implements IEvaluation {
private double targetFertilityRate;
private Set<Person> persons;
private SimPathsModel model;
private FertileFilter fertileFilter;
private long numFertile;


// CONSTRUCTOR
public FertilityAlignment(Set<Person> 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;
}
}
107 changes: 63 additions & 44 deletions src/main/java/simpaths/model/InSchoolAlignment.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,71 +21,90 @@
*/
public class InSchoolAlignment implements IEvaluation {

private final double targetStudentShare;
private final Set<Person> 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<Person> persons;
private SimPathsModel model;
private long numEligible;
private long numDeterministicStudents;
private double baseReEntryExpected;
Comment on lines +30 to +32


// CONSTRUCTOR
public InSchoolAlignment(Set<Person> 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();
Comment on lines +47 to +54

// 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;
Comment on lines +102 to +103

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last Copilot comments are not critical; rather, they are misleading: they flag that numDeterministicStudents is dead code because its filter (demAge >= 16 && demAge < 16) can never match, but we may change the parameters for 16y.o. in the future, so it is better to have this condition in place.

I will proceed with merging this PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree - I hadn't flagged them as well.

return targetStudentShare - expectedStudentShare;
Comment on lines 83 to +104

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intended, @Mariia-Var?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks you @dav-sonn,
I have fixed the issue - needed to re-order inSchool alignment in the BuildSchedule.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dav-sonn, could you please do re-review with Copilot?
(I can't do it myself without a pro subscription yet..)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below is the new review.

}

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;
}
}
4 changes: 3 additions & 1 deletion src/main/java/simpaths/model/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
15 changes: 7 additions & 8 deletions src/main/java/simpaths/model/SimPathsModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]);
}


Expand Down
Loading