From 132f9a3e26a7443a831f6adea164c62cc0762570 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:14:31 +0200 Subject: [PATCH 1/9] minor --- .gitignore | 2 + input/DatabaseCountryYear.xlsx | Bin 3339 -> 3340 bytes input/EUROMODpolicySchedule.xlsx | Bin 8436 -> 8444 bytes .../training/population_initial_UK_2019.csv | 75317 ++++++++-------- .../experiment/SimPathsCollector.java | 8 + .../simpaths/experiment/SimPathsStart.java | 2 +- src/main/java/simpaths/model/BenefitUnit.java | 71 +- .../java/simpaths/model/annotations/Lag.java | 64 + .../model/annotations/NullInitialised.java | 35 + .../model/annotations/UpdateManager.java | 185 + 10 files changed, 38158 insertions(+), 37526 deletions(-) create mode 100644 src/main/java/simpaths/model/annotations/Lag.java create mode 100644 src/main/java/simpaths/model/annotations/NullInitialised.java create mode 100644 src/main/java/simpaths/model/annotations/UpdateManager.java diff --git a/.gitignore b/.gitignore index a558e809f..eb1c1949a 100644 --- a/.gitignore +++ b/.gitignore @@ -246,3 +246,5 @@ input/EUROMODoutput/baseline/* # IntelliJ .idea/ + +input/InitialPopulations/original/* diff --git a/input/DatabaseCountryYear.xlsx b/input/DatabaseCountryYear.xlsx index e2f6b353138fc4e0afdac02d51cb82064e9d4ef7..eaeed84eab4bebe111622bcd1e4f3cb36a1313ce 100644 GIT binary patch delta 754 zcmeB{>XG6N@MdNaVc_84V7S!UyOFn&kr_yD?qrk%GbS%(ItLagV|IiH>|#-`H{?EK zAi(+{_JjSDH)fs(7fo8y{NPa*`|%6&%r2;yPVf3xy;!#8(#L(@D+(4>-L$KAa(Fyf z#U@>a`TCD*J9evleY)l5M6b2sOonN_^08BvJXmRB^;zPfg{S8xlg9y*^o~7r&@_{c z=-nxQVCj=*%~uWQKJ+W(SP=b&VbR;ngAtz2Yt8yJ8r?cq)J=YY>F2`gK4w=d8d3fYOb|Je{0-*}X@|=8&N1JUs7f28Q D@;Wl& delta 728 zcmeB?>XzaS@MdNaVc_84U^pC^ypgw(kr_yD?qrk%GbS%(ItLagV|IiH>|#-`-^g{y zK!W8#><3w=ve{b_0zEG|78Gu6PR^*$UErDZFXYSH-ZBBNNT+%!C@PLGwY((!& z`2$OzJ!`&dc(fqhg5`p48NX_r;a7x-(PTYJTT*qU{loJ$?uP4#?94Ocs4Oef5P3@%3qf6kl|rwv4uB|$V~A(|6_{#N~=Pb!e`rx_2yX|m75%B zB&oXj*^@&V=XCXZuH*#lio5<{QEcuS@oJSrefk@B#CruQ3hijfTbl9XiEzfkMH{`j zzVH4PA^a^pcgL+SH~)YG@WIxv%fj{{$wd8S-W3w?R>ztI$=fbrysv(Em+l3;WO<{v2Dh? zAAzpgYp7*@J_U`xXZ;D0cC;!IkEG^o!>#5i5X^Bg6b<^#2 zYhP`3ePFdqXnOYcfK`<)9%-y=JLX*FeLAb=>LMkt`uZ-}NxchHrn4?xr6u@n59=z9 z$9Y2D(~fA}+%%~u=%U0*6}P$9Uh9{h+!Xw4RY$-}cGaYuie*N^YXWyoH%!txI&Z(n z(o^ZLRve4iywfpy#_2P^OvkPnu86Kz;m&at9SZn z|N3>O>N=mEJQ#lcd8H>q-@@eoQkU|JK5++lvvaU@`q?aCWMDYR&cJ{Y-J44}4>5t` z+luEX8<@c)EX@pND2N$AqWPMX%I0$M7_jK%Yf{?vx%smV1pe7&{y+HXdrSGJMH94I zLtnRtUfnhQe4pV#tz&1*Jg;T`uWg?kFsm#yROEr)`MbsaJB@YpzAbW!US=}Av}y0g zZo$U zzjEFK`^+iJHomX-bBlb|zWBoYHEydtrF!iur++Ll>kTV>_OG=f@T%3prG{tk6q@MH zRH)LC4r31TQ0-t0&y~5W;Z~&W6w>#8*G6TI_5D1>U&Z($MdobsxfHhHmW+qU>W;>z zuP2%RI->R>`UOki_gv#eK_(6xpNX39*L~S}%x+$~PgAh!R-H2v^+x@(=IQ4@J0s&% zz3lnxYT;h-LrmNEyne4d^}pY`70*A(pKL0hbksJoVS4u*XLl=h%`laUr)M`aSf6h7 z{c-T7AAimlg{3q<-FaX2RkqwpaXp=92!4HF}l{C$wGZGMV-CxI+4Y zXY+5wXq26r*Sq&Z@89XcbDQOMFX@rk6r-`zd6h8Bp1B*DG8e=*?~mQZoqk(Yc4Pf% z#b>rhFDZ((_#e#S_+|Ud;bUCmeeDnJ2mj4%QLG44tz|vkHp9F+(&>UX2iwJuH@)=d zH>Cf%bp3!%g>Q@T3omE0H{5~JaoHgo5Aj`LQ7)Zfku7~Qf?I0uG&Z-FPo{1-Ipfuy z6Q7sml~^5;+%tts^Xz|z=Kiw88B})kY_r1ySx&ZcL&J( zJNbva9GEAmAk8E#Hd$Xxh9AvMlM@xB8T}^LDyYkYEJ1d?1k{BP>f+?X3fgSR`G6lkrW2*u)CeLP9 zuRrVGY#?#$z1ly1-}mb-C?>aUY)(rT{`#OZB+OlT%Z+KZf4+-#PTaWd&56&|zb1XJ z{5^49e<|y?5G}PITy2{fm`-tTOs(18l)rxVZkBbMZ_f_V%?{r=@aY|08=gM{3EzJsUrin6X zE0ZEuZRTCAp>FCs@m$cA%)S3~w8G|m68Bwl?!M;4gTk;*U2(Plf z>~4Fk?QZmz8*$I>1*aumoOaE~;spPf!?9fK(}EVd9u~{ZyQtJX?S?a3THFD#Q{p4u5x8d_`Vs{ArF0R;E%>Jd~b~*0*yPullD0lly)yQJz(_qi|)H z)_jTI2OiJ)d*Jb%oB!p$t4UVOu}*zZ$XEBE(68#j$N3zerS=6dlMu;}D_Qrh*m^KKjP)XHzD7x*&o$2{prBAI)VY9j*I?6%R~ zB*VQ`$A8k~HHZJzPMNeL@0O@*?+vx$6Ni;2Z+b3X9k@_8c-CpFqX#b>@33aJ%=9t-Y~$;#)**MQ?P`Na z5NFbXP-%bju8DU#1*VGs`gcOO<+^*@%qs479WK8}wM^}ZTt1bq&?5&vm7bKYO%nd1 zThg@R_cqA@O{oPDpSxz?@B8BNSnj;B+QF5RqQuhbIcB>1o}X_1b(!2k*~_25?(J0T zNNkMG`~BNPbl+s{mpcx*Z+bB|WpSU*q3+6a3ze@qc8O}nv2E5lbT8?hP+h^c%eK&80y_2DPC6l1+B^PSfvveZ7fx+dw$HgUlnxi`B)1@>_nT-qO_YOt{9 zdg1rC!DqFve~zwiEee~k%_#oqJLAW*FF8D9nb-36lg|IShQ5hw7JM-9(b1kdwJcm^ z$K5yy&Q0azx3ZLv`p=tfbkuDt&lUMO2{v<7JZ)z6G)e|(U&@#!-@HdIx+3ZG!W){^ zRy;}dBKzjNlKrwnHTB4ZqcJCCgTe$pL?v@xa@fySzx%`Ud)L%tH`brF6SFxQR;$xB zzbR6l$NEh3qq$8Rf(5=cA2#N!{jlcLD^Z=-$p^orat5j^zH(K6Y|QC@;N;A<>j!kI zd|R%+_Hs6R!yPCcmmRY45bqTU<C!hXxTW?^V{?1?Wa@^KGd9Huu?X8+%6=4y z^J<;)tbRfHit6J&w->aT_}u-N#hzv^c&Ti;f%{6ddbuX*@R{>Lu~KlxfM>2om~WN*GZy!i(^D0fVV z^W1iViGg7a8z^@$GKnz2i^0k7WbHtiVY0rQ8JNzHvj)@4ng+UOdBSyrn)1llzVsEHp%!`Dd)a$tulDo8U5PqtOi1oJW#q?z2rCufOE hO?mX$Select simulation start year"; // sizing for GUI - int height = 280, width = 600; + int height = 220, width = 600; Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); if (screenSize.width < 850) { width = (int) (screenSize.width * 0.95); diff --git a/src/main/java/simpaths/model/BenefitUnit.java b/src/main/java/simpaths/model/BenefitUnit.java index 2f704f6f4..cd44fae50 100644 --- a/src/main/java/simpaths/model/BenefitUnit.java +++ b/src/main/java/simpaths/model/BenefitUnit.java @@ -8,6 +8,8 @@ import org.hibernate.annotations.Fetch; import simpaths.data.ManagerRegressions; import simpaths.data.MultiValEvent; +import simpaths.model.annotations.Lag; +import simpaths.model.annotations.UpdateManager; import simpaths.model.enums.*; import org.apache.commons.collections4.keyvalue.MultiKey; import org.apache.commons.collections4.map.LinkedMap; @@ -73,14 +75,16 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar private Integer yBenUCReceivedFlag; private Integer yBenLegacyReceivedFlag; private Double yDispEquivYear; - @Transient private Double yDispEquivYearL1; + //@Lag(field = "yDispEquivYear") @Transient private Double yDispEquivYearL1; + @Lag(getter = "getEquivalisedDisposableIncomeYearly") @Transient private Double yDispEquivYearL1; @Transient private Double yDiffDispEquivPrevYear; private Integer yPvrtyFlag; //1 if at risk of poverty, defined by an equivalisedDisposableIncomeYearly < 60% of median household's - @Transient private Integer yPvrtyFlagL1; - @Transient private Indicator i_demNChild0to2L1; //Lag(1) of d_children_3under; - @Transient private Indicator dem4to12L1; //Lag(1) of d_children_4_12; - @Transient private Integer numberChildren02_lag1; //Lag(1) of the number of children aged 0-2 in the household - @Transient private Integer numberChildrenAll_lag1; //Lag(1) of the number of children of all ages in the household + //@Lag(field = "yPvrtyFlag") @Transient private Integer yPvrtyFlagL1; + @Lag(getter = "getAtRiskOfPoverty") @Transient private Integer yPvrtyFlagL1; + @Lag(getter = "getIndicatorChildren0to3") @Transient private Indicator dem0to3L1; + @Lag(getter = "getIndicatorChildren4to12") @Transient private Indicator dem4to12L1; //Lag(1) of d_children_4_12; + @Lag(getter = "getNumberChildren0to2") @Transient private Integer numberChildren02_lag1; //Lag(1) of the number of children aged 0-2 in the household + @Lag(getter = "getNumberChildrenAll") @Transient private Integer numberChildrenAll_lag1; //Lag(1) of the number of children of all ages in the household private Double xChildCareWeek; private Double xCareWeek; private Integer careProvidedFlag; @@ -88,10 +92,12 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar @Transient private Match demDbMatchTax; @Enumerated(EnumType.STRING) private Region region; //Region of household. Also used in findDonorHouseholdsByLabour method @Enumerated(EnumType.STRING) private Ydses_c5 yHhQuintilesMonthC5; - @Transient private Ydses_c5 yHhQuintilesC5L1; + //@Lag(field = "yHhQuintilesMonthC5") @Transient private Ydses_c5 yHhQuintilesMonthC5L1; + @Lag(getter = "getYdses_c5") @Transient private Ydses_c5 yHhQuintilesMonthC5L1; @Transient private Double i_yNonBenHhGrossAsinh; private Dhhtp_c4 dhhtp_c4; - @Transient private Dhhtp_c4 demCompHhC4L1; + //@Lag(field = "dhhtp_c4") @Transient private Dhhtp_c4 demCompHhC4L1; + @Lag(getter = "getDhhtp_c4") @Transient private Dhhtp_c4 demCompHhC4L1; private String demCreatedByConstructor; @Column(name="wealthPrptyFlag") private Boolean wealthPrptyFlag; // are any of the individuals in the benefit unit a homeowner? True / false @Transient ArrayList> covid19MonthlyStateAndGrossIncomeAndWorkHoursTripleMale = new ArrayList<>(); @@ -199,7 +205,7 @@ public BenefitUnit(Long id, long statSeed) { this.numberChildrenAll_lag1 = 0; this.numberChildren02_lag1 = 0; - this.i_demNChild0to2L1 = Indicator.False; + this.dem0to3L1 = Indicator.False; this.dem4to12L1 = Indicator.False; this.xChildCareWeek = 0.0; this.xCareWeek = 0.0; @@ -296,14 +302,14 @@ public BenefitUnit(BenefitUnit originalBenefitUnit, long benefitUnitInnov, Sampl ); this.numberChildrenAll_lag1 = originalBenefitUnit.numberChildrenAll_lag1; this.numberChildren02_lag1 = originalBenefitUnit.numberChildren02_lag1; - this.i_demNChild0to2L1 = originalBenefitUnit.i_demNChild0to2L1; + this.dem0to3L1 = originalBenefitUnit.dem0to3L1; this.dem4to12L1 = originalBenefitUnit.dem4to12L1; this.xChildCareWeek = originalBenefitUnit.xChildCareWeek; this.xCareWeek = originalBenefitUnit.xCareWeek; this.careProvidedFlag = originalBenefitUnit.careProvidedFlag; this.region = originalBenefitUnit.region; this.yHhQuintilesMonthC5 = originalBenefitUnit.getYdses_c5(); - this.yHhQuintilesC5L1 = originalBenefitUnit.yHhQuintilesC5L1; + this.yHhQuintilesMonthC5L1 = originalBenefitUnit.yHhQuintilesMonthC5L1; this.demCompHhC4L1 = originalBenefitUnit.demCompHhC4L1; this.wealthPrptyFlag = originalBenefitUnit.wealthPrptyFlag; demCreatedByConstructor = Objects.requireNonNullElse(originalBenefitUnit.demCreatedByConstructor,"CopyConstructor"); @@ -321,6 +327,7 @@ public enum Processes { Update, //This updates the household fields, such as number of children of a certain age UpdateOutputVariables, UpdateWealth, + UpdateDemographics, CalculateChangeInEDI, //Calculate change in equivalised disposable income Homeownership, ReceivesBenefits, @@ -342,6 +349,9 @@ public void onEvent(Enum type) { } case UpdateWealth -> { updateWealth(); + } + case UpdateDemographics -> { + } case CalculateChangeInEDI -> { calculateEquivalisedDisposableIncomeYearly(); //Update BU's EDI @@ -370,7 +380,7 @@ protected void initializeFields() { if (getNumberChildrenAll()==0) xChildCareWeek = 0.0; // Transient lagged values are not loaded from DB; initialize them for year-1 regressors. - if (i_demNChild0to2L1 == null) i_demNChild0to2L1 = getIndicatorChildren(0,3); + if (dem0to3L1 == null) dem0to3L1 = getIndicatorChildren(0,3); if (dem4to12L1 == null) dem4to12L1 = getIndicatorChildren(4,12); if (numberChildrenAll_lag1 == null) numberChildrenAll_lag1 = getNumberChildrenAll(); if (numberChildren02_lag1 == null) numberChildren02_lag1 = getNumberChildren(0,2); @@ -386,21 +396,12 @@ protected void initializeFields() { protected void updateAttributes() { + UpdateManager.applyAnnotations(this); + // unit specific variables if (getNumberChildrenAll()==0) xChildCareWeek = 0.0; - // lags - i_demNChild0to2L1 = getIndicatorChildren(0,3); - dem4to12L1 = getIndicatorChildren(4,12); - numberChildrenAll_lag1 = getNumberChildrenAll(); - numberChildren02_lag1 = getNumberChildren(0,2); - demCompHhC4L1 = getDhhtp_c4(); - - yDispEquivYearL1 = getEquivalisedDisposableIncomeYearly(); - yPvrtyFlagL1 = getAtRiskOfPoverty(); - yHhQuintilesC5L1 = getYdses_c5(); - // random draws statInnovations.getNewDoubleDraws(); } @@ -4147,6 +4148,9 @@ public int getNumberChildrenAll() { public int getNumberChildren(int age) { return getNumberChildren(age, age); } + public Integer getNumberChildren0to2() { + return getNumberChildren(0, 2); + } public int getNumberChildren(int minAge, int maxAge) { int nChildren = 0; if (model==null) { @@ -4163,21 +4167,26 @@ public int getNumberChildren(int minAge, int maxAge) { } public Indicator getIndicatorChildren(int minAge, int maxAge) { Indicator flag = Indicator.False; - if (model==null) { - for (int aa=minAge; aa<=maxAge; aa++) { - if (getNumberChildrenByAge(aa) > 0) { - flag = Indicator.True; - break; - } + for (int aa=minAge; aa<=maxAge; aa++) { + if (getNumberChildrenByAge(aa) > 0) { + flag = Indicator.True; + break; } } return flag; } + public Indicator getIndicatorChildren0to3() { + + return getIndicatorChildren(0,3); + } + public Indicator getIndicatorChildren4to12() { + return getIndicatorChildren(4,12); + } public Integer getNumberChildrenAll_lag1() { return numberChildrenAll_lag1; } public Integer getNumberChildren02_lag1() { return numberChildren02_lag1; } - public Indicator getIndicatorChildren03_lag1() { return i_demNChild0to2L1; } + public Indicator getIndicatorChildren03_lag1() { return dem0to3L1; } public Indicator getIndicatorChildren412_lag1() { return dem4to12L1; } @@ -4274,7 +4283,7 @@ public Ydses_c5 getYdses_c5() { } public Ydses_c5 getYdses_c5_lag1() { - return yHhQuintilesC5L1; + return yHhQuintilesMonthC5L1; } public double getTmpHHYpnbihs_dv_asinh() { diff --git a/src/main/java/simpaths/model/annotations/Lag.java b/src/main/java/simpaths/model/annotations/Lag.java new file mode 100644 index 000000000..56d89b945 --- /dev/null +++ b/src/main/java/simpaths/model/annotations/Lag.java @@ -0,0 +1,64 @@ +package simpaths.model.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field as a lagged value to be updated at the start of each simulation time interval. + * + *

At each period update the annotated field will receive the value of its specified source, + * read from the same object before any lag assignments are written. This read-first semantics + * ensures correct behaviour for multi-period lag chains (e.g. L3 ← L2 ← L1 ← current) + * regardless of field declaration order.

+ * + *

Specify the source using exactly one of:

+ *
    + *
  • {@link #field()} — name of a field on the same object to copy directly.
  • + *
  • {@link #getter()} — name of a no-argument getter method on the same object to invoke.
  • + *
+ * + *

If both are specified, {@code getter} takes precedence. If neither is specified, + * {@link UpdateManager#applyAnnotations(Object)} will throw an {@link IllegalArgumentException}.

+ * + *

Example — direct field reference:

+ *
{@code
+ *   @Lag(field = "labC4")
+ *   private Les_c4 labC4L1;
+ * }
+ * + *

Example — getter method:

+ *
{@code
+ *   @Lag(getter = "getHouseholdStatus")
+ *   private Indicator demStatusHhL1;
+ * }
+ * + *

Example — multi-period lag chain (L2 ← L1, then L1 ← current getter):

+ *
{@code
+ *   @Lag(field = "yEmpPersGrossMonthL1")
+ *   private Double yEmpPersGrossMonthL2;
+ *
+ *   @Lag(getter = "getyEmpPersGrossMonth")
+ *   private Double yEmpPersGrossMonthL1;
+ * }
+ * + * @see NullInitialised + * @see UpdateManager + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Lag { + + /** + * Name of the field on the same object whose value should be copied into the annotated field. + * Ignored if {@link #getter()} is also specified. + */ + String field() default ""; + + /** + * Name of the no-argument method on the same object to invoke to obtain the source value. + * Takes precedence over {@link #field()} when both are specified. + */ + String getter() default ""; +} diff --git a/src/main/java/simpaths/model/annotations/NullInitialised.java b/src/main/java/simpaths/model/annotations/NullInitialised.java new file mode 100644 index 000000000..da2b9ef60 --- /dev/null +++ b/src/main/java/simpaths/model/annotations/NullInitialised.java @@ -0,0 +1,35 @@ +package simpaths.model.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field as a current-period value that should be set to {@code null} at the start + * of each simulation time interval. + * + *

Use this annotation on fields whose values are computed fresh within the current period + * and must not carry over from the previous period. When + * {@link UpdateManager#applyAnnotations(Object)} is called, all {@code @NullInitialised} + * fields are cleared after all {@link Lag} assignments have been applied, so a field + * annotated with both will end up {@code null}.

+ * + *

This annotation is only valid on fields of reference (non-primitive) type. Applying it + * to a primitive field will cause {@link UpdateManager} to throw an + * {@link IllegalArgumentException}.

+ * + *

Example:

+ *
{@code
+ *   // Computed each period during LabourMarketAndIncomeUpdate; must be null at period start.
+ *   @NullInitialised
+ *   private Labour labourSupplyWeekly;
+ * }
+ * + * @see Lag + * @see UpdateManager + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface NullInitialised { +} diff --git a/src/main/java/simpaths/model/annotations/UpdateManager.java b/src/main/java/simpaths/model/annotations/UpdateManager.java new file mode 100644 index 000000000..557037945 --- /dev/null +++ b/src/main/java/simpaths/model/annotations/UpdateManager.java @@ -0,0 +1,185 @@ +package simpaths.model.annotations; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Reflection-based processor that applies {@link Lag} and {@link NullInitialised} annotations + * at the start of each simulation time interval. + * + *

Processing order

+ *
    + *
  1. Read phase — all {@link Lag}-annotated fields have their source values read + * (via field reference or getter) and cached before any writes occur.
  2. + *
  3. Lag write phase — all cached lag values are written to their annotated fields.
  4. + *
  5. Null phase — all {@link NullInitialised}-annotated fields are set to + * {@code null}.
  6. + *
+ * + *

Reading all sources before writing any lag field guarantees correct behaviour for + * multi-period lag chains regardless of field declaration order. For example:

+ *
+ *   L3 ← L2  (reads L2 before L2 is overwritten by the L2←L1 assignment)
+ *   L2 ← L1  (reads L1 before L1 is overwritten by the L1←current assignment)
+ *   L1 ← current
+ * 
+ * + *

Typical usage in an entity's update method

+ *
{@code
+ *   // In Person.updateLaggedVariables() or BenefitUnit.updateAttributes():
+ *   UpdateManager.applyAnnotations(this);
+ * }
+ * + * @see Lag + * @see NullInitialised + */ +public final class UpdateManager { + + private UpdateManager() {} + + /** + * Applies all {@link Lag} and {@link NullInitialised} annotations on the given entity, + * walking the full class hierarchy. + * + * @param entity the object whose annotated fields should be updated; must not be {@code null} + * @throws IllegalArgumentException if a {@link Lag} annotation specifies neither + * {@code field} nor {@code getter}, or if {@link NullInitialised} is placed on a + * primitive field + * @throws RuntimeException wrapping any reflection error encountered during processing + */ + public static void applyAnnotations(Object entity) { + if (entity == null) throw new IllegalArgumentException("entity must not be null"); + + Class clazz = entity.getClass(); + List lagAssignments = new ArrayList<>(); + List nullFields = new ArrayList<>(); + + // --- Discovery pass: collect all assignments without writing --- + for (Field field : getAllFields(clazz)) { + + Lag lag = field.getAnnotation(Lag.class); + if (lag != null) { + Object value = resolveSource(entity, clazz, lag, field); + lagAssignments.add(new LagAssignment(field, value)); + } + + if (field.isAnnotationPresent(NullInitialised.class)) { + if (field.getType().isPrimitive()) { + throw new IllegalArgumentException( + "@NullInitialised cannot be applied to primitive field '" + + field.getName() + "' in " + clazz.getName() + + ". Use a wrapper type (e.g. Double instead of double)."); + } + nullFields.add(field); + } + } + + // --- Lag write phase --- + for (LagAssignment assignment : lagAssignments) { + try { + assignment.field().setAccessible(true); + assignment.field().set(entity, assignment.value()); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Failed to write lag field '" + assignment.field().getName() + "'", e); + } + } + + // --- Null phase --- + for (Field field : nullFields) { + try { + field.setAccessible(true); + field.set(entity, null); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Failed to null-initialise field '" + field.getName() + "'", e); + } + } + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + private static Object resolveSource(Object entity, Class clazz, Lag lag, Field lagField) { + if (!lag.getter().isEmpty()) { + return invokeGetter(entity, clazz, lag.getter(), lagField); + } + if (!lag.field().isEmpty()) { + return readField(entity, clazz, lag.field(), lagField); + } + throw new IllegalArgumentException( + "@Lag on field '" + lagField.getName() + "' in " + clazz.getName() + + " must specify either 'field' or 'getter'."); + } + + private static Object invokeGetter(Object entity, Class clazz, String getterName, Field lagField) { + try { + Method method = findMethod(clazz, getterName); + method.setAccessible(true); + return method.invoke(entity); + } catch (Exception e) { + throw new RuntimeException( + "Failed to invoke getter '" + getterName + "' for @Lag field '" + + lagField.getName() + "' in " + clazz.getName(), e); + } + } + + private static Object readField(Object entity, Class clazz, String fieldName, Field lagField) { + try { + Field source = findField(clazz, fieldName); + source.setAccessible(true); + return source.get(entity); + } catch (Exception e) { + throw new RuntimeException( + "Failed to read field '" + fieldName + "' for @Lag field '" + + lagField.getName() + "' in " + clazz.getName(), e); + } + } + + /** Returns all declared fields across the full class hierarchy (class → superclass). */ + private static List getAllFields(Class clazz) { + List fields = new ArrayList<>(); + Class current = clazz; + while (current != null && current != Object.class) { + for (Field f : current.getDeclaredFields()) { + fields.add(f); + } + current = current.getSuperclass(); + } + return fields; + } + + /** Finds a field by name, walking up the class hierarchy. */ + private static Field findField(Class clazz, String name) throws NoSuchFieldException { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(name); + } catch (NoSuchFieldException ignored) { + current = current.getSuperclass(); + } + } + throw new NoSuchFieldException( + "Field '" + name + "' not found in class hierarchy of " + clazz.getName()); + } + + /** Finds a no-argument method by name, walking up the class hierarchy. */ + private static Method findMethod(Class clazz, String name) throws NoSuchMethodException { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredMethod(name); + } catch (NoSuchMethodException ignored) { + current = current.getSuperclass(); + } + } + throw new NoSuchMethodException( + "Method '" + name + "()' not found in class hierarchy of " + clazz.getName()); + } + + /** Immutable pair holding a lag field and its resolved source value. */ + private record LagAssignment(Field field, Object value) {} +} From cca2ffb8a1f3a175f3ca14cf038fbb464ad51bf9 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:32:33 +0200 Subject: [PATCH 2/9] minor --- src/main/java/simpaths/model/BenefitUnit.java | 31 +++++++------------ .../java/simpaths/model/SimPathsModel.java | 2 +- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/main/java/simpaths/model/BenefitUnit.java b/src/main/java/simpaths/model/BenefitUnit.java index cd44fae50..ea0eb6b9b 100644 --- a/src/main/java/simpaths/model/BenefitUnit.java +++ b/src/main/java/simpaths/model/BenefitUnit.java @@ -9,6 +9,7 @@ import simpaths.data.ManagerRegressions; import simpaths.data.MultiValEvent; import simpaths.model.annotations.Lag; +import simpaths.model.annotations.NullInitialised; import simpaths.model.annotations.UpdateManager; import simpaths.model.enums.*; import org.apache.commons.collections4.keyvalue.MultiKey; @@ -61,10 +62,10 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar private Long statSeed; // unit specific variables - @Transient private States labStatesContObject; - private Double yInvestYear; - private Double yPensYear; - private Double xDiscretionaryYear; + @NullInitialised @Transient private States labStatesContObject; + @NullInitialised private Double yInvestYear; + @NullInitialised private Double yPensYear; + @NullInitialised private Double xDiscretionaryYear; @Column(name="wealthTotValue") private Double wealthTotValue; // total net wealth (includes pensions assets and housing) @Column(name="wealthPensValue") private Double wealthPensValue; // total private (personal and occupational) pensions @Column(name="wealthPrptyValue") private Double wealthPrptyValue; // value of main home (gross of mortgage debt) @@ -75,11 +76,9 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar private Integer yBenUCReceivedFlag; private Integer yBenLegacyReceivedFlag; private Double yDispEquivYear; - //@Lag(field = "yDispEquivYear") @Transient private Double yDispEquivYearL1; @Lag(getter = "getEquivalisedDisposableIncomeYearly") @Transient private Double yDispEquivYearL1; @Transient private Double yDiffDispEquivPrevYear; private Integer yPvrtyFlag; //1 if at risk of poverty, defined by an equivalisedDisposableIncomeYearly < 60% of median household's - //@Lag(field = "yPvrtyFlag") @Transient private Integer yPvrtyFlagL1; @Lag(getter = "getAtRiskOfPoverty") @Transient private Integer yPvrtyFlagL1; @Lag(getter = "getIndicatorChildren0to3") @Transient private Indicator dem0to3L1; @Lag(getter = "getIndicatorChildren4to12") @Transient private Indicator dem4to12L1; //Lag(1) of d_children_4_12; @@ -92,12 +91,10 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar @Transient private Match demDbMatchTax; @Enumerated(EnumType.STRING) private Region region; //Region of household. Also used in findDonorHouseholdsByLabour method @Enumerated(EnumType.STRING) private Ydses_c5 yHhQuintilesMonthC5; - //@Lag(field = "yHhQuintilesMonthC5") @Transient private Ydses_c5 yHhQuintilesMonthC5L1; @Lag(getter = "getYdses_c5") @Transient private Ydses_c5 yHhQuintilesMonthC5L1; @Transient private Double i_yNonBenHhGrossAsinh; - private Dhhtp_c4 dhhtp_c4; - //@Lag(field = "dhhtp_c4") @Transient private Dhhtp_c4 demCompHhC4L1; - @Lag(getter = "getDhhtp_c4") @Transient private Dhhtp_c4 demCompHhC4L1; + private Dhhtp_c4 demCompHhC4; + @Lag(getter = "getDemCompHhC4") @Transient private Dhhtp_c4 demCompHhC4L1; private String demCreatedByConstructor; @Column(name="wealthPrptyFlag") private Boolean wealthPrptyFlag; // are any of the individuals in the benefit unit a homeowner? True / false @Transient ArrayList> covid19MonthlyStateAndGrossIncomeAndWorkHoursTripleMale = new ArrayList<>(); @@ -325,7 +322,6 @@ public BenefitUnit(BenefitUnit originalBenefitUnit, long benefitUnitInnov, Sampl public enum Processes { Update, //This updates the household fields, such as number of children of a certain age - UpdateOutputVariables, UpdateWealth, UpdateDemographics, CalculateChangeInEDI, //Calculate change in equivalised disposable income @@ -344,14 +340,11 @@ public void onEvent(Enum type) { updateAttributes(); clearStates(); } - case UpdateOutputVariables -> { - updateOutputVariables(); - } case UpdateWealth -> { updateWealth(); } case UpdateDemographics -> { - + updateDemographics(); } case CalculateChangeInEDI -> { calculateEquivalisedDisposableIncomeYearly(); //Update BU's EDI @@ -384,7 +377,7 @@ protected void initializeFields() { if (dem4to12L1 == null) dem4to12L1 = getIndicatorChildren(4,12); if (numberChildrenAll_lag1 == null) numberChildrenAll_lag1 = getNumberChildrenAll(); if (numberChildren02_lag1 == null) numberChildren02_lag1 = getNumberChildren(0,2); - demCompHhC4L1 = getDhhtp_c4(); + demCompHhC4L1 = getDemCompHhC4(); // clean-up odd ends if (getYdses_c5() == null) { @@ -413,8 +406,8 @@ protected void updateWealth() { /* Contemporaneous values of dhhtp_c4 are required for validation. Update and output here. */ - private void updateOutputVariables() { - dhhtp_c4 = getDhhtp_c4(); + private void updateDemographics() { + demCompHhC4 = getDemCompHhC4(); } @@ -4296,7 +4289,7 @@ public void setTmpHHYpnbihs_dv_asinh(double val) { i_yNonBenHhGrossAsinh = val; } - public Dhhtp_c4 getDhhtp_c4() { + public Dhhtp_c4 getDemCompHhC4() { if (getMale()!=null && getFemale()!=null) { if (getChildren().size()>0) return Dhhtp_c4.CoupleChildren; diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index f67ab3e7d..699c8636d 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -562,6 +562,7 @@ public void buildSchedule() { yearlySchedule.addEvent(this, Processes.FertilityAlignment); //Align to fertility rates implied by projected population statistics. yearlySchedule.addCollectionEvent(persons, Person.Processes.Fertility); yearlySchedule.addCollectionEvent(persons, Person.Processes.GiveBirth, false); //Cannot use read-only collection schedule as newborn children cause concurrent modification exception. Need to specify false in last argument of Collection event. + addCollectionEventToAllYears(benefitUnits, BenefitUnit.Processes.UpdateDemographics); // TIME USE MODULE // Social care @@ -623,7 +624,6 @@ public void buildSchedule() { yearlySchedule.addEvent(this, Processes.CheckForImperfectTaxDBMatches); addEventToAllYears(tests, Tests.Processes.RunTests); //Run tests addCollectionEventToAllYears(persons, Person.Processes.UpdateOutputVariables); // Update idPartner, dhhtp_c4 - addCollectionEventToAllYears(benefitUnits, BenefitUnit.Processes.UpdateOutputVariables); // Update dhhtp_c4 addEventToAllYears(Processes.EndYear); // UPDATE YEAR From 17da42cc1f0389989baf088d8e6460b5bf8e5b43 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:37:19 +0200 Subject: [PATCH 3/9] minor --- .../simpaths/data/statistics/Statistics2.java | 6 +- .../experiment/SimPathsCollector.java | 2 +- .../simpaths/experiment/SimPathsObserver.java | 4 +- src/main/java/simpaths/model/BenefitUnit.java | 80 ++++++------ src/main/java/simpaths/model/Person.java | 116 ++++++++++++++++-- 5 files changed, 147 insertions(+), 61 deletions(-) diff --git a/src/main/java/simpaths/data/statistics/Statistics2.java b/src/main/java/simpaths/data/statistics/Statistics2.java index 08dd069dc..bd7312bba 100644 --- a/src/main/java/simpaths/data/statistics/Statistics2.java +++ b/src/main/java/simpaths/data/statistics/Statistics2.java @@ -644,13 +644,13 @@ else if ((double)person.getLabourSupplyHoursWeekly() > 1.0) invInc[ii] += person.getBenefitUnit().getInvestmentIncomeAnnual() / 12.0 / es; penInc[ii] += person.getBenefitUnit().getPensionIncomeAnnual() / 12.0 / es; - disInc[ii] += person.getBenefitUnit().getDisposableIncomeMonthly() / es; + disInc[ii] += person.getBenefitUnit().getDisposableIncomeMonthlyNoNull() / es; if (person.getBenefitUnit().getInvestmentIncomeAnnual()<0.0) { invLosses[ii] += person.getBenefitUnit().getInvestmentIncomeAnnual() / 12.0 / es; - grossDisInc[ii] += (person.getBenefitUnit().getDisposableIncomeMonthly() - + grossDisInc[ii] += (person.getBenefitUnit().getDisposableIncomeMonthlyNoNull() - person.getBenefitUnit().getInvestmentIncomeAnnual() / 12.0) / es; } else { - grossDisInc[ii] += person.getBenefitUnit().getDisposableIncomeMonthly() / es; + grossDisInc[ii] += person.getBenefitUnit().getDisposableIncomeMonthlyNoNull() / es; } double expenditurePerMonth = person.getBenefitUnit().getDiscretionaryConsumptionPerYear(false) / 12.0 + person.getBenefitUnit().getChildcareCostPerWeek(false) * Parameters.WEEKS_PER_MONTH + diff --git a/src/main/java/simpaths/experiment/SimPathsCollector.java b/src/main/java/simpaths/experiment/SimPathsCollector.java index df73f3de4..e26299224 100644 --- a/src/main/java/simpaths/experiment/SimPathsCollector.java +++ b/src/main/java/simpaths/experiment/SimPathsCollector.java @@ -430,7 +430,7 @@ private class Ydses_c5 implements IDoubleSource { public void update() { //Ydses_c5 - householdsGrossIncomesCS = new CrossSection.Double(model.getBenefitUnits(), BenefitUnit.class, "getTmpHHYpnbihs_dv_asinh", true); //Populate CS + householdsGrossIncomesCS = new CrossSection.Double(model.getBenefitUnits(), BenefitUnit.class, "getTmpHHYpnbihs_dv_asinhNoNull", true); //Populate CS percentileFunctionHouseholdsGrossIncomes = new PercentileArrayFunction(householdsGrossIncomesCS); //Get p50 percentileFunctionHouseholdsGrossIncomes.updateSource(); diff --git a/src/main/java/simpaths/experiment/SimPathsObserver.java b/src/main/java/simpaths/experiment/SimPathsObserver.java index 6c01e473f..276ab8dbe 100644 --- a/src/main/java/simpaths/experiment/SimPathsObserver.java +++ b/src/main/java/simpaths/experiment/SimPathsObserver.java @@ -1527,10 +1527,10 @@ else if(edu.equals(Education.High)) { } for (Gender gender : Gender.values()) { GenderEducationWorkingCSfilter genderEducationWorkingFilter = new GenderEducationWorkingCSfilter(gender, edu); - Weighted_CrossSection.Double DispIncWorkingCS = new Weighted_CrossSection.Double(model.getPersons(), Person.class, "getDisposableIncomeMonthly", true); // Note: these are nominal values for each simulated year + Weighted_CrossSection.Double DispIncWorkingCS = new Weighted_CrossSection.Double(model.getPersons(), Person.class, "getDisposableIncomeMonthlyNoNull", true); // Note: these are nominal values for each simulated year DispIncWorkingCS.setFilter(genderEducationWorkingFilter); GenderEducationCSfilter genderEducationCSfilter = new GenderEducationCSfilter(gender, edu); - Weighted_CrossSection.Double DispIncAllCS = new Weighted_CrossSection.Double(model.getPersons(), Person.class, "getDisposableIncomeMonthly", true); // Note: these are nominal values for each simulated year + Weighted_CrossSection.Double DispIncAllCS = new Weighted_CrossSection.Double(model.getPersons(), Person.class, "getDisposableIncomeMonthlyNoNull", true); // Note: these are nominal values for each simulated year DispIncAllCS.setFilter(genderEducationCSfilter); DispIncByGenderAndEducationPlotter.addSeries("Workers (" + gender.toString() + ", " + edu.toString() + ")", new Weighted_MeanArrayFunction(DispIncWorkingCS), null, colorArrayList.get(colorCounter), false); colorCounter++; diff --git a/src/main/java/simpaths/model/BenefitUnit.java b/src/main/java/simpaths/model/BenefitUnit.java index ea0eb6b9b..ec8e1ad4b 100644 --- a/src/main/java/simpaths/model/BenefitUnit.java +++ b/src/main/java/simpaths/model/BenefitUnit.java @@ -71,10 +71,10 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar @Column(name="wealthPrptyValue") private Double wealthPrptyValue; // value of main home (gross of mortgage debt) @Column(name="wealthMortgageDebtValue") private Double wealthMortgageDebtValue; // value of outstanding mortgage debt private Double yDispMonth; - private Double yGrossMonth; - private Double yBenAmountMonth; - private Integer yBenUCReceivedFlag; - private Integer yBenLegacyReceivedFlag; + @NullInitialised private Double yGrossMonth; + @NullInitialised private Double yBenAmountMonth; + @NullInitialised private Integer yBenUCReceivedFlag; + @NullInitialised private Integer yBenLegacyReceivedFlag; private Double yDispEquivYear; @Lag(getter = "getEquivalisedDisposableIncomeYearly") @Transient private Double yDispEquivYearL1; @Transient private Double yDiffDispEquivPrevYear; @@ -84,16 +84,16 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar @Lag(getter = "getIndicatorChildren4to12") @Transient private Indicator dem4to12L1; //Lag(1) of d_children_4_12; @Lag(getter = "getNumberChildren0to2") @Transient private Integer numberChildren02_lag1; //Lag(1) of the number of children aged 0-2 in the household @Lag(getter = "getNumberChildrenAll") @Transient private Integer numberChildrenAll_lag1; //Lag(1) of the number of children of all ages in the household - private Double xChildCareWeek; - private Double xCareWeek; - private Integer careProvidedFlag; - private Long idtaxDbDonor; - @Transient private Match demDbMatchTax; + @NullInitialised private Double xChildCareWeek; + @NullInitialised private Double xCareWeek; + @NullInitialised private Integer careProvidedFlag; + @NullInitialised private Long idtaxDbDonor; + @NullInitialised @Transient private Match demDbMatchTax; @Enumerated(EnumType.STRING) private Region region; //Region of household. Also used in findDonorHouseholdsByLabour method @Enumerated(EnumType.STRING) private Ydses_c5 yHhQuintilesMonthC5; @Lag(getter = "getYdses_c5") @Transient private Ydses_c5 yHhQuintilesMonthC5L1; - @Transient private Double i_yNonBenHhGrossAsinh; - private Dhhtp_c4 demCompHhC4; + @NullInitialised @Transient private Double i_yNonBenHhGrossAsinh; + @NullInitialised private Dhhtp_c4 demCompHhC4; @Lag(getter = "getDemCompHhC4") @Transient private Dhhtp_c4 demCompHhC4L1; private String demCreatedByConstructor; @Column(name="wealthPrptyFlag") private Boolean wealthPrptyFlag; // are any of the individuals in the benefit unit a homeowner? True / false @@ -104,33 +104,31 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar @Transient private Double labPersistValueLabourInnov; @Transient private Integer lastYear; - @Transient private Integer i_demYear; - @Transient private Occupancy i_demOccupancy; - @Transient private Education i_eduHighestC4; - @Transient private Integer i_labHrsWork1Week; - @Transient private Integer i_labHrsWork2Week; + @NullInitialised @Transient private Integer i_demYear; + @NullInitialised @Transient private Occupancy i_demOccupancy; + @NullInitialised @Transient private Education i_eduHighestC4; + @NullInitialised @Transient private Integer i_labHrsWork1Week; + @NullInitialised @Transient private Integer i_labHrsWork2Week; // ================= At Risk of Work cache to avoid unnecessary atRiskOfWork() calls ================= - @Transient private Boolean cachedMaleAtRiskOfWork = null; - @Transient private Boolean cachedFemaleAtRiskOfWork = null; + @NullInitialised @Transient private Boolean cachedMaleAtRiskOfWork = null; + @NullInitialised @Transient private Boolean cachedFemaleAtRiskOfWork = null; // ================= Labour-choice cache for fast alignment ================= - @Transient private Integer labourChoiceCacheYear = null; + @NullInitialised @Transient private Integer labourChoiceCacheYear = null; // cached discrete choice set - @Transient private LinkedHashSet> cachedPossibleLabourCombinations = null; + @NullInitialised @Transient private LinkedHashSet> cachedPossibleLabourCombinations = null; // cached tax/income outputs by labour pair - @Transient private MultiKeyMap cachedEvalByLabourPairs = - MultiKeyMap.multiKeyMap(new LinkedMap<>()); + @NullInitialised @Transient private MultiKeyMap cachedEvalByLabourPairs = MultiKeyMap.multiKeyMap(new LinkedMap<>()); // cached utility regression scores by labour pair - @Transient private MultiKeyMap cachedUtilityScoreByLabourPairs = - MultiKeyMap.multiKeyMap(new LinkedMap<>()); + @NullInitialised @Transient private MultiKeyMap cachedUtilityScoreByLabourPairs = MultiKeyMap.multiKeyMap(new LinkedMap<>()); // score cache validity markers - @Transient private Integer labourScoreCacheYear = null; - @Transient private Object labourScoreCacheKey = null; + @NullInitialised @Transient private Integer labourScoreCacheYear = null; + @NullInitialised @Transient private Object labourScoreCacheKey = null; // helper – NOT persisted, no annotation needed private static class LabourEval { @@ -338,7 +336,6 @@ public void onEvent(Enum type) { switch ((Processes) type) { case Update -> { updateAttributes(); - clearStates(); } case UpdateWealth -> { updateWealth(); @@ -4185,19 +4182,11 @@ public Indicator getIndicatorChildren412_lag1() { } public double getEquivalisedDisposableIncomeYearly() { - double val; - if (yDispEquivYear != null) { - val = yDispEquivYear; - } else { - val = -9999.99; - } - return val; + return Objects.requireNonNullElse(yDispEquivYear, -9999.99); } public int getAtRiskOfPoverty() { - if (yPvrtyFlag != null) { - return yPvrtyFlag; - } else return 0; + return Objects.requireNonNullElse(yPvrtyFlag, 0); } public Integer getAtRiskOfPoverty_lag1() { @@ -4208,6 +4197,9 @@ public void setAtRiskOfPoverty(Integer yPvrtyFlag) { this.yPvrtyFlag = yPvrtyFlag; } + public Double getDisposableIncomeMonthlyNoNull() { + return Objects.requireNonNullElse(yDispMonth, 0.0); + } public Double getDisposableIncomeMonthly() { return yDispMonth; } @@ -4221,7 +4213,7 @@ public Double getBenefitsReceivedPerMonth() { } public Integer getReceivedUC() { - return (yBenUCReceivedFlag == null ? 0 : yBenUCReceivedFlag); + return Objects.requireNonNullElse(yBenUCReceivedFlag, 0); } public void setReceivedUC(Integer yBenUCReceivedFlag) { @@ -4229,7 +4221,7 @@ public void setReceivedUC(Integer yBenUCReceivedFlag) { } public Integer getReceivedLegacyBenefits() { - return (yBenLegacyReceivedFlag == null ? 0 : yBenLegacyReceivedFlag); + return Objects.requireNonNullElse(yBenLegacyReceivedFlag, 0); } public void setReceivedLegacyBenefits(Integer yBenLegacyReceivedFlag) { @@ -4279,6 +4271,10 @@ public Ydses_c5 getYdses_c5_lag1() { return yHhQuintilesMonthC5L1; } + public double getTmpHHYpnbihs_dv_asinhNoNull() { + return Objects.requireNonNullElse(i_yNonBenHhGrossAsinh, 0.0); + } + public double getTmpHHYpnbihs_dv_asinh() { if (i_yNonBenHhGrossAsinh ==null) throw new RuntimeException("tmpHHYpnbihs_dv_asinh accessed before initialised"); @@ -4357,14 +4353,10 @@ public boolean isDecreaseInYearlyEquivalisedDisposableIncome() { return (yDispEquivYear != null && yDispEquivYearL1 != null && yDispEquivYear < yDispEquivYearL1); } - public void clearStates() { - if (labStatesContObject !=null) labStatesContObject = null; - } - void setStates() { // reset states if necessary - if (labStatesContObject !=null) clearStates(); + if (labStatesContObject !=null) labStatesContObject = null; // populate states object if ( Parameters.grids == null) { diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index 6da170ead..b465726c5 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -16,6 +16,7 @@ import simpaths.data.Parameters; import simpaths.data.RegressionName; import simpaths.data.filters.FertileFilter; +import simpaths.model.annotations.UpdateManager; import simpaths.model.decisions.Axis; import simpaths.model.decisions.DecisionParams; import simpaths.model.enums.*; @@ -64,13 +65,13 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Enumerated(EnumType.STRING) private SampleExit demExitSample = SampleExit.NotYet; //entry to sample via international immigration // person level variables - private int demAge; //Age + private Integer demAge; //Age @Column(name = "demAgeSq") private Integer demAgeSq; //Age squared private Dcpst demPartnerStatus; @Enumerated(EnumType.STRING) private Indicator demAdultChildFlag; - @Transient private boolean demIoFlag; // true if a dummy person instantiated for IO decision solution - @Enumerated(EnumType.STRING) private Gender demMaleFlag; // gender - @Enumerated(EnumType.STRING) private Education eduHighestC4; //Education level (4 categories incl. in education) + @Transient private Boolean demIoFlag = false; // true if a dummy person instantiated for IO decision solution + @Enumerated(EnumType.STRING) private Gender demMaleFlag; // gender + @Enumerated(EnumType.STRING) private Education eduHighestC4; //Education level (4 categories incl. in education) @Transient private Education eduHighestC4L1; //Lag(1) of education level @Enumerated(EnumType.STRING) private Education eduHighestMotherC4; //Mother's education level @Enumerated(EnumType.STRING) private Education eduHighestFatherC4; //Father's education level @@ -304,7 +305,7 @@ public Person(Gender gender, Person mother) { yBenNonUCReceivedFlag = false; yBenUCReceivedFlag = false; yFinDstrssFlag = mother.getYFinDstrssFlag(); - updateVariables(false); + updateAttributes(false); } // a "copy constructor" for persons: used by the cloneBenefitUnit method of the SimPathsModel object @@ -620,16 +621,104 @@ public void setAdditionalFieldsInInitialPopulation() { labHrsWorkWeek = labHrsWorkEnumWeek.getValue(); } } + + if(UnionMatchingMethod.SBAM.equals(model.getUnionMatchingMethod())) { + updateAgeGroup(); + } + + demGiveBirthFlag = false; + demBePartnerFlag = false; + demLeavePartnerFlag = false; + eduLeaveSchoolFlag = false; + setSedex(Indicator.False); //This variable is False by default + // is set to true only when person leaves school in this specific year + // eduSpellFlag = (Les_c4.Student.equals(labC4)) ? Indicator.True : Indicator.False; + // no need to update eduSpellFlag as its value is persisted from the previous year + + if (!Parameters.checkFinite(careHrsInformalWeek)) + careHrsInformalWeek = 0.0; + if (demAge type) { aging(); } case Update -> { - updateVariables(false); + updateAttributes(false); } case UpdateOutputVariables -> { updateOutputVariables(); @@ -2084,7 +2173,10 @@ protected void projectEquivConsumption() { } } - protected void updateVariables(boolean initialUpdate) { + + protected void updateAttributes(boolean initialUpdate) { + + UpdateManager.applyAnnotations(this); //Reset flags to default values @@ -5747,6 +5839,8 @@ public double getEquivalisedDisposableIncomeYearly() { return benefitUnit.getEquivalisedDisposableIncomeYearly(); } + public double getDisposableIncomeMonthlyNoNull() { return benefitUnit.getDisposableIncomeMonthlyNoNull();} + public double getDisposableIncomeMonthly() { return benefitUnit.getDisposableIncomeMonthly();} public double getWageOffer() { From 7648b0d497266ceb6b404d33378be512068add52 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:46:22 +0200 Subject: [PATCH 4/9] minor --- src/main/java/simpaths/model/Person.java | 66 +++++++++--------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index b465726c5..822cf06ab 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -305,7 +305,7 @@ public Person(Gender gender, Person mother) { yBenNonUCReceivedFlag = false; yBenUCReceivedFlag = false; yFinDstrssFlag = mother.getYFinDstrssFlag(); - updateAttributes(false); + updateAttributes(); } // a "copy constructor" for persons: used by the cloneBenefitUnit method of the SimPathsModel object @@ -809,7 +809,7 @@ public void onEvent(Enum type) { aging(); } case Update -> { - updateAttributes(false); + updateAttributes(); } case UpdateOutputVariables -> { updateOutputVariables(); @@ -2173,8 +2173,8 @@ protected void projectEquivConsumption() { } } - - protected void updateAttributes(boolean initialUpdate) { + + protected void updateAttributes() { UpdateManager.applyAnnotations(this); @@ -2189,8 +2189,6 @@ protected void updateAttributes(boolean initialUpdate) { // eduSpellFlag = (Les_c4.Student.equals(labC4)) ? Indicator.True : Indicator.False; // no need to update eduSpellFlag as its value is persisted from the previous year - if (initialUpdate && !Parameters.checkFinite(careHrsInformalWeek)) - careHrsInformalWeek = 0.0; if (demAge Date: Tue, 14 Apr 2026 18:50:17 +0200 Subject: [PATCH 5/9] minor --- src/main/java/simpaths/model/Person.java | 209 ++++++++++------------- 1 file changed, 91 insertions(+), 118 deletions(-) diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index 822cf06ab..50a282622 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -16,6 +16,8 @@ import simpaths.data.Parameters; import simpaths.data.RegressionName; import simpaths.data.filters.FertileFilter; +import simpaths.model.annotations.Lag; +import simpaths.model.annotations.NullInitialised; import simpaths.model.annotations.UpdateManager; import simpaths.model.decisions.Axis; import simpaths.model.decisions.DecisionParams; @@ -72,82 +74,82 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Transient private Boolean demIoFlag = false; // true if a dummy person instantiated for IO decision solution @Enumerated(EnumType.STRING) private Gender demMaleFlag; // gender @Enumerated(EnumType.STRING) private Education eduHighestC4; //Education level (4 categories incl. in education) - @Transient private Education eduHighestC4L1; //Lag(1) of education level + @Lag(field="eduHighestC4") @Transient private Education eduHighestC4L1; //Lag(1) of education level @Enumerated(EnumType.STRING) private Education eduHighestMotherC4; //Mother's education level @Enumerated(EnumType.STRING) private Education eduHighestFatherC4; //Father's education level @Enumerated(EnumType.STRING) private Ethnicity demEthnC6; //Ethnicity @Enumerated(EnumType.STRING) private Indicator eduSpellFlag; // in continuous education - @Enumerated(EnumType.STRING) private Indicator eduSpellFlagL1; // in continuous education + @Lag(field="eduSpellFlag") @Enumerated(EnumType.STRING) private Indicator eduSpellFlagL1; // in continuous education @Enumerated(EnumType.STRING) private Indicator eduReturnFlag; // return to education @Enumerated(EnumType.STRING) private Les_c4 labC4; //Activity (employment) status @Enumerated(EnumType.STRING) private Les_c7_covid labC7Covid; //Activity (employment) status used in the Covid-19 models - @Transient private Les_c4 labC4L1; //Lag(1) of activity_status - @Transient private Les_c7_covid labC7CovidL1; //Lag(1) of 7-category activity status + @Lag(field="labC4") @Transient private Les_c4 labC4L1; //Lag(1) of activity_status + @Lag(field="labC7Covid") @Transient private Les_c7_covid labC7CovidL1; //Lag(1) of 7-category activity status @Column(name="labWorkHist") private Integer labEmpNyear; //Work history in months (number of months in employment) (Note: this is monthly in EM, but simulation updates annually so increment by 12 months). @Enumerated(EnumType.STRING) private Indicator healthDsblLongtermFlag; //Long-term sick or disabled if = 1 - @Transient private Indicator healthDsblLongtermFlagL1; //Lag(1) of long-term sick or disabled + @Lag(field="healthDsblLongtermFlag") @Transient private Indicator healthDsblLongtermFlagL1; //Lag(1) of long-term sick or disabled @Enumerated(EnumType.STRING) @Column(name="careNeedFlag") private Indicator careNeedFlag; @Column(name="careHrsFormal") private Double careHrsFormalWeek; @Column(name="careFormalX") private Double careFormalX; @Column(name="careHrsInformal") private Double careHrsInformalWeek; private Boolean labWageOfferLowFlag; - @Transient private Boolean labWageOfferLowFlagL1; + @Lag(getter="getLowWageOffer") @Transient private Boolean labWageOfferLowFlagL1; @Transient private SocialCareReceipt careReceivedFlag; @Transient private Boolean careFormalFlag; @Transient private Boolean careFromInformalFlag; @Column(name="careHrsProvidedWeek") private Double careHrsProvidedWeek; @Enumerated(EnumType.STRING) @Column(name="careProvidedFlag") private SocialCareProvision careProvidedFlag; - @Transient private SocialCareProvision careProvidedFlagL1; - @Transient private Indicator careNeedFlagL1; - @Transient private Double careHrsFormalWeekL1; - @Transient private Double careHrsInformalWeekL1; - @Transient private Double careHrsProvidedWeekL1; - @Transient private Boolean demPrptyFlagL1; + @Lag(field="careProvidedFlag") @Transient private SocialCareProvision careProvidedFlagL1; + @Lag(field="careNeedFlag") @Transient private Indicator careNeedFlagL1; + @Lag(field="careHrsFormalWeek") @Transient private Double careHrsFormalWeekL1; + @Lag(field="careHrsProvidedWeek") @Transient private Double careHrsProvidedWeekL1; + @Lag(field="careHrsInformalWeek") @Transient private Double careHrsInformalWeekL1; + @Lag(getter="isHousingOwned") @Transient private Boolean demPrptyFlagL1; // partner lags - @Transient private Dcpst demPartnerStatusL1; // lag partnership status - @Transient private Dcpst demPartnerStatusL2; // lag (2) partnership status - @Transient private Education eduHighestPartnerC4L1; //Lag(1) of partner's education - @Transient private Dhe healthPartnerSelfRatedL1; - @Transient private Lesdf_c4 labStatusPartnerAndOwnC4L1; //Lag(1) of own and partner's activity status - @Transient private Long idPartnerL1; - @Transient private HouseholdStatus demStatusHhL1; //Lag(1) of household_status - @Transient private Integer demAgePartnerDiffL1; //Lag(1) of difference between ages of partners in union - @Transient private Double yPersAndPartnerGrossDiffMonthL1; //Lag(1) of difference between own and partner's gross personal non-benefit income + @Lag(field="demPartnerStatusL1") @Transient private Dcpst demPartnerStatusL2; // lag (2) partnership status + @Lag(getter="getDcpst") @Transient private Dcpst demPartnerStatusL1; // lag partnership status + @Lag(getter="getEduHighestPartner") @Transient private Education eduHighestPartnerC4L1; //Lag(1) of partner's education + @Lag(getter="getHealthPartner") @Transient private Dhe healthPartnerSelfRatedL1; + @Lag(getter="getLesdf_c4") @Transient private Lesdf_c4 labStatusPartnerAndOwnC4L1; //Lag(1) of own and partner's activity status + @Lag(getter="getIdPartner") @Transient private Long idPartnerL1; + @Lag(getter="getHouseholdStatus") @Transient private HouseholdStatus demStatusHhL1; //Lag(1) of household_status + @Lag(getter="getAgeDifferencePartner") @Transient private Integer demAgePartnerDiffL1; //Lag(1) of difference between ages of partners in union + @Lag(getter="getYnbcpdf_dv") @Transient private Double yPersAndPartnerGrossDiffMonthL1; //Lag(1) of difference between own and partner's gross personal non-benefit income @Enumerated(EnumType.STRING) private Indicator eduExitSampleFlag; // year left education - @Transient private Boolean demGiveBirthFlag; - @Transient private Boolean eduLeaveSchoolFlag; - @Transient private Boolean demBePartnerFlag; + @NullInitialised @Transient private Boolean demGiveBirthFlag; + @NullInitialised @Transient private Boolean eduLeaveSchoolFlag; + @NullInitialised @Transient private Boolean demBePartnerFlag; @Transient private Boolean demAlignPartnerProcess; - @Transient private Boolean demLeavePartnerFlag; // Used in partnership alignment process. Indicates that this person has found partner in a test run of union matching. + @NullInitialised @Transient private Boolean demLeavePartnerFlag; // Used in partnership alignment process. Indicates that this person has found partner in a test run of union matching. @Column(name="wgt") private Double wgt; @Column(name="healthPsyDstrss0to12") private Double healthPsyDstrss0to12; //Psychological distress GHQ-12 0-12 caseness score - @Transient private Double healthPsyDstrss0to12L1; - @Transient private Dhe healthSelfRatedL1; + @Lag(field="healthPsyDstrss0to12") @Transient private Double healthPsyDstrss0to12L1; + @Lag(field="healthSelfRated") @Transient private Dhe healthSelfRatedL1; @Enumerated(EnumType.STRING) private Dhe healthSelfRated; private Double healthWbScore0to36; //Psychological distress GHQ-12 Likert scale - @Transient private Double healthWbScore0to36L1; //Lag(1) of dhm + @Lag(field="healthWbScore0to36") @Transient private Double healthWbScore0to36L1; //Lag(1) of dhm private Double healthMentalMcs; //mental well-being: SF12 mental component summary score - @Transient private Double healthMentalMcsL1; //mental well-being: SF12 mental component summary score lag 1 + @Lag(field="healthMentalMcs") @Transient private Double healthMentalMcsL1; //mental well-being: SF12 mental component summary score lag 1 private Double healthPhysicalPcs; //physical well-being: SF12 physical component summary score - @Transient private Double healthPhysicalPcsL1; //physical well-being: SF12 physical component summary score lag 1 + @Lag(field="healthPhysicalPcs") @Transient private Double healthPhysicalPcsL1; //physical well-being: SF12 physical component summary score lag 1 private Double healthMentalPartnerMcs; //mental well-being: SF12 mental component summary score (partner) private Double healthPhysicalPartnerPcs; //physical well-being: SF12 physical component summary score (partner) private Double demLifeSatScore0to10; //life satisfaction - score 0-10 - @Transient private Double demLifeSatScore0to10L1; //life satisfaction - score 0-10 lag 1 + @Lag(field="demLifeSatScore0to10") @Transient private Double demLifeSatScore0to10L1; //life satisfaction - score 0-10 lag 1 @Column(name="demLifeSatEQ5D") private Double demLifeSatEQ5D; @Column(name="yFinDstrssFlag") private Boolean yFinDstrssFlag; - @Transient private Boolean yBenReceivedFlagL1; // Lag(1) of whether person receives benefits + @Lag(field="yBenReceivedFlag") @Transient private Boolean yBenReceivedFlagL1; // Lag(1) of whether person receives benefits @Transient private Boolean yBenReceivedFlag; // Does person receive benefits @Column(name="yBenUCReceivedFlag") private Boolean yBenUCReceivedFlag; // Person receives UC - @Transient private Boolean yBenUCReceivedFlagL1; + @Lag(field="yBenUCReceivedFlag") @Transient private Boolean yBenUCReceivedFlagL1; @Column(name="yBenNonUCReceivedFlag") private Boolean yBenNonUCReceivedFlag; // Person receives a benefit which is not UC - @Transient private Boolean yBenNonUCReceivedFlagL1; + @Lag(field="yBenNonUCReceivedFlag") @Transient private Boolean yBenNonUCReceivedFlagL1; @Column(name="yLifeTime") private Double yLifeTime; // mean annual equivalised household disposable income by age @Enumerated(EnumType.STRING) private Labour labHrsWorkEnumWeek; //Number of hours of labour supplied each week - @Transient private Labour labHrsWorkEnumWeekL1; // Lag(1) (previous year's value) of weekly labour supply + @Lag(getter="getLabourSupplyWeekly") @Transient private Labour labHrsWorkEnumWeekL1; // Lag(1) (previous year's value) of weekly labour supply @Column(name = "HOURS_WORKED_WEEKLY") private Integer labHrsWorkWeek; private Integer labHrsWorkWeekL1; // Lag(1) of hours worked weekly - use to initialise labour supply weekly_L1 (TODO) @@ -157,28 +159,28 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, // is a separate process in the simulation, and it is computed for every adult // individual in the simulated population, in each simulated period. @Column(name="labWageHrly") private Double labWageFullTimeHrly; //Is hourly rate. Initialised with value: ils_earns / (4.34 * lhw), where lhw is the weekly hours a person worked in EUROMOD input data - @Column(name="labWageFullTimeHrlyL1") private Double labWageFullTimeHrlyL1; // Lag(1) of potentialHourlyEarnings + @Lag(field="labWageFullTimeHrly") @Column(name="labWageFullTimeHrlyL1") private Double labWageFullTimeHrlyL1; // Lag(1) of potentialHourlyEarnings @Transient private Series.Double yDispEquivYear; private Double xEquivYear; @Transient private Series.Double xEquivYearL1; private Integer demPartnerNYear; //Number of years in partnership @Transient private Integer demPartnerNYearL1; //Lag(1) of number of years in partnership private Double yNonBenPersGrossMonth; // asinh of personal non-benefit income per month - @Transient private Double yNonBenPersGrossMonthL1; //Lag(1) of gross personal non-benefit income + @Lag(getter="getyNonBenPersGrossMonth") @Transient private Double yNonBenPersGrossMonthL1; //Lag(1) of gross personal non-benefit income private Double yMiscPersGrossMonth; // asinh of non-employment non-benefit income per month (capital and pension) private Double yCapitalPersMonth; // asinh of capital income per month private Double yPensPersGrossMonth; // asinh of pension income per month - @Transient private Double yCapitalPersMonthL1; //Lag(1) of ypncp - @Transient private Double yCapitalPersMonthL2; //Lag(2) of capital income - @Transient private Double yPensPersGrossMonthL1; //Lag(1) of pension income - @Transient private Double yPensPersGrossMonthL2; //Lag(2) of pension income - @Transient private Double yMiscPersGrossMonthL1; //Lag(1) of gross personal non-benefit non-employment income - @Transient private Double yMiscPersGrossMonthL2; //Lag(2) of gross personal non-benefit non-employment income - @Transient private Double yMiscPersGrossMonthL3; //Lag(3) of gross personal non-benefit non-employment income + @Lag(field="yCapitalPersMonthL1") @Transient private Double yCapitalPersMonthL2; //Lag(2) of capital income + @Lag(getter="getyCapitalPersMonth") @Transient private Double yCapitalPersMonthL1; //Lag(1) of ypncp + @Lag(field="yPensPersGrossMonthL1") @Transient private Double yPensPersGrossMonthL2; //Lag(2) of pension income + @Lag(getter="getyPensPersGrossMonth") @Transient private Double yPensPersGrossMonthL1; //Lag(1) of pension income + @Lag(field="yMiscPersGrossMonthL2") @Transient private Double yMiscPersGrossMonthL3; //Lag(3) of gross personal non-benefit non-employment income + @Lag(field="yMiscPersGrossMonthL1") @Transient private Double yMiscPersGrossMonthL2; //Lag(2) of gross personal non-benefit non-employment income + @Lag(getter="getyMiscPersGrossMonth") @Transient private Double yMiscPersGrossMonthL1; //Lag(1) of gross personal non-benefit non-employment income private Double yEmpPersGrossMonth; // asinh transform of personal labour income per month - @Transient private Double yEmpPersGrossMonthL1; //Lag(1) of gross personal employment income - @Transient private Double yEmpPersGrossMonthL2; //Lag(2) of gross personal employment income - @Transient private Double yEmpPersGrossMonthL3; //Lag(3) of gross personal employment income + @Lag(field="yEmpPersGrossMonthL2") @Transient private Double yEmpPersGrossMonthL3; //Lag(3) of gross personal employment income + @Lag(field="yEmpPersGrossMonthL1") @Transient private Double yEmpPersGrossMonthL2; //Lag(2) of gross personal employment income + @Lag(getter="getyEmpPersGrossMonth") @Transient private Double yEmpPersGrossMonthL1; //Lag(1) of gross personal employment income //For matching process @Transient private Double demAgeDiffDesired; @@ -2180,16 +2182,12 @@ protected void updateAttributes() { //Reset flags to default values - demGiveBirthFlag = false; - demBePartnerFlag = false; - demLeavePartnerFlag = false; - eduLeaveSchoolFlag = false; setSedex(Indicator.False); //This variable is False by default // is set to true only when person leaves school in this specific year // eduSpellFlag = (Les_c4.Student.equals(labC4)) ? Indicator.True : Indicator.False; // no need to update eduSpellFlag as its value is persisted from the previous year - if (demAge Date: Tue, 14 Apr 2026 20:48:00 +0200 Subject: [PATCH 6/9] minor --- .../RegionEducationAtRiskOfWorkCSfilter.java | 7 ++- .../data/filters/RegionEducationCSfilter.java | 6 ++- .../RegionEducationWorkingCSfilter.java | 6 ++- .../ValidEducationAgeGroupCSfilter.java | 8 ++- .../filters/ValidEducationRegionCSfilter.java | 6 ++- src/main/java/simpaths/model/Person.java | 50 +++++++++---------- 6 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/main/java/simpaths/data/filters/RegionEducationAtRiskOfWorkCSfilter.java b/src/main/java/simpaths/data/filters/RegionEducationAtRiskOfWorkCSfilter.java index f6256d7cd..4addb48c4 100644 --- a/src/main/java/simpaths/data/filters/RegionEducationAtRiskOfWorkCSfilter.java +++ b/src/main/java/simpaths/data/filters/RegionEducationAtRiskOfWorkCSfilter.java @@ -3,6 +3,7 @@ import microsim.statistics.ICollectionFilter; import simpaths.model.Person; import simpaths.model.enums.Education; +import simpaths.model.enums.Les_c4; import simpaths.model.enums.Region; public class RegionEducationAtRiskOfWorkCSfilter implements ICollectionFilter{ @@ -19,7 +20,11 @@ public RegionEducationAtRiskOfWorkCSfilter(Region demRgn, Education education) { public boolean isFiltered(Object object) { if(object instanceof Person) { Person person = (Person) object; - return (person.getRegion().equals(demRgn) && person.getDeh_c4().equals(education) && person.atRiskOfWork()); + if (person.getDeh_c4()==null) { + return false; + } else { + return (person.getRegion().equals(demRgn) && person.getDeh_c4().equals(education) && person.atRiskOfWork()); + } } else throw new IllegalArgumentException("Object passed to RegionEducationCSfilter must be of type Person!"); } diff --git a/src/main/java/simpaths/data/filters/RegionEducationCSfilter.java b/src/main/java/simpaths/data/filters/RegionEducationCSfilter.java index 131b1855d..aa26b387c 100644 --- a/src/main/java/simpaths/data/filters/RegionEducationCSfilter.java +++ b/src/main/java/simpaths/data/filters/RegionEducationCSfilter.java @@ -19,7 +19,11 @@ public RegionEducationCSfilter(Region demRgn, Education education) { public boolean isFiltered(Object object) { if(object instanceof Person) { Person person = (Person) object; - return (person.getRegion().equals(demRgn) && person.getDeh_c4().equals(education)); + if (person.getDeh_c4()==null) { + return false; + } else { + return (person.getRegion().equals(demRgn) && person.getDeh_c4().equals(education)); + } } else throw new IllegalArgumentException("Object passed to RegionEducationCSfilter must be of type Person!"); } diff --git a/src/main/java/simpaths/data/filters/RegionEducationWorkingCSfilter.java b/src/main/java/simpaths/data/filters/RegionEducationWorkingCSfilter.java index ca0090178..f5cafed61 100644 --- a/src/main/java/simpaths/data/filters/RegionEducationWorkingCSfilter.java +++ b/src/main/java/simpaths/data/filters/RegionEducationWorkingCSfilter.java @@ -20,7 +20,11 @@ public RegionEducationWorkingCSfilter(Region demRgn, Education education) { public boolean isFiltered(Object object) { if(object instanceof Person) { Person person = (Person) object; - return (person.getRegion().equals(demRgn) && person.getDeh_c4().equals(education) && person.getLes_c4().equals(Les_c4.EmployedOrSelfEmployed) && person.getGrossEarningsYearly() >= 0.); + if (person.getDeh_c4()==null || person.getLes_c4()==null) { + return false; + } else { + return (person.getRegion().equals(demRgn) && person.getDeh_c4().equals(education) && person.getLes_c4().equals(Les_c4.EmployedOrSelfEmployed) && person.getGrossEarningsYearly() >= 0.); + } } else throw new IllegalArgumentException("Object passed to RegionEducationWorkingCSfilter must be of type Person!"); } diff --git a/src/main/java/simpaths/data/filters/ValidEducationAgeGroupCSfilter.java b/src/main/java/simpaths/data/filters/ValidEducationAgeGroupCSfilter.java index 31d2d26ad..91bbc5324 100644 --- a/src/main/java/simpaths/data/filters/ValidEducationAgeGroupCSfilter.java +++ b/src/main/java/simpaths/data/filters/ValidEducationAgeGroupCSfilter.java @@ -4,6 +4,8 @@ import simpaths.model.enums.Les_c4; import microsim.statistics.ICollectionFilter; +import java.util.Objects; + /* This filter is like an age group filter, but only selects individuals who are not Students @@ -22,7 +24,11 @@ public ValidEducationAgeGroupCSfilter(int ageFrom, int ageTo) { public boolean isFiltered(Object object) { Person person = (Person) object; - return ( (person.getDemAge() >= ageFrom) && (person.getDemAge() <= ageTo) && !person.getLes_c4().equals(Les_c4.Student)); + if (person.getLes_c4()==null) { + return false; + } else { + return ( (person.getDemAge() >= ageFrom) && (person.getDemAge() <= ageTo) && !person.getLes_c4().equals(Les_c4.Student)); + } } public int getAgeFrom() { diff --git a/src/main/java/simpaths/data/filters/ValidEducationRegionCSfilter.java b/src/main/java/simpaths/data/filters/ValidEducationRegionCSfilter.java index 20bee7491..2d56ba6f1 100644 --- a/src/main/java/simpaths/data/filters/ValidEducationRegionCSfilter.java +++ b/src/main/java/simpaths/data/filters/ValidEducationRegionCSfilter.java @@ -24,7 +24,11 @@ public ValidEducationRegionCSfilter(Region demRgn) { public boolean isFiltered(Object object) { if(object instanceof Person) { Person person = (Person) object; - return (person.getRegion().equals(demRgn) && !person.getLes_c4().equals(Les_c4.Student) && person.getDemAge() >= 18 && person.getDeh_c4() != null); + if (person.getLes_c4()==null) { + return false; + } else { + return (person.getRegion().equals(demRgn) && !person.getLes_c4().equals(Les_c4.Student) && person.getDemAge() >= 18 && person.getDeh_c4() != null); + } } else if(object instanceof BenefitUnit) { BenefitUnit benefitUnit = (BenefitUnit) object; diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index 50a282622..23ee76cb7 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -191,36 +191,36 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Transient private Boolean eduLeftEduFlag; //This is set to true at the point when individual leaves partnership and never reset. So if true, individual has been / is in a partnership - @Transient private Boolean demLeftPartnerFlag; - @Transient private Integer labHrsWorkNewL1; // Define a variable to keep previous month's value of work hours to be used in the Covid-19 module - @Transient private Double covidYLabGrossL1; + @NullInitialised @Transient private Boolean demLeftPartnerFlag; + @NullInitialised @Transient private Integer labHrsWorkNewL1; // Define a variable to keep previous month's value of work hours to be used in the Covid-19 module + @NullInitialised @Transient private Double covidYLabGrossL1; @Transient private Indicator covidSEISSReceivedFlag = Indicator.False; - @Transient private Double covidYLabGross; - private Quintiles covidYLabGrossXt5; - @Transient private Double labWageRegressRandomCompoponentEmp; - @Transient private Double labWageRegressRandomCompoponentNotEmp; + @NullInitialised @Transient private Double covidYLabGross; + @NullInitialised private Quintiles covidYLabGrossXt5; + @NullInitialised @Transient private Double labWageRegressRandomCompoponentEmp; + @NullInitialised @Transient private Double labWageRegressRandomCompoponentNotEmp; @Transient private Map personContinuousHoursLabourSupplyMap = new EnumMap<>(Labour.class); // local variables interact with regression models - @Transient private Integer i_demYear; - @Transient private Region i_demRgn; - @Transient private Dhhtp_c4 i_demCompHhC4L1; - @Transient private Ydses_c5 i_yHhQuintilesC5; - @Transient private Integer i_demNchildL1; - @Transient private Integer i_demNchild; - @Transient private Integer i_demNchild0to2L1; - @Transient private Integer i_demNchild0to17; - @Transient private Indicator i_demNChild0to2; - @Transient private Dcpst i_demPartnerStatus; + @NullInitialised @Transient private Integer i_demYear; + @NullInitialised @Transient private Region i_demRgn; + @NullInitialised @Transient private Dhhtp_c4 i_demCompHhC4L1; + @NullInitialised @Transient private Ydses_c5 i_yHhQuintilesC5; + @NullInitialised @Transient private Integer i_demNchildL1; + @NullInitialised @Transient private Integer i_demNchild; + @NullInitialised @Transient private Integer i_demNchild0to2L1; + @NullInitialised @Transient private Integer i_demNchild0to17; + @NullInitialised @Transient private Indicator i_demNChild0to2; + @NullInitialised @Transient private Dcpst i_demPartnerStatus; // innovations @Transient Innovations statInnovations; //TODO: Remove when no longer needed. Used to calculate mean score of employment selection regression. - @Transient public static Double statMScore; - @Transient public static Double statFScore; - @Transient public static Double countMale; - @Transient public static Double countFemale; + @NullInitialised @Transient public static Double statMScore; + @NullInitialised @Transient public static Double statFScore; + @NullInitialised @Transient public static Double countMale; + @NullInitialised @Transient public static Double countFemale; @Transient public static Double statInverseMillsRatioMaxM = Double.MIN_VALUE; @Transient public static Double statInverseMillsRatioMinM = Double.MAX_VALUE; @Transient public static Double statInverseMillsRatioMaxF = Double.MIN_VALUE; @@ -5485,15 +5485,15 @@ public Double getyNonBenPersGrossMonthL1() { return yNonBenPersGrossMonthL1; } - public double getyMiscPersGrossMonth() { + public Double getyMiscPersGrossMonth() { return yMiscPersGrossMonth; } - public double getyCapitalPersMonth() { + public Double getyCapitalPersMonth() { return yCapitalPersMonth; } - public double getyPensPersGrossMonth() { + public Double getyPensPersGrossMonth() { return yPensPersGrossMonth; } @@ -5503,7 +5503,7 @@ public void setYptciihs_dv(double yMiscPersGrossMonth) { throw new IllegalArgumentException("yptciihs_dv is not finite"); } - public double getyMiscPersGrossMonthL1() { + public Double getyMiscPersGrossMonthL1() { return yMiscPersGrossMonthL1; } From 8b656b93089b64b39dfeb541fe9d3bde0eb4506c Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:45:42 +0200 Subject: [PATCH 7/9] minor --- src/main/java/simpaths/model/Person.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index 23ee76cb7..3f895c713 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -94,9 +94,9 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Column(name="careHrsInformal") private Double careHrsInformalWeek; private Boolean labWageOfferLowFlag; @Lag(getter="getLowWageOffer") @Transient private Boolean labWageOfferLowFlagL1; - @Transient private SocialCareReceipt careReceivedFlag; - @Transient private Boolean careFormalFlag; - @Transient private Boolean careFromInformalFlag; + @NullInitialised @Transient private SocialCareReceipt careReceivedFlag; + @NullInitialised @Transient private Boolean careFormalFlag; + @NullInitialised @Transient private Boolean careFromInformalFlag; @Column(name="careHrsProvidedWeek") private Double careHrsProvidedWeek; @Enumerated(EnumType.STRING) @Column(name="careProvidedFlag") private SocialCareProvision careProvidedFlag; @Lag(field="careProvidedFlag") @Transient private SocialCareProvision careProvidedFlagL1; @@ -121,7 +121,7 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @NullInitialised @Transient private Boolean demGiveBirthFlag; @NullInitialised @Transient private Boolean eduLeaveSchoolFlag; @NullInitialised @Transient private Boolean demBePartnerFlag; - @Transient private Boolean demAlignPartnerProcess; + @NullInitialised @Transient private Boolean demAlignPartnerProcess; @NullInitialised @Transient private Boolean demLeavePartnerFlag; // Used in partnership alignment process. Indicates that this person has found partner in a test run of union matching. @Column(name="wgt") private Double wgt; @Column(name="healthPsyDstrss0to12") private Double healthPsyDstrss0to12; //Psychological distress GHQ-12 0-12 caseness score @@ -141,12 +141,12 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Column(name="demLifeSatEQ5D") private Double demLifeSatEQ5D; @Column(name="yFinDstrssFlag") private Boolean yFinDstrssFlag; @Lag(field="yBenReceivedFlag") @Transient private Boolean yBenReceivedFlagL1; // Lag(1) of whether person receives benefits - @Transient private Boolean yBenReceivedFlag; // Does person receive benefits + @NullInitialised @Transient private Boolean yBenReceivedFlag; // Does person receive benefits @Column(name="yBenUCReceivedFlag") private Boolean yBenUCReceivedFlag; // Person receives UC @Lag(field="yBenUCReceivedFlag") @Transient private Boolean yBenUCReceivedFlagL1; @Column(name="yBenNonUCReceivedFlag") private Boolean yBenNonUCReceivedFlag; // Person receives a benefit which is not UC @Lag(field="yBenNonUCReceivedFlag") @Transient private Boolean yBenNonUCReceivedFlagL1; - @Column(name="yLifeTime") private Double yLifeTime; // mean annual equivalised household disposable income by age + @NullInitialised @Column(name="yLifeTime") private Double yLifeTime; // mean annual equivalised household disposable income by age @Enumerated(EnumType.STRING) private Labour labHrsWorkEnumWeek; //Number of hours of labour supplied each week @Lag(getter="getLabourSupplyWeekly") @Transient private Labour labHrsWorkEnumWeekL1; // Lag(1) (previous year's value) of weekly labour supply @@ -161,7 +161,7 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Column(name="labWageHrly") private Double labWageFullTimeHrly; //Is hourly rate. Initialised with value: ils_earns / (4.34 * lhw), where lhw is the weekly hours a person worked in EUROMOD input data @Lag(field="labWageFullTimeHrly") @Column(name="labWageFullTimeHrlyL1") private Double labWageFullTimeHrlyL1; // Lag(1) of potentialHourlyEarnings @Transient private Series.Double yDispEquivYear; - private Double xEquivYear; + @NullInitialised private Double xEquivYear; @Transient private Series.Double xEquivYearL1; private Integer demPartnerNYear; //Number of years in partnership @Transient private Integer demPartnerNYearL1; //Lag(1) of number of years in partnership @@ -185,7 +185,7 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, //For matching process @Transient private Double demAgeDiffDesired; @Transient private Double yWageDesired; - @Transient private Integer demAgeGroup; + @NullInitialised @Transient private Integer demAgeGroup; //This is set to true at the point when individual leaves education and never reset. So if true, individual has not always been in continuous education. @Transient private Boolean eduLeftEduFlag; From 8a9ee48fa51f1d58cb35e342ccfea04d253bcd2d Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:20:27 +0200 Subject: [PATCH 8/9] final --- src/main/java/simpaths/model/BenefitUnit.java | 14 +++++++------- src/main/java/simpaths/model/SimPathsModel.java | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/simpaths/model/BenefitUnit.java b/src/main/java/simpaths/model/BenefitUnit.java index ec8e1ad4b..0da9c716f 100644 --- a/src/main/java/simpaths/model/BenefitUnit.java +++ b/src/main/java/simpaths/model/BenefitUnit.java @@ -66,18 +66,18 @@ public class BenefitUnit implements EventListener, IDoubleSource, Weight, Compar @NullInitialised private Double yInvestYear; @NullInitialised private Double yPensYear; @NullInitialised private Double xDiscretionaryYear; - @Column(name="wealthTotValue") private Double wealthTotValue; // total net wealth (includes pensions assets and housing) - @Column(name="wealthPensValue") private Double wealthPensValue; // total private (personal and occupational) pensions - @Column(name="wealthPrptyValue") private Double wealthPrptyValue; // value of main home (gross of mortgage debt) - @Column(name="wealthMortgageDebtValue") private Double wealthMortgageDebtValue; // value of outstanding mortgage debt - private Double yDispMonth; + @NullInitialised @Column(name="wealthTotValue") private Double wealthTotValue; // total net wealth (includes pensions assets and housing) + @NullInitialised @Column(name="wealthPensValue") private Double wealthPensValue; // total private (personal and occupational) pensions + @NullInitialised @Column(name="wealthPrptyValue") private Double wealthPrptyValue; // value of main home (gross of mortgage debt) + @NullInitialised @Column(name="wealthMortgageDebtValue") private Double wealthMortgageDebtValue; // value of outstanding mortgage debt + @NullInitialised private Double yDispMonth; @NullInitialised private Double yGrossMonth; @NullInitialised private Double yBenAmountMonth; @NullInitialised private Integer yBenUCReceivedFlag; @NullInitialised private Integer yBenLegacyReceivedFlag; - private Double yDispEquivYear; + @NullInitialised private Double yDispEquivYear; @Lag(getter = "getEquivalisedDisposableIncomeYearly") @Transient private Double yDispEquivYearL1; - @Transient private Double yDiffDispEquivPrevYear; + @NullInitialised @Transient private Double yDiffDispEquivPrevYear; private Integer yPvrtyFlag; //1 if at risk of poverty, defined by an equivalisedDisposableIncomeYearly < 60% of median household's @Lag(getter = "getAtRiskOfPoverty") @Transient private Integer yPvrtyFlagL1; @Lag(getter = "getIndicatorChildren0to3") @Transient private Indicator dem0to3L1; diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 699c8636d..7939ca560 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -505,8 +505,8 @@ public void buildSchedule() { addEventToAllYears(Processes.GarbageCollection); if (enableIntertemporalOptimisations) yearlySchedule.addCollectionEvent(benefitUnits, BenefitUnit.Processes.UpdateWealth); - addCollectionEventToAllYears(benefitUnits, BenefitUnit.Processes.Update); - addCollectionEventToAllYears(persons, Person.Processes.Update); + yearlySchedule.addCollectionEvent(benefitUnits, BenefitUnit.Processes.Update); + yearlySchedule.addCollectionEvent(persons, Person.Processes.Update); yearlySchedule.addCollectionEvent(persons, Person.Processes.Aging); From a39fbef578dba3bad3883d3e75a8a49eb993eccd Mon Sep 17 00:00:00 2001 From: dav-sonn <62243276+dav-sonn@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:03:16 +0100 Subject: [PATCH 9/9] Promote Javadoc headings to h2 Replace

with

to solve error in PR #453. --- src/main/java/simpaths/model/annotations/UpdateManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/simpaths/model/annotations/UpdateManager.java b/src/main/java/simpaths/model/annotations/UpdateManager.java index 557037945..17601964c 100644 --- a/src/main/java/simpaths/model/annotations/UpdateManager.java +++ b/src/main/java/simpaths/model/annotations/UpdateManager.java @@ -9,7 +9,7 @@ * Reflection-based processor that applies {@link Lag} and {@link NullInitialised} annotations * at the start of each simulation time interval. * - *

Processing order

+ *

Processing order

*
    *
  1. Read phase — all {@link Lag}-annotated fields have their source values read * (via field reference or getter) and cached before any writes occur.
  2. @@ -26,7 +26,7 @@ * L1 ← current * * - *

    Typical usage in an entity's update method

    + *

    Typical usage in an entity's update method

    *
    {@code
      *   // In Person.updateLaggedVariables() or BenefitUnit.updateAttributes():
      *   UpdateManager.applyAnnotations(this);