From 8d5ce796abb1abc1556a2f52c323595a42810b19 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 2 Sep 2026 13:54:10 +0100 Subject: [PATCH 1/4] fix 1 --- docs/md/fmprofiles.md | 2 ++ examples/fm_alloc/README.md | 56 +++++++++++++++++++++++++++++++++++++ ktest/ctrl.sha1 | 6 ++++ ktest/runtests.sh | 16 ++++++++++- src/fmcalc/fmcalc.cpp | 36 ++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 examples/fm_alloc/README.md diff --git a/docs/md/fmprofiles.md b/docs/md/fmprofiles.md index 107429b4..08be2d25 100644 --- a/docs/md/fmprofiles.md +++ b/docs/md/fmprofiles.md @@ -121,6 +121,8 @@ The choices are as follows; | Back-allocate losses to items in proportion to prior level loss | 2 | | Back-allocate losses to items in proportion to prior level loss (reinsurance)| 3 | +Allocrules 2 and 3 perform the same calculation and give the same answer. Rule 3 works out the item proportions at every level and layer as the calculation proceeds, while rule 2 only works them out when they are needed at the final level. Where there is more than one layer, each layer is allocated using its own prior level losses under both rules. + ## Effective deductibles Often there are more than one hierarchal levels with deductibles, and there is a choice of methods of accumulation of deductibles through the hierarchy. Whenever a rule with a deductible is used in the loss calculation then it is accumulated through the calculation in an **effective_deductible** variable. The effective deductible is the smaller of the deductible amount and the loss. diff --git a/examples/fm_alloc/README.md b/examples/fm_alloc/README.md new file mode 100644 index 00000000..d7d1165d --- /dev/null +++ b/examples/fm_alloc/README.md @@ -0,0 +1,56 @@ +# fmcalc back-allocation test cases + +Two small financial structures for testing the fmcalc back-allocation rules (`-a1`, `-a2`, +`-a3`). Both have more than one policy layer and are run by `ktest/runtests.sh`. + +## case1 - layers with different loss distributions + +Four items, two per location, two coverages per location, two policy layers: + +| level | aggregation | terms | +|-------|--------------------------------|----------------------------------------------------------| +| 1 | one agg per item (coverage) | layer 1 limits items 1 and 3 to 500; layer 2 limits items 2 and 4 to 500 | +| 2 | items 1,2 -> agg 1; 3,4 -> agg 2 (location) | none | +| 3 | all -> agg 1 (account) | both layers limited to 4000 | + +Ground up losses are 1000, 3000, 3000, 1000 for items 1 to 4. + +`fm_xref` output ids are assigned in item then layer order, so output ids 1, 3, 5, 7 are +layer 1 for items 1 to 4 and output ids 2, 4, 6, 8 are layer 2 for items 1 to 4. + +Each layer's level 2 losses are: + +* layer 1 - location 1 = 500 + 3000 = 3500, location 2 = 500 + 1000 = 1500 +* layer 2 - location 1 = 1000 + 500 = 1500, location 2 = 3000 + 500 = 3500 + +so the two layers distribute their losses over the locations in opposite proportions. The +level 3 limit of 4000 applies to a 5000 loss in both layers, and 4000 is back-allocated: + +| output_id | item | layer | -a1 | -a2 and -a3 | +|-----------|------|-------|------|-------------| +| 1 | 1 | 1 | 500 | 400 | +| 2 | 1 | 2 | 500 | 800 | +| 3 | 2 | 1 | 1500 | 2400 | +| 4 | 2 | 2 | 1500 | 400 | +| 5 | 3 | 1 | 1500 | 400 | +| 6 | 3 | 2 | 1500 | 2400 | +| 7 | 4 | 1 | 500 | 800 | +| 8 | 4 | 2 | 500 | 400 | + +Alloc rule 1 allocates in proportion to the ground up losses, so both layers give the same +answer. Alloc rules 2 and 3 allocate in proportion to the prior level losses, so each layer +follows its own distribution and the two layers are mirror images of each other. + +This is a regression test for [#2055](https://github.com/OasisLMF/OasisLMF/issues/2055), +where alloc rule 2 back-allocated every layer using layer 1's proportions and gave +171.43, 1028.57, 933.33 and 1866.67 for the layer 2 items above. Because layer ids follow +the row order of the account file, that made per item results depend on the order of the +input. + +## case2 - a layer with no loss below the top level + +Five items, three layers, two levels, with deductibles that leave some level 1 +aggregations with no loss in layer 1 but a loss in a later layer. Alloc rule 2 only fills +in the item proportions for layer 1, and skips the aggregations that had no loss, so the +later layers were left with no proportions to allocate with and fmcalc segmentation +faulted. The expected output is pinned for all three allocation rules. diff --git a/ktest/ctrl.sha1 b/ktest/ctrl.sha1 index 50662f0c..074f9bc8 100644 --- a/ktest/ctrl.sha1 +++ b/ktest/ctrl.sha1 @@ -62,6 +62,12 @@ a62faf4156627dc625adbaf9aaa4bff23029a515 fmaalcalc1.csv 8335618f732c5ae973601fe680377e2374adc17b fmaltmeanonly2.csv d0e7b6b6a14dbc1d93f7e0ea8b445452dce7185a fmcalc.bin 06fb225398abc1b693854041f2497f79278df3d9 fmcalc.csv +021abfdb4f76c0643c19a3db3bc5f77755928a40 fmcalc_case1_alloc1.csv +08faec684a4967ae802b93246b93cb9424b284d8 fmcalc_case1_alloc2.csv +08faec684a4967ae802b93246b93cb9424b284d8 fmcalc_case1_alloc3.csv +a2d4c29bac3a48972d9b1245a7243a4147fc163d fmcalc_case2_alloc1.csv +3f6abfc8330463d8557abfc3436ffd0d7694c8ac fmcalc_case2_alloc2.csv +3f6abfc8330463d8557abfc3436ffd0d7694c8ac fmcalc_case2_alloc3.csv 0c17942d2c2dfbc31082487298974dfebdd88d9d fmcalt1.csv eb8323a057144157481d0a650cf0528c43658fa6 fmcalt2.csv 031adbec3606c91274e62354575563381d384bc1 fmelt1.csv diff --git a/ktest/runtests.sh b/ktest/runtests.sh index 171e32ea..89d6968b 100755 --- a/ktest/runtests.sh +++ b/ktest/runtests.sh @@ -61,7 +61,21 @@ installertest() # test fmcalc ../src/fmcalc/fmcalc > ../ktest/testout/fmcalc.bin < ../ktest/testout/gulcalci.bin - + + # test fmcalc back allocation rules on multi level, multi layer structures + # see examples/fm_alloc/README.md + for case in case1 case2; do + mkdir -p ../ktest/testout/fm_alloc/$case + ../src/fmprogrammetobin/fmprogrammetobin < fm_alloc/$case/fm_programme.csv > ../ktest/testout/fm_alloc/$case/fm_programme.bin + ../src/fmpolicytctobin/fmpolicytctobin < fm_alloc/$case/fm_policytc.csv > ../ktest/testout/fm_alloc/$case/fm_policytc.bin + ../src/fmprofiletobin/fmprofiletobin < fm_alloc/$case/fm_profile.csv > ../ktest/testout/fm_alloc/$case/fm_profile.bin + ../src/fmxreftobin/fmxreftobin < fm_alloc/$case/fm_xref.csv > ../ktest/testout/fm_alloc/$case/fm_xref.bin + ../src/gultobin/gultobin -S2 < fm_alloc/$case/guls.csv > ../ktest/testout/fm_alloc/$case/guls.bin + for allocrule in 1 2 3; do + ../src/fmcalc/fmcalc -p ../ktest/testout/fm_alloc/$case -a$allocrule < ../ktest/testout/fm_alloc/$case/guls.bin | ../src/fmtocsv/fmtocsv > ../ktest/testout/fmcalc_${case}_alloc$allocrule.csv + done + done + # test summary samples ../src/summarycalc/summarycalc -i -1 ../ktest/testout/gulsummarycalc1.bin < ../ktest/testout/gulcalci.bin ../src/summarycalc/summarycalc -i -2 ../ktest/testout/gulsummarycalc2.bin < ../ktest/testout/gulcalci.bin diff --git a/src/fmcalc/fmcalc.cpp b/src/fmcalc/fmcalc.cpp index 84c16590..904b4973 100644 --- a/src/fmcalc/fmcalc.cpp +++ b/src/fmcalc/fmcalc.cpp @@ -249,6 +249,42 @@ void fmcalc::compute_item_proportions(std::vector 2) { + // allocrule 2 only computes proportions at the top level, so this layer's proportions for the previous level may not exist yet + vector &previous_layer_agg_vec = agg_vecs[level_ - 1][previous_layer_]; + bool item_props_missing = false; + for (size_t i = 0; i < previous_layer_agg_vec.size(); i++) { + if (previous_layer_agg_vec[i].item_idx != nullptr && previous_layer_agg_vec[i].item_prop == nullptr) { + item_props_missing = true; + break; + } + } + if (item_props_missing) { + unsigned int previous_previous_layer = 1; + if (previous_layer_ <= (unsigned int) level_to_max_layer_[level_ - 2]) { + previous_previous_layer = previous_layer_; + } + compute_item_proportions(agg_vecs, guls, level_ - 1, previous_layer_, previous_previous_layer, allowzeros); + } + } + else { + // first level proportions are the ground up loss shares and are the same for every layer, but layer 1 skips aggregations that had no loss + vector &first_agg_vec = agg_vecs[1][1]; + for (size_t i = 0; i < first_agg_vec.size(); i++) { + LossRec &first = first_agg_vec[i]; + if (first.item_idx == nullptr || first.item_prop != nullptr) continue; + OASIS_FLOAT gul_total = 0; + for (int idx : *(first.item_idx)) gul_total += guls[idx]; + first.item_prop = std::make_shared>(std::vector(first.item_idx->size(), 0)); + if (gul_total > 0) { + size_t j = 0; + for (int idx : *(first.item_idx)) { + first.item_prop->at(j) = guls[idx] / gul_total; + j++; + } + } + } + } vector &prev_agg_vec = agg_vecs[level_ - 1][previous_layer_]; vector &prev_agg_vec_base = agg_vecs[level_ - 1][1]; for (size_t i = 0; i < prev_agg_vec.size(); i++) { From c1bbe254c3d6c494d68ad2a7a8ce10126900e6fd Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 2 Sep 2026 13:57:07 +0100 Subject: [PATCH 2/4] Add the fm_alloc test fixtures missed by the examples csv ignore rule runtests.sh and ctrl.sha1 already reference these files. Co-Authored-By: Claude Opus 5 (1M context) --- examples/.gitignore | 1 + examples/fm_alloc/case1/fm_policytc.csv | 15 ++++++++++++++ examples/fm_alloc/case1/fm_profile.csv | 4 ++++ examples/fm_alloc/case1/fm_programme.csv | 11 ++++++++++ examples/fm_alloc/case1/fm_xref.csv | 9 ++++++++ examples/fm_alloc/case1/guls.csv | 17 ++++++++++++++++ examples/fm_alloc/case2/fm_policytc.csv | 19 +++++++++++++++++ examples/fm_alloc/case2/fm_profile.csv | 12 +++++++++++ examples/fm_alloc/case2/fm_programme.csv | 11 ++++++++++ examples/fm_alloc/case2/fm_xref.csv | 16 +++++++++++++++ examples/fm_alloc/case2/guls.csv | 26 ++++++++++++++++++++++++ 11 files changed, 141 insertions(+) create mode 100644 examples/fm_alloc/case1/fm_policytc.csv create mode 100644 examples/fm_alloc/case1/fm_profile.csv create mode 100644 examples/fm_alloc/case1/fm_programme.csv create mode 100644 examples/fm_alloc/case1/fm_xref.csv create mode 100644 examples/fm_alloc/case1/guls.csv create mode 100644 examples/fm_alloc/case2/fm_policytc.csv create mode 100644 examples/fm_alloc/case2/fm_profile.csv create mode 100644 examples/fm_alloc/case2/fm_programme.csv create mode 100644 examples/fm_alloc/case2/fm_xref.csv create mode 100644 examples/fm_alloc/case2/guls.csv diff --git a/examples/.gitignore b/examples/.gitignore index f9a2c4bf..bdb64b9d 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -21,3 +21,4 @@ getmodel.bin gul_mode2_items2.bin log !static/validation/* +!fm_alloc/*/*.csv diff --git a/examples/fm_alloc/case1/fm_policytc.csv b/examples/fm_alloc/case1/fm_policytc.csv new file mode 100644 index 00000000..361e7f13 --- /dev/null +++ b/examples/fm_alloc/case1/fm_policytc.csv @@ -0,0 +1,15 @@ +layer_id,level_id,agg_id,profile_id +1,1,1,2 +1,1,2,1 +1,1,3,2 +1,1,4,1 +2,1,1,1 +2,1,2,2 +2,1,3,1 +2,1,4,2 +1,2,1,1 +1,2,2,1 +2,2,1,1 +2,2,2,1 +1,3,1,3 +2,3,1,3 diff --git a/examples/fm_alloc/case1/fm_profile.csv b/examples/fm_alloc/case1/fm_profile.csv new file mode 100644 index 00000000..92ccb291 --- /dev/null +++ b/examples/fm_alloc/case1/fm_profile.csv @@ -0,0 +1,4 @@ +profile_id,calcrule_id,deductible1,deductible2,deductible3,attachment1,limit1,share1,share2,share3 +1,100,0,0,0,0,0,0,0,0 +2,14,0,0,0,0,500,0,0,0 +3,14,0,0,0,0,4000,0,0,0 diff --git a/examples/fm_alloc/case1/fm_programme.csv b/examples/fm_alloc/case1/fm_programme.csv new file mode 100644 index 00000000..90531b9a --- /dev/null +++ b/examples/fm_alloc/case1/fm_programme.csv @@ -0,0 +1,11 @@ +from_agg_id,level_id,to_agg_id +1,1,1 +2,1,2 +3,1,3 +4,1,4 +1,2,1 +2,2,1 +3,2,2 +4,2,2 +1,3,1 +2,3,1 diff --git a/examples/fm_alloc/case1/fm_xref.csv b/examples/fm_alloc/case1/fm_xref.csv new file mode 100644 index 00000000..894c47b6 --- /dev/null +++ b/examples/fm_alloc/case1/fm_xref.csv @@ -0,0 +1,9 @@ +output_id,agg_id,layer_id +1,1,1 +2,1,2 +3,2,1 +4,2,2 +5,3,1 +6,3,2 +7,4,1 +8,4,2 diff --git a/examples/fm_alloc/case1/guls.csv b/examples/fm_alloc/case1/guls.csv new file mode 100644 index 00000000..e87e8fe4 --- /dev/null +++ b/examples/fm_alloc/case1/guls.csv @@ -0,0 +1,17 @@ +event_id,item_id,sidx,loss +1,1,-3,1000 +1,1,-2,0 +1,1,-1,0 +1,1,1,1000 +1,2,-3,3000 +1,2,-2,0 +1,2,-1,0 +1,2,1,3000 +1,3,-3,3000 +1,3,-2,0 +1,3,-1,0 +1,3,1,3000 +1,4,-3,1000 +1,4,-2,0 +1,4,-1,0 +1,4,1,1000 diff --git a/examples/fm_alloc/case2/fm_policytc.csv b/examples/fm_alloc/case2/fm_policytc.csv new file mode 100644 index 00000000..abbb99c0 --- /dev/null +++ b/examples/fm_alloc/case2/fm_policytc.csv @@ -0,0 +1,19 @@ +layer_id,level_id,agg_id,profile_id +1,1,1,10 +2,1,1,1 +3,1,1,10 +1,1,2,5 +2,1,2,4 +3,1,2,9 +1,1,3,4 +2,1,3,2 +3,1,3,1 +1,1,4,8 +2,1,4,7 +3,1,4,4 +1,1,5,5 +2,1,5,3 +3,1,5,9 +1,2,1,7 +2,2,1,6 +3,2,1,9 diff --git a/examples/fm_alloc/case2/fm_profile.csv b/examples/fm_alloc/case2/fm_profile.csv new file mode 100644 index 00000000..91f138e6 --- /dev/null +++ b/examples/fm_alloc/case2/fm_profile.csv @@ -0,0 +1,12 @@ +profile_id,calcrule_id,deductible1,deductible2,deductible3,attachment1,limit1,share1,share2,share3 +1,100,0,0,0,0,0,0,0,0 +2,1,1000,0,0,0,8000,0,0,0 +3,100,0,0,0,0,0,0,0,0 +4,12,200,0,0,0,0,0,0,0 +5,12,1000,0,0,0,0,0,0,0 +6,12,0,0,0,0,0,0,0,0 +7,1,200,0,0,0,500,0,0,0 +8,100,0,0,0,0,0,0,0,0 +9,100,0,0,0,0,0,0,0,0 +10,1,1000,0,0,0,500,0,0,0 +11,100,0,0,0,0,0,0,0,0 diff --git a/examples/fm_alloc/case2/fm_programme.csv b/examples/fm_alloc/case2/fm_programme.csv new file mode 100644 index 00000000..9c6c5235 --- /dev/null +++ b/examples/fm_alloc/case2/fm_programme.csv @@ -0,0 +1,11 @@ +from_agg_id,level_id,to_agg_id +1,1,4 +2,1,2 +3,1,3 +4,1,5 +5,1,1 +1,2,1 +2,2,1 +3,2,1 +4,2,1 +5,2,1 diff --git a/examples/fm_alloc/case2/fm_xref.csv b/examples/fm_alloc/case2/fm_xref.csv new file mode 100644 index 00000000..8e817812 --- /dev/null +++ b/examples/fm_alloc/case2/fm_xref.csv @@ -0,0 +1,16 @@ +output_id,agg_id,layer_id +1,1,1 +2,1,2 +3,1,3 +4,2,1 +5,2,2 +6,2,3 +7,3,1 +8,3,2 +9,3,3 +10,4,1 +11,4,2 +12,4,3 +13,5,1 +14,5,2 +15,5,3 diff --git a/examples/fm_alloc/case2/guls.csv b/examples/fm_alloc/case2/guls.csv new file mode 100644 index 00000000..434fc95f --- /dev/null +++ b/examples/fm_alloc/case2/guls.csv @@ -0,0 +1,26 @@ +event_id,item_id,sidx,loss +1,1,-3,1000 +1,1,-2,0 +1,1,-1,0 +1,1,1,1000 +1,1,2,1000 +1,2,-3,3000 +1,2,-2,0 +1,2,-1,0 +1,2,1,3000 +1,2,2,3000 +1,3,-3,5000 +1,3,-2,0 +1,3,-1,0 +1,3,1,5000 +1,3,2,5000 +1,4,-3,500 +1,4,-2,0 +1,4,-1,0 +1,4,1,500 +1,4,2,500 +1,5,-3,100 +1,5,-2,0 +1,5,-1,0 +1,5,1,100 +1,5,2,100 From 81ffec2226771e0f160e41c2eb846bc5729811b8 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 14 Sep 2026 16:12:21 +0100 Subject: [PATCH 3/4] Compute per-layer proportions at every level, not just level 1 compute_item_proportions had a second branch with the same defect as the one already fixed on this branch: where the level below does not carry the current layer, it copied layer 1's item proportions at the same level instead of computing them. Restrict that shortcut to level 1, which is the only level with no level below to compute from; at any higher level the proportions are computable from agg_vecs[level_ - 1][1], which every layer above it shares. This removes the two problems the earlier commits left behind: - the null dereference, for the shape where an intermediate level has more layers than the level beneath it and the recursion terminates in the copy branch with layer 1's proportions absent - a whole layer's loss being dropped where layer 1's proportions at that level are all zeros, so every other layer copied the zeros and allocated nothing The second of these affects alloc rule 3 as well as rule 2, so rule 3 is no longer untouched by this branch. Over 300 randomly generated multi-layer structures, back-allocated losses now sum to the -a0 gross for every layer under -a1, -a2 and -a3, with no crashes; before this commit -a2 crashed on 14 and lost loss on 10, and -a3 lost loss on 19. Deleting the branch outright does not work because rule 3 calls into level 1. case3 and case4 pin the two shapes. Without this commit case3 -a2, case4 -a2 and case4 -a3 fail. Co-Authored-By: Claude Opus 5 (1M context) --- examples/fm_alloc/README.md | 36 ++++++++++++++++++++++-- examples/fm_alloc/case3/fm_policytc.csv | 7 +++++ examples/fm_alloc/case3/fm_profile.csv | 4 +++ examples/fm_alloc/case3/fm_programme.csv | 6 ++++ examples/fm_alloc/case3/fm_xref.csv | 5 ++++ examples/fm_alloc/case3/guls.csv | 11 ++++++++ examples/fm_alloc/case4/fm_policytc.csv | 6 ++++ examples/fm_alloc/case4/fm_profile.csv | 4 +++ examples/fm_alloc/case4/fm_programme.csv | 6 ++++ examples/fm_alloc/case4/fm_xref.csv | 5 ++++ examples/fm_alloc/case4/guls.csv | 11 ++++++++ ktest/ctrl.sha1 | 6 ++++ ktest/runtests.sh | 2 +- src/fmcalc/fmcalc.cpp | 2 +- 14 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 examples/fm_alloc/case3/fm_policytc.csv create mode 100644 examples/fm_alloc/case3/fm_profile.csv create mode 100644 examples/fm_alloc/case3/fm_programme.csv create mode 100644 examples/fm_alloc/case3/fm_xref.csv create mode 100644 examples/fm_alloc/case3/guls.csv create mode 100644 examples/fm_alloc/case4/fm_policytc.csv create mode 100644 examples/fm_alloc/case4/fm_profile.csv create mode 100644 examples/fm_alloc/case4/fm_programme.csv create mode 100644 examples/fm_alloc/case4/fm_xref.csv create mode 100644 examples/fm_alloc/case4/guls.csv diff --git a/examples/fm_alloc/README.md b/examples/fm_alloc/README.md index d7d1165d..60da7d80 100644 --- a/examples/fm_alloc/README.md +++ b/examples/fm_alloc/README.md @@ -1,7 +1,7 @@ # fmcalc back-allocation test cases -Two small financial structures for testing the fmcalc back-allocation rules (`-a1`, `-a2`, -`-a3`). Both have more than one policy layer and are run by `ktest/runtests.sh`. +Four small financial structures for testing the fmcalc back-allocation rules (`-a1`, `-a2`, +`-a3`). All have more than one policy layer and are run by `ktest/runtests.sh`. ## case1 - layers with different loss distributions @@ -54,3 +54,35 @@ aggregations with no loss in layer 1 but a loss in a later layer. Alloc rule 2 o in the item proportions for layer 1, and skips the aggregations that had no loss, so the later layers were left with no proportions to allocate with and fmcalc segmentation faulted. The expected output is pinned for all three allocation rules. + +## case3 - a layer with no loss at the top level, with layers below it + +Two items with ground up losses of 1000 and 3000, three levels, a single layer at level 1 and +two layers at levels 2 and 3. Layer 1 is wiped out at the top level by a deductible larger than +the loss; layer 2 is limited to 4000. + +`compute_item_proportions` has a second branch for the case where the level below does not carry +the current layer, and it copied layer 1's proportions *at the same level* rather than computing +them. The walk down the levels that alloc rule 2 performs terminates in that branch, and where +layer 1 has no loss there its proportions are null, so fmcalc dereferenced a null pointer and +segmentation faulted. This case is the regression test for that crash, which showed up under +`-a2` only. + +## case4 - a single layer on the level below a layered level + +The same structure as case3 with a single layer at level 2, so the top level takes the copy +branch directly rather than through the recursion. Layer 1's proportions there are all zeros +rather than null, so every other layer copied the zeros, no item received a share, and layer 2's +whole 4000 loss was dropped instead of being allocated - back-allocated losses summed to zero +against a non-zero layer loss. This affected `-a2` and `-a3` alike, so this case is the +regression test for alloc rule 3 as well. + +In both cases layer 1 back-allocates 0 and layer 2 back-allocates its 4000 as 1000 and 3000, in +ground up proportion, and all three allocation rules agree: + +| output_id | item | layer | -a1, -a2 and -a3 | +|-----------|------|-------|------------------| +| 1 | 1 | 1 | 0 | +| 2 | 1 | 2 | 1000 | +| 3 | 2 | 1 | 0 | +| 4 | 2 | 2 | 3000 | diff --git a/examples/fm_alloc/case3/fm_policytc.csv b/examples/fm_alloc/case3/fm_policytc.csv new file mode 100644 index 00000000..f5b86edf --- /dev/null +++ b/examples/fm_alloc/case3/fm_policytc.csv @@ -0,0 +1,7 @@ +layer_id,level_id,agg_id,profile_id +1,1,1,1 +1,1,2,1 +1,2,1,1 +2,2,1,1 +1,3,1,2 +2,3,1,3 diff --git a/examples/fm_alloc/case3/fm_profile.csv b/examples/fm_alloc/case3/fm_profile.csv new file mode 100644 index 00000000..eae5b5d2 --- /dev/null +++ b/examples/fm_alloc/case3/fm_profile.csv @@ -0,0 +1,4 @@ +profile_id,calcrule_id,deductible1,deductible2,deductible3,attachment1,limit1,share1,share2,share3 +1,100,0,0,0,0,0,0,0,0 +2,12,100000,0,0,0,0,0,0,0 +3,14,0,0,0,0,4000,0,0,0 diff --git a/examples/fm_alloc/case3/fm_programme.csv b/examples/fm_alloc/case3/fm_programme.csv new file mode 100644 index 00000000..ccdf3078 --- /dev/null +++ b/examples/fm_alloc/case3/fm_programme.csv @@ -0,0 +1,6 @@ +from_agg_id,level_id,to_agg_id +1,1,1 +2,1,2 +1,2,1 +2,2,1 +1,3,1 diff --git a/examples/fm_alloc/case3/fm_xref.csv b/examples/fm_alloc/case3/fm_xref.csv new file mode 100644 index 00000000..bb514ecb --- /dev/null +++ b/examples/fm_alloc/case3/fm_xref.csv @@ -0,0 +1,5 @@ +output_id,agg_id,layer_id +1,1,1 +2,1,2 +3,2,1 +4,2,2 diff --git a/examples/fm_alloc/case3/guls.csv b/examples/fm_alloc/case3/guls.csv new file mode 100644 index 00000000..7b786143 --- /dev/null +++ b/examples/fm_alloc/case3/guls.csv @@ -0,0 +1,11 @@ +event_id,item_id,sidx,loss +1,1,-3,1000 +1,1,-2,0 +1,1,-1,0 +1,1,1,1000 +1,1,2,1000 +1,2,-3,3000 +1,2,-2,0 +1,2,-1,0 +1,2,1,3000 +1,2,2,3000 diff --git a/examples/fm_alloc/case4/fm_policytc.csv b/examples/fm_alloc/case4/fm_policytc.csv new file mode 100644 index 00000000..30c7e010 --- /dev/null +++ b/examples/fm_alloc/case4/fm_policytc.csv @@ -0,0 +1,6 @@ +layer_id,level_id,agg_id,profile_id +1,1,1,1 +1,1,2,1 +1,2,1,1 +1,3,1,2 +2,3,1,3 diff --git a/examples/fm_alloc/case4/fm_profile.csv b/examples/fm_alloc/case4/fm_profile.csv new file mode 100644 index 00000000..eae5b5d2 --- /dev/null +++ b/examples/fm_alloc/case4/fm_profile.csv @@ -0,0 +1,4 @@ +profile_id,calcrule_id,deductible1,deductible2,deductible3,attachment1,limit1,share1,share2,share3 +1,100,0,0,0,0,0,0,0,0 +2,12,100000,0,0,0,0,0,0,0 +3,14,0,0,0,0,4000,0,0,0 diff --git a/examples/fm_alloc/case4/fm_programme.csv b/examples/fm_alloc/case4/fm_programme.csv new file mode 100644 index 00000000..ccdf3078 --- /dev/null +++ b/examples/fm_alloc/case4/fm_programme.csv @@ -0,0 +1,6 @@ +from_agg_id,level_id,to_agg_id +1,1,1 +2,1,2 +1,2,1 +2,2,1 +1,3,1 diff --git a/examples/fm_alloc/case4/fm_xref.csv b/examples/fm_alloc/case4/fm_xref.csv new file mode 100644 index 00000000..bb514ecb --- /dev/null +++ b/examples/fm_alloc/case4/fm_xref.csv @@ -0,0 +1,5 @@ +output_id,agg_id,layer_id +1,1,1 +2,1,2 +3,2,1 +4,2,2 diff --git a/examples/fm_alloc/case4/guls.csv b/examples/fm_alloc/case4/guls.csv new file mode 100644 index 00000000..7b786143 --- /dev/null +++ b/examples/fm_alloc/case4/guls.csv @@ -0,0 +1,11 @@ +event_id,item_id,sidx,loss +1,1,-3,1000 +1,1,-2,0 +1,1,-1,0 +1,1,1,1000 +1,1,2,1000 +1,2,-3,3000 +1,2,-2,0 +1,2,-1,0 +1,2,1,3000 +1,2,2,3000 diff --git a/ktest/ctrl.sha1 b/ktest/ctrl.sha1 index 074f9bc8..98d1edcd 100644 --- a/ktest/ctrl.sha1 +++ b/ktest/ctrl.sha1 @@ -68,6 +68,12 @@ d0e7b6b6a14dbc1d93f7e0ea8b445452dce7185a fmcalc.bin a2d4c29bac3a48972d9b1245a7243a4147fc163d fmcalc_case2_alloc1.csv 3f6abfc8330463d8557abfc3436ffd0d7694c8ac fmcalc_case2_alloc2.csv 3f6abfc8330463d8557abfc3436ffd0d7694c8ac fmcalc_case2_alloc3.csv +f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case3_alloc1.csv +f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case3_alloc2.csv +f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case3_alloc3.csv +f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case4_alloc1.csv +f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case4_alloc2.csv +f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case4_alloc3.csv 0c17942d2c2dfbc31082487298974dfebdd88d9d fmcalt1.csv eb8323a057144157481d0a650cf0528c43658fa6 fmcalt2.csv 031adbec3606c91274e62354575563381d384bc1 fmelt1.csv diff --git a/ktest/runtests.sh b/ktest/runtests.sh index 89d6968b..8ad1daf2 100755 --- a/ktest/runtests.sh +++ b/ktest/runtests.sh @@ -64,7 +64,7 @@ installertest() # test fmcalc back allocation rules on multi level, multi layer structures # see examples/fm_alloc/README.md - for case in case1 case2; do + for case in case1 case2 case3 case4; do mkdir -p ../ktest/testout/fm_alloc/$case ../src/fmprogrammetobin/fmprogrammetobin < fm_alloc/$case/fm_programme.csv > ../ktest/testout/fm_alloc/$case/fm_programme.bin ../src/fmpolicytctobin/fmpolicytctobin < fm_alloc/$case/fm_policytc.csv > ../ktest/testout/fm_alloc/$case/fm_policytc.bin diff --git a/src/fmcalc/fmcalc.cpp b/src/fmcalc/fmcalc.cpp index 904b4973..c689da78 100644 --- a/src/fmcalc/fmcalc.cpp +++ b/src/fmcalc/fmcalc.cpp @@ -238,7 +238,7 @@ void fmcalc::compute_item_proportions(std::vector &prev_agg_vec = agg_vecs[level_][1]; vector ¤t_agg_vec = agg_vecs[level_][layer_]; size_t iMax = prev_agg_vec.size(); From 8d5ccbec22e6213a9635533795021b75bc9a031e Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 14 Sep 2026 16:31:57 +0100 Subject: [PATCH 4/4] Skip items with no aggregation at the previous level compute_item_proportions builds an item to previous-level-index lookup from agg_vecs[level_ - 1][previous_layer_], leaving -1 for any item that vector does not cover. That -1 was used unchecked: once when inserting into the set used to total the previous level's loss, and once when reading back item_idx to find the item's position. Both read out of bounds, and the second dereferenced a null item_idx and segmentation faulted. An item is left unmapped when the previous level's aggregation ids are not contiguous, so the loss vector holds entries that no item maps to. Released 3.12.4 faults on this under -a3; the per-layer recursion added earlier on this branch reaches the same code from more places and made it fault under -a2 too, so this is a regression on this branch as well as a pre-existing crash. Unmapped items now contribute nothing to the previous level total and take no share, which is the only meaning available - there is no prior level loss to allocate in proportion to. On the reduced structure in case5 both -a2 and -a3 now back-allocate the full layer gross of 520.00 and 92.50. case5 is that structure, reduced from the OasisLMF#2055 client data by dropping items until the fault stopped reproducing. Without this commit case5 -a2 and -a3 fail; 3.12.4 fails case5 -a3. Co-Authored-By: Claude Opus 5 (1M context) --- examples/fm_alloc/README.md | 22 +++++++++++++++++++++- examples/fm_alloc/case5/fm_policytc.csv | 22 ++++++++++++++++++++++ examples/fm_alloc/case5/fm_profile.csv | 12 ++++++++++++ examples/fm_alloc/case5/fm_programme.csv | 8 ++++++++ examples/fm_alloc/case5/fm_xref.csv | 5 +++++ examples/fm_alloc/case5/guls.csv | 9 +++++++++ ktest/ctrl.sha1 | 3 +++ ktest/runtests.sh | 2 +- src/fmcalc/fmcalc.cpp | 4 ++-- 9 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 examples/fm_alloc/case5/fm_policytc.csv create mode 100644 examples/fm_alloc/case5/fm_profile.csv create mode 100644 examples/fm_alloc/case5/fm_programme.csv create mode 100644 examples/fm_alloc/case5/fm_xref.csv create mode 100644 examples/fm_alloc/case5/guls.csv diff --git a/examples/fm_alloc/README.md b/examples/fm_alloc/README.md index 60da7d80..fb534831 100644 --- a/examples/fm_alloc/README.md +++ b/examples/fm_alloc/README.md @@ -1,6 +1,6 @@ # fmcalc back-allocation test cases -Four small financial structures for testing the fmcalc back-allocation rules (`-a1`, `-a2`, +Five small financial structures for testing the fmcalc back-allocation rules (`-a1`, `-a2`, `-a3`). All have more than one policy layer and are run by `ktest/runtests.sh`. ## case1 - layers with different loss distributions @@ -86,3 +86,23 @@ ground up proportion, and all three allocation rules agree: | 2 | 1 | 2 | 1000 | | 3 | 2 | 1 | 0 | | 4 | 2 | 2 | 3000 | + +## case5 - an item with no aggregation at the previous level + +Two items, four levels, four layers, reduced from the client structure in +[#2055](https://github.com/OasisLMF/OasisLMF/issues/2055) by dropping items until the fault +stopped reproducing. The aggregation ids at level 1 are not contiguous - aggs 1 and 4 carry no +items - so the level 1 loss vector holds entries that no item maps to, and the item to +previous-level-index lookup yields -1 for the items that do carry loss. That -1 was then used to +index the previous level's vector, both when totalling the previous level's loss and when reading +back `item_idx`, so fmcalc read out of bounds and segmentation faulted on a null `item_idx`. + +Released 3.12.4 faults here under `-a3`, and the per-layer recursion added earlier on this branch +made it fault under `-a2` as well. Both are fixed by skipping items that have no entry at the +previous level. + +`-a2` and `-a3` back-allocate the whole of each layer's loss - 520.00 for layer 1 and 92.50 for +layer 2, matching the `-a0` gross. `-a1` allocates only 54.41 to layer 2 on this structure, which +does not match its gross; that is a separate pre-existing problem in the rule 1 path and is out of +scope here. The `-a1` expected output is pinned so that a change in it is noticed, not because it +is correct. diff --git a/examples/fm_alloc/case5/fm_policytc.csv b/examples/fm_alloc/case5/fm_policytc.csv new file mode 100644 index 00000000..8c417eb2 --- /dev/null +++ b/examples/fm_alloc/case5/fm_policytc.csv @@ -0,0 +1,22 @@ +layer_id,level_id,agg_id,profile_id +1,1,1,2 +2,1,1,3 +1,1,2,3 +2,1,2,2 +1,1,3,3 +3,1,3,2 +1,1,4,3 +4,1,4,2 +1,2,1,4 +2,2,1,5 +3,2,1,5 +1,2,2,1 +4,2,2,1 +1,3,1,6 +2,3,1,7 +3,3,1,7 +4,3,1,7 +1,4,1,8 +2,4,1,9 +3,4,1,10 +4,4,1,11 diff --git a/examples/fm_alloc/case5/fm_profile.csv b/examples/fm_alloc/case5/fm_profile.csv new file mode 100644 index 00000000..7109b3dd --- /dev/null +++ b/examples/fm_alloc/case5/fm_profile.csv @@ -0,0 +1,12 @@ +profile_id,calcrule_id,deductible1,deductible2,deductible3,attachment1,limit1,share1,share2,share3 +1,100,0,0,0,0,0,0,0,0 +2,14,0,0,0,0,1000000000,0,0,0 +3,14,0,0,0,0,100000,0,0,0 +4,12,25000,0,0,0,0,0,0,0 +5,100,0,0,0,0,0,0,0,0 +6,100,0,0,0,0,0,0,0,0 +7,12,50000,0,0,0,0,0,0,0 +8,2,0,0,0,0,1000000000,0.02,0,0 +9,2,0,0,0,0,1000000000,0.0925,0,0 +10,2,0,0,0,0,1000000000,0.1675,0,0 +11,2,0,0,0,0,1000000000,0.044,0,0 diff --git a/examples/fm_alloc/case5/fm_programme.csv b/examples/fm_alloc/case5/fm_programme.csv new file mode 100644 index 00000000..58260289 --- /dev/null +++ b/examples/fm_alloc/case5/fm_programme.csv @@ -0,0 +1,8 @@ +from_agg_id,level_id,to_agg_id +1,1,2 +2,1,3 +2,2,1 +3,2,1 +1,3,1 +2,3,1 +1,4,1 diff --git a/examples/fm_alloc/case5/fm_xref.csv b/examples/fm_alloc/case5/fm_xref.csv new file mode 100644 index 00000000..f48152cf --- /dev/null +++ b/examples/fm_alloc/case5/fm_xref.csv @@ -0,0 +1,5 @@ +output_id,agg_id,layer_id +1,1,1 +2,1,2 +3,2,1 +4,2,3 diff --git a/examples/fm_alloc/case5/guls.csv b/examples/fm_alloc/case5/guls.csv new file mode 100644 index 00000000..279df673 --- /dev/null +++ b/examples/fm_alloc/case5/guls.csv @@ -0,0 +1,9 @@ +event_id,item_id,sidx,loss +1,1,-3,30000 +1,1,-2,0 +1,1,-1,0 +1,1,1,30000 +1,2,-3,21000 +1,2,-2,0 +1,2,-1,0 +1,2,1,21000 diff --git a/ktest/ctrl.sha1 b/ktest/ctrl.sha1 index 98d1edcd..afdfa7b8 100644 --- a/ktest/ctrl.sha1 +++ b/ktest/ctrl.sha1 @@ -74,6 +74,9 @@ f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case3_alloc3.csv f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case4_alloc1.csv f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case4_alloc2.csv f2dc19f02eef6b1e24b53fe454718d3aeb9d17f8 fmcalc_case4_alloc3.csv +0aa66b40fe566610fdf23aba0ec4e6fd85f689b4 fmcalc_case5_alloc1.csv +19fb2d7d30b45ca0b19fbb9288756ceefb306f02 fmcalc_case5_alloc2.csv +19fb2d7d30b45ca0b19fbb9288756ceefb306f02 fmcalc_case5_alloc3.csv 0c17942d2c2dfbc31082487298974dfebdd88d9d fmcalt1.csv eb8323a057144157481d0a650cf0528c43658fa6 fmcalt2.csv 031adbec3606c91274e62354575563381d384bc1 fmelt1.csv diff --git a/ktest/runtests.sh b/ktest/runtests.sh index 8ad1daf2..d01d6a57 100755 --- a/ktest/runtests.sh +++ b/ktest/runtests.sh @@ -64,7 +64,7 @@ installertest() # test fmcalc back allocation rules on multi level, multi layer structures # see examples/fm_alloc/README.md - for case in case1 case2 case3 case4; do + for case in case1 case2 case3 case4 case5; do mkdir -p ../ktest/testout/fm_alloc/$case ../src/fmprogrammetobin/fmprogrammetobin < fm_alloc/$case/fm_programme.csv > ../ktest/testout/fm_alloc/$case/fm_programme.bin ../src/fmpolicytctobin/fmpolicytctobin < fm_alloc/$case/fm_policytc.csv > ../ktest/testout/fm_alloc/$case/fm_policytc.bin diff --git a/src/fmcalc/fmcalc.cpp b/src/fmcalc/fmcalc.cpp index c689da78..30bc69ce 100644 --- a/src/fmcalc/fmcalc.cpp +++ b/src/fmcalc/fmcalc.cpp @@ -319,7 +319,7 @@ void fmcalc::compute_item_proportions(std::vector s; while (it != agg_vecs[level_][layer_][y].item_idx->end()) { - s.insert(v[*it]); + if (v[*it] >= 0) s.insert(v[*it]); //prev_gul_total += prev_agg_vec[*it].loss; it++; } @@ -337,7 +337,7 @@ void fmcalc::compute_item_proportions(std::vector>(std::vector()); } - if (prev_gul_total > 0) { + if (prev_gul_total > 0 && v[*it] >= 0 && prev_agg_vec[v[*it]].item_idx != nullptr) { int j = -1; const std::vector& z = *(prev_agg_vec[v[*it]].item_idx); for (size_t i = 0; i < z.size(); i++) {