KHE diary for 2025
==================

At the end of 2024 I was working on task grouping by resource
constraints, finishing off the dynamic programming algorithm.

31 December 2024.  Now have a new method of finding group
  costs that does not call the combinatorial grouper at all.
  Fixed some bugs but now it seems to be working:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01605 ]

  Not a bad cost actually; but still I need to rethink how
  profile grouping, as now redone, fits into things generally.

1 January 2025.  Finished adding optional tasks:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01655 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01610 0.01730 0.01745 0.01755 0.01780 0.01825 0.01840 0.01850
      0.01865 0.01875 0.01910 0.01915
    ]

  Not a great result, but there is a lot to look into.  I could be
  doing brilliantly on Constraint:17 but doing nothing on the others.
  Here are the details of the 1610 solution:

                                     LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     24       28
    O  Available times (negative)     43       56
    Y  Unnecessary assignments        19       25
    X  Unassigned tasks                5        2
    ---------------------------------------------
    U - O + Y - X                     -5       -5

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150      60
    Avoid Unavailable Times Constraint (7 points)    	 70      70
    Cluster Busy Times Constraint (28 points) 	   	950    1240
    Limit Active Intervals Constraint (3 points) 	 75     240
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1610

  I was often getting limit active intervals costs over 300, so there
  has been some improvement there, but there are still Cnstraint:17
  defects, and other things have blown out.  Our recent best result
  (on 30 Nov 2024, running for 30 minutes) was

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01485 0.01510 0.01530 0.01535 0.01555 0.01570 0.01570 0.01610
      0.01615 0.01620 0.01620 0.01645
    ]

  On 3 November 2024 the best 5 minute run was producing

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.1 mins:
      0.01560 0.01700 0.01710 0.01740 0.01745 0.01755 0.01755 0.01810
      0.01830 0.01845 0.01855 0.01895
    ]

  although the second best is not convincing.  Altogether 1610 does not
  seem so bad, although it is rel = 1.29, whereas 1485 is rel = 1.19,
  which is much better.  And on we go.

2 January 2025.  I've been looking into whether the second pass
  really does help.  It seems that it does.  Here is the first pass:

    Soln(cost 0.07095, undersized_durn 15, oversized_durn 0)

  and here is the second pass:

    Soln(cost 0.07290, undersized_durn 4, oversized_durn 0)

  So the total duration of undersized groups (not counting optional
  groups or groups right at the end) has been reduced from 15 to 4.
  There were 10 optional tasks grouped with non-optional tasks,
  which makes sense.  Running time of whole thing was 2.3 secs.

  Done a fair bit of tidying up in khe_sr_tgrc_dynamic_profile.c.

  Started working on removing khe_sr_tgrc_profile.c and moving
  any relevant code across to khe_sr_tgrc_dynamic_profile.c.

3 January 2025.  Ditched the term "profile grouping", replacing it with
  "interval grouping".  "DPG" now stands for "dynamic programming grouping".
  Got rid of khe_sr_tgrc_profile.c and moved any relevant code across to
  khe_sr_tgrc_dynamic_profile.c.  Then changed the file name from
  khe_sr_tgrc_dynamic_profile.c to khe_sr_tgrc_interval_grouping.c.

  It's still working, although not brilliantly:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01790 ]

  The debug prints of the results of grouping have not changed.

6 January 2025.  Spent the last couple of days thinking about changing
  optimal assignment by dynamic programming so that when there is just
  one resource it handles grouped tasks optimally.  Today I copied
  khe_sr_dynamic_resource.c into save_khe_sr_dynamic_resource.c and
  then did two things:

  * I wrote KheDrsSolnNotForDominanceTesting, which identifies
    solutions that have a single resource and are part-way through
    a task;

  * I made sure those solutions do not participate in dominance testing.
    by shunting them into a separate list: field non_dominance_solns in
    KHE_DRS_SOLN_SET.

  Is it really all done?  It seems to be.  Here's the first run:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01790 ]

  There are four Constraint:17 defects in my final solution, with
  total cost 90.  This compares pretty well with LOR17, which has
  three Constraint:17 defects with total cost 75.  However I now
  seem to have a lot of other defects.  Still it's progress.  Here's
  another run:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01855 ]

  I'm just at the beginning of looking into what's going on here.
  I've revised timetable printing so that it prints one cell per
  day, not one cell per time, when there is a days frame.

7 January 2025.  Working on the HSEval buffer oveflow bug that I
  found yesterday.  And yes, it really was a bug in my own code,
  allocating one less than the right number of characters.

  Comparing what KheDynamicResourceSequentialSolve did with the
  final solution, there is little or no correlation!  If the
  results of KheDynamicResourceSequentialSolve are optimal,
  why are they being broken up so badly?  Can we fix them
  and see how that goes?

  I've realized that min limit 4 and max limit 5 has a problem:
  groups of length 4 are no use to most resources, because other
  constraints require 2 consecutive Early shifts, 2 consecutive
  Day shifts, and so on, and a group of 4 abutted with a group
  of 2 makes 6, which is over the consecutive days limit.  Of
  course a group of 4 could become an entire group of its own.
  Anyway I tried a hack which builds groups of size 5.  I
  got this result:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01850 ]

  And once again the final solutions seem to bear no resemblance
  to the initial ones.  Taking away the hack, I'm currently getting
  these quite poor results:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01745 0.01795 0.01805 0.01840 0.01845 0.01865 0.01875 0.01895
      0.01905 0.01915 0.02000 0.02040
    ]

  With DEBUG16 from khe_sr_dynamic_vlsn.c on, I get this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01780 0.01800 0.01800 0.01815 0.01840 0.01865 0.01875 0.01910
      0.01910 0.01955 0.01980 0.01985
    ] 

  This seems slightly better, although both results are quite bad.

8 January 2025.  Working on getting fixing sorted out properly.
  I've started by placing the first repair pass inside task
  grouping by resource constraints.  It produced this:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01915 ]

  Let's have a look at a solution which never removes the grouping:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 5.02490 ]

  I've verified that in this solution, Constraint:17 violations have
  total cost 75, which is the same as in LOR17.  So task grouping is
  working, but it is working at the expense of other things.

9 January 2025.  Working on discussion section of solution adjustment.
  Good stuff.

10 January 2025.  I've basically finished the discussion section of
  solution adjustment.  It's ready to implement.

11 January 2025.  Implementing the revised task grouping function.
  Moved type KHE_TASK_GROUPER to KHE_TASK_RESOURCE_GROUPER, to free
  up the KHE_TASK_GROUPER type name for a new task grouper module.

12 January 2025.  I spent the day working on the documentation - the new
  task grouper section.  It is well organized now, and it covers all
  the nasty details.  I've also implemented quite a lot of it; adding
  fixed tasks is still to do, and building the group is still to do.

13 January 2025.  Finished implementing task grouping, including
  assigned tasks, fixed tasks, and undoing using sa - the lot in
  fact.  Not bad given that I started the whole thing on 9 January.

14 January 2025.  Now making make sure that the leader task can be
  assigned to the common parent, if there is a common parent.
  Task grouping is now implemented, audited, documented, and ready
  to use.  I've also updated the mtask finder to use a task grouper.
  Again, all implemented, audited, documnted, and in use.

  Audited fixing and concluded that all is in order.

15 January 2025.  I've revised the task grouper to make
  KHE_TASK_GROUPER_ENTRY into a public type that the user
  can use to interface to task grouping more efficiently.
  I've now used it in interval grouping.  It's all done
  with a clean compile, it just needs a careful audit.

16 January 2025.  Polished khe_sr_task_grouper.c (which is just
  great now) and khe_sr_tgrc_interval_grouping.c, which is less
  great but good enough.  I've reorganized the types somewhat:
  type KHE_DPG_TASK_GROUP from interval grouping now inherits
  KHE_TASK_GROUPER_ENTRY rather than having a field of that type.

  Started testing.  I seem to be using an undefined task value:

    KheTaskAssignUnFix internal error (wrong magic number)

  There may be a logic error in KheMTaskGroupMakeOneGroup
  (file khe_sr_tgrc_group.c).  I've done some debugging
  and it shows that on line 311 of khe_sr_task_grouper.c,
  entry->task is not a valid task.  But I can't for the
  life of me see why not.

17 January 2025.  Testing and debugging the task grouping code.
  I fixed yesterday's bug, I was using pointers into an array,
  which did not work well when the array was resized.  It all
  seems good now, I have a run which shows the interval grouping
  timetables as expected.  First runs:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01710 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01715 0.01730 0.01735 0.01765 0.01780 0.01785 0.01820 0.01840
      0.01910 0.01915 0.01960 0.01990
    ]

  Actually this represents serious progress, because instead of
  about 300 points of limit active intervals defects, I now have
  only 165 points, including only 75 points of Constraint:17
  defects, equalling (at last) the LOR solution in this respect:

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150      60
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1410
    Limit Active Intervals Constraint (3 points) 	 75     165
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1715

  In the KHE solution there are 210 points of max working weekends
  defects (counted in the 1410 points for cluster defects), as
  compared to 90 points in the LOR solution.  So this explains 120
  points of the blowout in cost.  If we were to lose this 120 points
  we would be down to 1595, which would be a very respectable result.
  Although KHE is also (220 - 140) = 80 points ahead in assign
  resource and avoid unavailable times defects, and this presumably
  has to be paid for somewhere.

18 January 2025.  Reviewed the discussion section of the solution
  adjusters section.  I made some changes and it's now all good.
  Removed KheTaskSetUnGroup from code and doc, and updated its
  only current use (khe_sr_group_by_resource.c), replacing it
  by the solution adjuster and task grouper.

  I've removed task bound groups from the solver.  Now I need to
  remove that section of the chapter and renumber.

19 January 2025.  Finished removing task bound groups.  All
  the documentation is fixed up and renumbered.

  Did an off-site backup today.

  Documented void KheSolnAdjusterUndo and KheSolnAdjusterRedo,
  and started work on a new version of khe_soln_adjuster.c
  that supports these operations.

20 January 2025.  Working on KheSolnAdjusterUndo and
  KheSolnAdjusterRedo.  All done with clean compile.

  Worked on khe_sr_task_multi_grouper.c; it's all done
  except KheTaskMultiGrouperMakeGroups.

21 January 2025.  Finished khe_sr_task_multi_grouper.c
  and removed these two old solvers:

  * KHE_TASK_RESOURCE_GROUPER - This is used by resource matching.
    It would be good to get rid of it by adding undo and redo
    operations to the solution adjuster.  However, it makes the
    first task assigned a particular resource the leader of all
    tasks assigned that resource; and it reduces the domains of
    leader tasks to make sure that other tasks are assignable
    to them.  All reasonable, but all rather different from the
    task grouping we do elsewhere.  Still, I could probably
    do without these differences.  How many days does this
    grouping cover?  If just a few, it is effectively the same
    as what follows.

  * KheGroupByResource - similar to KHE_TASK_RESOURCE_GROUPER
    only each run of consecutive tasks assigned the same
    resource is grouped, not all tasks assigned the resource.
    Clearly the two specs are similar and could be combined.
    At present this function is called only by do-it-yourself,
    and my standard "rs" values do not invoke it.  So it is
    effectively unused.  But the kind of grouping carried out
    by interval grouping to start with (grouping tasks that lie
    on adjacent days and are assigned the same resource) is
    exactly this kind of grouping.

22 January 2025.  Finished reorganizing task grouping, except
  that (1) I have not yet looked at the two "Implementation notes"
  sections of the documentation; and (2) I have no yet considered
  what to do about the k*tgrc*.[ch] files:

    jeff  6196 Jan 22 15:24 khe_sr_tgrc.h
    jeff 58098 Dec 30 11:48 khe_sr_tgrc_comb.c
    jeff 38221 Dec  7 11:20 khe_sr_tgrc_elim.c
    jeff 34966 Jan 14 17:19 khe_sr_tgrc_group.c
    jeff 18753 Jan 22 15:19 khe_sr_tgrc_main.c

  These are small sizes e.g khe_sr_interval_grouping.c is 154658,
  which is larger than the sum of these sizes.  They all combine
  to produce a single function (KheCombinatorialGrouping), so they
  do logically belong together.  One option would be to cat them
  all into a file called khe_sr_combinatorial_grouping.c.

23 January 2025.  Testing the revised code.  Currently getting a
  non-obvious core dump (khe_sr_resource_matching.c, line 2113).
  Found and fixed it; I was clearing a free list, not the list
  I wanted to clear.  First results:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01675 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01705 0.01735 0.01765 0.01770 0.01790 0.01805 0.01820 0.01900
      0.01920 0.01920 0.01950 0.01950
    ]

24 January 2025.  Changed solution adjusters to accept an arena.
  It's working now, I got this result:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01690 0.01730 0.01760 0.01795 0.01795 0.01815 0.01825 0.01825
      0.01900 0.01940 0.02005 0.02010
    ]

  Not very good, needs looking into.  On 3 November 2024 the best 5
  minute run was producing 1560, although the second best was 1700.
  Here is a run in which the task grouping only holds during the
  initial construction, not during the first repair phase:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01755 0.01770 0.01775 0.01815 0.01830 0.01870 0.01940 0.01955
      0.02030 0.02055 0.02125 0.02135
    ]

  Not so good, I'm withdrawing this.  Here's a run in which we
  stop after the first repair, so that everything remains grouped:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01900 0.01910 0.01955 0.01980 0.01990 0.02025 0.02040 0.02065
      0.02070 0.02105 0.02175 0.02185
    ]

  Not great, but the point of this run is to see what it produces.
  And there are no Constraint:17 defects at all, although there are
  some history defects from other limit active intervals constraints
  which are probably induced by avoiding the Constraint:17 defects.

  Let's try interval grouping more stuff, by setting
  rs_interval_grouping_min=3.  Also we'll return to repairing
  ungrouped.  Got this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01690 0.01720 0.01790 0.01795 0.01800 0.01815 0.01825 0.01840
      0.01900 0.01920 0.01940 0.02010
    ]

  But was there in fact any extra interval grouping?  No, none.
  Actually none of the other constraints looks to be worth doing
  interval grouping for.  And recalling how Constraint:17 dominates
  the defect list, that's probably right.  So let's focus on only
  that one constraint.

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01675 ]

  is better than the 1690 I was getting as best of 12.  But it is
  still quite a poor result, rel = 1.34.  My earlier 1560 result
  had rel = 1.25.  Trying a longer run now:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01645 0.01665 0.01700 0.01740 0.01740 0.01745 0.01795 0.01815
      0.01825 0.01830 0.01915 0.01965
    ]

  with rel = 1.32.  Underwhelming.  Here is a 30-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01565 0.01610 0.01620 0.01635 0.01670 0.01695 0.01705 0.01710
      0.01710 0.01715 0.01770 0.01835
    ]

  Not bad, rel = 1.25.  OK, back to 5-minute runs now.  Turning off
  sequential gives

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01645 0.01690 0.01795 0.01795 0.01805 0.01815 0.01840 0.01870
      0.01875 0.01945 0.01950 0.02065
    ]

  Not bad for a 5-minute run.  Doing more sequential (0.8):

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01720 0.01725 0.01730 0.01745 0.01750 0.01830 0.01870 0.01875
      0.01895 0.01915 0.01940 0.01960
    ]

  Not much good.  Doing less sequential (0.4):

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01685 0.01715 0.01715 0.01765 0.01775 0.01815 0.01825 0.01910
      0.01935 0.01955 0.01955 0.01960
    ]

  Again, not great.  Here is the default value again (0.6):

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01675 0.01770 0.01795 0.01820 0.01840 0.01850 0.01870 0.01870
      0.01905 0.01915 0.01950 0.01950
    ]

  The differences seem to be more or less random.  Now let's take
  away grouping from the first repair phase:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01650 0.01690 0.01760 0.01765 0.01950 0.01950 0.01970 0.02000
      0.02005 0.02015 0.02030 0.02060
    ]

  And on we go.  This 1650 solution has an incredible 10
  Constraint:17 defects, total cost 14 * 15 = 210.  If we
  add grouping back to the first repair phase, we get

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01690 0.01695 0.01745 0.01775 0.01780 0.01780 0.01920 0.01925
      0.01940 0.01950 0.01960 0.01990
    ]

  The best is worse but the worst is quite a lot better.  So the
  differences are basically random.  But this time we have only
  5 Constraint:17 defects, total cost 7 * 15 = 105.  So this is
  looking like a much better base for further work.

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      60
    Cluster Busy Times Constraint (28 points) 	   	950    1300
    Limit Active Intervals Constraint (3 points) 	 75     210
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1690

  There are 6 * 30 = 180 points worth of complete weekends and busy
  weekends defects in this KHE solution, as against 90 points
  of busy weekends constraints in the LOR solution.  Here is a 30
  minute run with these same settings:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01605 0.01620 0.01635 0.01670 0.01690 0.01705 0.01710 0.01710
      0.01715 0.01770 0.01815 0.01840
    ]

  This is rel = 1.28.  It's not encouraging that this is the best
  we can get to after 30 minutes.

25 January 2025.  As it says above, our starting point has to be
  the cost 1690 (rel = 1.35) solution which preserves the grouping
  through the first repair phase:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01690 0.01745 0.01755 0.01760 0.01780 0.01790 0.01815 0.01825
      0.01920 0.01920 0.01965 0.02035
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      60
    Cluster Busy Times Constraint (28 points) 	   	950    1300
    Limit Active Intervals Constraint (3 points) 	 75     210
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1690

  The total cost of Constraint:17 defects here is 7 * 15 = 105, which
  is reasonably close to LOR's 75.  There are two likely leads:

    * The 180 points of complete weekends and busy weekends defects,
      compared to LOR's 90.

    * The KHE solution has 11 unnecessary assignments in non-trainees,
      whereas the LOR solution has only 1.  (In trainees the two are
      pretty equal, KHE has 19 and LOR has 18.)  If we say that each of
      these ultimately costs 20, that is 200 points.  It goes a long
      way towards explaining why the KHE cluster cost is so much
      higher than the LOR cluster cost.

  If we fixed both of these problems we would have cost 1690 - 90 - 200
  = 1400 (rel 1.12), which would be good enough.

26 January 2025.  Decided to go with the plan for focussing vlsn search
  for cluster defects on resources that are not just available, they
  are under the limit on the constraint that is giving rise to the
  defect.  But I'll do it within the VLSN module, I won't do it in
  the platform for now.

  Implemented in a simple-minded way that searches monitor lists
  for the monitors we need.  Ready to test.

27 January 2025.  Documented the revised definition of "preferred
  resource" when repairing cluster defects.  Finished implementing
  the somewhat revised version of preferred resources for repair
  of cluster defects.  Judging from the debug output the fix is
  not doing very much.  Best of 1:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01690 ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     150
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1220
    Limit Active Intervals Constraint (3 points) 	 75     240
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1690

  but there is just one weekend defect (cost 60), which is better
  than LOR (cost 90), and the Constraint:17 defects are 6 * 15 = 90.
  which is close to LOR's 75.  So this is mission accomplished,
  arguably, but still the 1690 result is just rel = 1.35, because
  of blowouts elsewhere.  In particular, KHE now has 29 italic
  entries, as compared with LOR's 19, which leads indirectly to
  (29 - 19) * 20 = 200 points.  Getting rid of those would get
  us to 1490, which is rel = 1.19.  There are also problems
  with new limit active intervals defects popping up.

  Here is best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01750 0.01760 0.01780 0.01820 0.01835 0.01855 0.01890 0.01895
      0.01920 0.01920 0.01925 0.01955
    ]

  This was a clean run but still it has come out worse:

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     180
    Avoid Unavailable Times Constraint (7 points)    	 70      70
    Cluster Busy Times Constraint (28 points) 	   	950    1230
    Limit Active Intervals Constraint (3 points) 	 75     270
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1750

  Every component is a bit worse.  Constraint:17 is 105, weekends
  are 90, and so on.  Here is a 10-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01660 0.01675 0.01725 0.01740 0.01740 0.01745 0.01765 0.01775
      0.01795 0.01855 0.01880 0.01885
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      70
    Cluster Busy Times Constraint (28 points) 	   	950    1200
    Limit Active Intervals Constraint (3 points) 	 75     270
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1660

  This is rel = 1.33.  Constraint:17 is 9 * 15 = 135, which is too
  high, but weekends are 60, which is less that LOR's 90.  The
  problem now is italic entries, where the excess is (28 - 19) * 20
  = 180.  Getting rid of that would get us to 1660 - 180 = 1480,
  which is rel = 1.18.  But it may be better to aim to improve the
  limit active intervals defects, which (including Constraint:17)
  are costing 270 now compared to LOR's 75.  Anyway I seem to have
  cleaned up the weekends problem.

  Here is another 10-minute run but without rrd:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01680 0.01680 0.01695 0.01725 0.01745 0.01750 0.01760 0.01780
      0.01790 0.01800 0.01805 0.01810
    ]

  About the same.  The worst is quite a lot better.  But we'll
  stick with it for now.  Later we need to cull things that
  don't make a significant difference.  Back to the usual
  5-minute run now:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01760 0.01780 0.01785 0.01795 0.01805 0.01820 0.01825 0.01835
      0.01920 0.01925 0.01975 0.02020
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     180
    Avoid Unavailable Times Constraint (7 points)    	 70     100
    Cluster Busy Times Constraint (28 points) 	   	950    1270
    Limit Active Intervals Constraint (3 points) 	 75     210
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1760

  It's not a great result overall.  But it does have two huge
  positives:  weekend cost 90, and Constraint:17 cost 75.  Both
  are the same as LOR.  The obvious next target is the number
  of italic entries:  30 for KHE as opposed to 19 for LOR, a
  difference in cost of (30 - 19) * 20 = 220.  Getting rid of
  that would bring us to 1760 - 220 = 1540, which is rel = 1.23.
  There are also 210 - 75 = 135 points of non-Constraint:17 limit
  active intervals defects; all but 15 points of those are
  consecutive working days rather than shifts.

  HN_14 has workload limit 20 and workload 17, so there are 3
  shifts available there.  It also has a run of two shifts
  when the minimum is 3.  So its timetable is quite bad.
  Actually LOR has avail=2 here, but there are no bad runs.

  In HSEval, separated planning timetables into sub-resource types
  not just resource types.

  Here is a run with rrd added to the second repair phase as well
  as the first:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01685 0.01710 0.01720 0.01735 0.01785 0.01820 0.01850 0.01865
      0.01885 0.01885 0.01925 0.01950
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      60
    Cluster Busy Times Constraint (28 points) 	   	950    1160
    Limit Active Intervals Constraint (3 points) 	 75     345
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1685

  The point is that we have a lot of italic tasks and it would be
  good to get rid of them.  Quite a big improvement in cost,  The
  total number of italic entries is now 6+19 (the two numbers are
  first nurses and second trainees), as opposed to LOR's 1+18.
  This is a lot better than KHE's previous total of 30, which was
  presumably something like 11+19:  given that we are almost equal
  with LOR on trainees, the improvement is entirely in nurses.

  But there is now a blowout in limit active intervals cost,
  including 8 * 15 = 120 for Constraint:17 and lots of other
  stuff including too many consecutive days off (2 * 30) and
  too few or too many consecutive busy days (5 * 30 = 150).
  If we could reduce that by 200 we would have 1685 - 200 =
  1485 which is 1.19.  These are constraints that we have
  avoided previously, so it should not be impossible.  Here
  is a 10-minute run with the same settings:

    [ "INRC2-4-100-0-1108", 1 solution, in 10.0 mins: cost 0.01655 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01660 0.01690 0.01695 0.01710 0.01720 0.01750 0.01755 0.01770
      0.01775 0.01780 0.01800 0.01805
    ]

  The best is only slightly better, the worst is a lot better.  But
  once again a single-threaded run has done better than 12 threads,
  although only marginally so.  Just one weekend defect, 9+19 italic.

28 January 2025.  Revised the prefer resources case of targeted
  VLSN search to prefer overloaded resources.  First results:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01705 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01725 0.01740 0.01745 0.01800 0.01810 0.01820 0.01865 0.01885
      0.01895 0.01905 0.01915 0.01945
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      70
    Cluster Busy Times Constraint (28 points) 	   	950    1250
    Limit Active Intervals Constraint (3 points) 	 75     285
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1725

  This is quite a lot worse than the previous 5-minute run.
  Constraint:17 cost is 90, weekends are 90 (both OK).  Italic
  entries are 9+17 - worse than the 6+19 we had before.  Although
  17 for trainees is better than LOR's 18 (1+18 altogether). so
  there is a first there.  All kinds of things are better than
  ever, but the total is not good at all.  Here's a 10-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01670 0.01690 0.01715 0.01725 0.01730 0.01730 0.01755 0.01770
      0.01775 0.01790 0.01800 0.01805
    ]

  This is much the same as the previous 10-minute run.  I think
  I need to return to the 5-minute run and then keep grinding.
  I'm getting blowouts in limit active intervals defects other
  than Constraint:17, as well as overall workload.  Here is
  the latest 5-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01720 0.01750 0.01770 0.01790 0.01795 0.01810 0.01820 0.01835
      0.01915 0.01930 0.01945 0.01960
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     150
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1250
    Limit Active Intervals Constraint (3 points) 	 75     245
    ---------------------------------------------------------------
      Grand total                 	 	       1245    1720

  Weekends are 90, Constraint:17 is 90, both reasonable.  Italic is
  9+18, LOR's is 1+18, giving delta cost (9+18 - 1+18) * 20 = 160.
  The other problem fuelling the difference of 300 in the cluster
  cost is negatives:
                                     LOR      KHE
    ---------------------------------------------
    U  Available times (positive)   20+4     22+9
    O  Available times (negative)   36+7    46+12
    Y  Unnecessary assignments      1+18     9+18
    X  Unassigned tasks              5+0      5+0
    ---------------------------------------------
    U - O + Y - X                     -5       -5

  The difference in U + Y is (58 - 43) * 20 = 300.

  Here's a 60-minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 60.0 mins:
      0.01535 0.01585 0.01605 0.01625 0.01650 0.01675 0.01685 0.01700
      0.01710 0.01720 0.01725 0.01730
    ]

  This is rel = 1.23.  It's interesting, Constrain:17 has cost 120,
  weekends have cost 60, and the four components are 90, 80, 1200,
  and 165.  It's noticeable that the 1200 is still far above LOR.
  So let's look at that:

                                     LOR      KHE (60 minutes)
    ---------------------------------------------
    U  Available times (positive)   20+4     22+6
    O  Available times (negative)   36+7    47+10
    Y  Unnecessary assignments      1+18     8+19
    X  Unassigned tasks              5+0      3+0
    ---------------------------------------------
    U - O + Y - X                     -5       -5

  With the difference in U + Y being (22+6 + 8+19) - (20+4 + 1+18)
  = (28 + 27) - (24 + 19) = 55 - 43 = 12 * 20 = 240.  Overloaded
  resources are holding this solution back, in short.

  In the 60-minute solution, on 4Sat and 4Sun, there is a simple
  repair that would reduce cost.  Unassigned TR_75, unassign
  TR_88, and assign TR+75's 4Sun night shift to TR_88.  Why is
  that not being done?  It would save 40 points.  We get it in
  a 5-minute run as well, diversifier 4, although in that case
  it would not save any actual points, at least, not directly.
  It's not done because TR_75 requires sequences of consecutive
  busy days to have length at least 4, and this repair would
  give one sequence of length 2.  A whynot run has shown this
  and also that it is tried by ejection chains.

  Tracing combinatorial grouping.  It is doing some weekend
  grouping but it does not seem to be grouping Late with Night
  or Day with Night - not sure why yet.  Here's the debug output:

    [ KheArchiveParallelSolve(INRC2-4-100-0-1108) soln_group KHE24x1
      threads 1, make 1, keep 1, time omit, limit -1.0 secs, use_cache false)
      parallel solve of INRC2-4-100-0-1108: starting solve 1 (last)
      combinatorial grouping: 3 x {cost 0.00000: |1Sat:Night.10|, 1Sun:Night.10}
      combinatorial grouping: 3 x {cost 0.00000: |1Sat:Late.10|, 1Sun:Late.10}
      combinatorial grouping: 3 x {cost 0.00000: |2Sat:Night.10|, 2Sun:Night.10}
      combinatorial grouping: 3 x {cost 0.00000: |2Sat:Late.10|, 2Sun:Late.10}
      combinatorial grouping: 2 x {cost 0.00000: |3Sat:Night.10|, 3Sun:Night.10}
      combinatorial grouping: 3 x {cost 0.00000: |4Sat:Night.12|, 4Sun:Night.12}
      combinatorial grouping: 4 x {cost 0.00000: |4Sat:Late.8|, 4Sun:Late.10}
      ---- trainee tasks below here ----
      combinatorial grouping: 1 x {cost 0.00000: |1Sat:Night.19|, 1Sun:Night.19}
      combinatorial grouping: 1 x {cost 0.00000: |1Sat:Late.19|, 1Sun:Late.21}
      combinatorial grouping: 1 x {cost 0.00000: |1Sat:Early.17|, 1Sun:Early.19}
      combinatorial grouping: 1 x {cost 0.00000: |2Sat:Night.19|, 2Sun:Night.19}
      combinatorial grouping: 1 x {cost 0.00000: |2Sat:Late.19|, 2Sun:Late.21}
      combinatorial grouping: 1 x {cost 0.00000: |2Sat:Early.17|, 2Sun:Early.19}
      combinatorial grouping: 2 x {cost 0.00000: |3Sat:Night.17|, 3Sun:Night.19}
      combinatorial grouping: 1 x {cost 0.00000: |3Sat:Late.19|, 3Sun:Late.17}
      [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01705 ]
    ] KheArchiveParallelSolve returning (5.0 mins elapsed)

  There is no grouping of 4Sat+4Sun at all, not sure why not.  Perhaps
  because both Day and Late on Sat can group with Night on Sun.  We
  need to be more permissive once we know that some kind of grouping
  is needed.  Where would that fit in best?  What about balanced
  weekends?  Are they based on complete weekends constraints?
  Yes they are, and furthermore they use a bipartite matching
  of Saturday tasks with Sunday tasks.  If we beef up this matching
  so that the edges indicate which pairs go together best, we can
  group the tasks on the selected edges and fix the unselected tasks.

29 January 2025.  Revising the documentation for complete weekends
  to include a matching that groups required tasks.  It's a big
  change and will need a substantial re-implementation.  I've
  finished the documentation and audited it; it's now ready to
  be implemented.  I also have to think about how it interacts
  with combinatorial grouping and interval grouping.

30 January 2025.  Implementing yesterday's design.  I've done
  everything except KheEdgeIsWanted.

1 February 2025.  Had yesterday off.  Back at work today.
  Implementing yesterday's design.  I've done everything
  except KheEdgeIsWanted.

2 February 2025.  Revised the documentation of KheTaskGrouperCost.
  It is a better function now, and better documented.  The next
  step is to implement what I've documented.

3 February 2025.  Finished auditing the documentation of the task
  grouper.  The next step is to audit and revise the implementation.

  Added KheResourceTimetableMonitorAddProperRootTasksInTimeRange
  and KheResourceTimetableMonitorAddProperRootTasksInInterval.
  But I note that there is already a function in the mtask finder:
  KheAddResourceProperRootTasksInInterval, which seems to be
  unused.  It has a slightly different `wholly within' aspect.

  Finished implementing KheTaskGrouperEntryCost, and finished
  revising and auditing khe_sr_task_grouper.c.

4 February 2025.  Tidying up today.  In khe_sr_interval_grouping.c,
  replaced most of KheDpgExtenderGroupCost by KheTaskGrouperCost.

5 February 2025.  Did some more documenting of mtask grouping, but
  I think it's time for some implementing now.  I've now got a
  clean compile of khe_sr_mtask_grouper.c, but the more interesting
  functions are still to do.

6 February 2025.  Got nothing done today, too many distractions.

7 February 2025.  Working on khe_sr_mtask_grouper.c.  All done
  including the two hard functions, KheMTaskGrouperEntryCost and
  KheMTaskGrouperEntryMakeGroups.  Needs an audit but looks good.

8 February 2025.  Got rid of khe_sr_tgrc_group.c.  All done
  and documented, with a clean compile.

  Previously I wrote "See if the mtask grouper can pass the cost
  calculation over to the task grouper."  But I've just written
  parallel code for this in khe_sr_mtask_grouper.c now.  That
  will have to do.

  Add day intervals to the task grouper? (like those in the mtask
  grouper).  The final interval is needed for finding costs anyway,
  so why not do it.  It will add to the memory cost of the task
  grouper, that's why not.  But we can replace the duration and
  extension fields by the interval, which will come out equal!
  Then there is nothing left in khe_dpg_task_group_rec except
  the optional field, which we could move into KHE_TASK_GROUPER_ENTRY
  as a service to users.  We can get it from the task.  The
  main problem with all this is that the task grouper currently
  knows nothing about the day frame.  Do we want it to know?
  So I'm not doing this now.

  Changed the name of the task multi-grouper to "simple grouper".
  All done and documented.

9 February 2025.  Concatenated khe_sr_tgrc_elim.c, khe_sr_tgrc_comb.c,
  and khe_sr_tgrc_main.c into a single file khe_sr_comb_grouping.c.
  Did some minor tidying up and now I have a clean compile.

  I've returned (at last) to khe_sr_balance_weekends.c.  I've been
  auditing it and the two functions that still need a careful audit
  are KheEdgeIsWanted and the c1, c2, c3 calculation in
  KheConstraintClassDoSolve.

10 February 2025.  Rewrote KheEdgeIsWanted based on new documentation.
  It's great now, it builds a group (returning false if it can't) and
  uses the group cost as part of the edge cost.  Just what we want.

  Moved complete weekends to the task grouping section, in the code
  and the documentation, since it is basically a task grouping solver.
  Changed its name to KheWeekendGrouping.

11 February 2025.  Worked on khe_sr_balance_weekends.c.  I've
  audited what I wrote yesterday about unbalanced demand, and
  implemented it, with a clean compile.

  Started work on updating interval grouping so that it can handle
  existing groups that are partially relevant and partially not.
  I've documented the aim clearly but no implementation yet.

13 February 2025.  Working on interval grouping.  I've revised the
  documentation to take account of heterogeneous tasks.  And I've
  revised the implementation to handle constraints in the same way
  that weekend grouping does, with constraint classes rather than
  subsequences of an array of monitors.  Clean compile but it all
  needs a careful audit.

14 February 2025.  Working on interval grouping.  Added an interval
  to each task grouper entry, and am now requiring disjoint
  intervals in grouped tasks as well as non-interference.  I've
  used this to justify the removal of the initial task grouping
  from interval grouping.

16 February 2025.  Working on interval grouping.  Got nothing done
  yesterday.  Today I audited the documentation and adjusted it to
  allow for the fact that we have simplified interval grouping by
  adding the no-interference rule to the task grouper.

17 February 2025.  Working on interval grouping.  Thinking of
  moving the timetable code to a separate file.  The
  khe_sr_interval_grouping.c file is 4368 lines, the timetable
  code is 621 lines, about 14%, although not all of it will
  move across.  But still, worth doing, I deem.

  Working on a separate "grouped tasks display" module, in file
  khe_sr_grouped_tasks_display.c.  All done with a clean compile,
  documented, audited and ready to use.  I've also removed all
  the timetable printing code in khe_sr_interval_grouping.c,
  replacing it by calls to the grouped tasks display module.
  Again, audited and ready to use.

18 February 2025.  Still auditing interval grouping.  I've decided
  to make a constraint classes module.  I've documented it and
  implemented it (in file khe_sr_constraint_classes.c), with a
  clean compile.  It needs an audit, and then all the modules
  that use constraint classes need to be updated to use it.

19 February 2025.  Audited khe_sr_constraint_classes.c and its
  documentation, it's all in good shape.  So now I'm working on
  replacing all occurrences of constraint classes by this module.

20 February 2025.  Working on KheConstraintClassResourceAllowZero
  and similar functions in khe_sr_constraint_classes.c.  All done,
  audited, and ready to use.

21 February 2025.  Working on khe_sr_assign_by_history.c.  I've
  revised it and tidied it up.  It compiles cleanly and looks
  beautiful.  Now it needs a careful audit.  Only three days for
  revised constraint classes (started 18 Feb).  Well worth it.

22 February 2025.  Finished auditing khe_sr_assign_by_history.c,
  khe_sr_weekend_grouping.c, and khe_sr_constraint_classes.c.
  So khe_sr_interval_grouping.c is all that's left.  I've deleted
  all its old stuff, I now have 2315 lines.  This compares with
  4368 lines on 17 February 2025, so I've reduced the file size
  by almost 50%, which is a great result.

  Removed the constraint class DevToCost functions.  They were not
  used and they don't really make sense.

23 February 2025.  Audited assign by history code and doc.  All
  good except two points that I need to sort out.

24 February 2025.  Audited weekend grouping.  Returned to assign
  by history to sort out the two points.  I've worked out that I
  was already doing the right thing about the tradeoff between
  admissible constraints and busy days constraints; I've documented
  my analysis there.

  In assign by history, I've decided what to do when admissible
  resources have pre-existing assignments:  omit them and the
  tasks they are assigned to from the matching, but incorporate
  the effect of them just as though the matching had assigned them.
  It's documented and ready to implement.

  Finished a substantial revision of the assign by history doc
  to take account of what should happen when asmissible resources
  are already assigned.  It needs an audit and then implementing.

25 February 2025.  Audited yesterday's rewrite of assign by history.
  It's ready to implement.  I've audited all of the implementation
  except KheHistoryInstanceStageSolve, which is where the new
  stuff will have to go.

26 February 2025.  Auditing the rewrite of assign by history.
  It is all done now, but it all needs another careful audit.

27 February 2025.  I've audited yesterdays busy days limit
  documentation and implemented it.  So assign by history
  is all done now.

  Just added KheTaskAddTimesToTimeSet, but it is buried in the
  mtask finder.  Arguably, it is part of a larger solver module
  which returns interesting facts about tasks.  Need to think
  about this and maybe come up with something.  Turns out there
  was a time set function that did the same thing, so I'm
  leaving this as is for now.

  Realized that I have omitted to use constraint classes in
  khe_sr_comb_grouping.c.  It looks like a fairly small job,
  so I'm starting on it now (with save_khe_sr_comb_grouping.c
  in place).  Actually I've done the changes and got to a
  clean compile; it just needs an audit and test now.

28 February 2025.  Audited the changes to khe_sr_comb_grouping.c.
  All good.  Updated the combination reduction documentation (only
  slightly) to record the fact that constraint classes are used.

  Returning (again!) to interval grouping.  I've audited the
  documentation, and made some substantial changes to which
  constraint classes are selected.

  Implemented KheConstraintClassHasUniformLimits in preparation
  for implementing the revised constraint class spec for interval
  grouping spec.  I've also implemented that new spec.

1 March 2025.  Audited interval grouping.  It seems good enough
  to start adding hetero to now.

  I've separated out the cost cache.  It is now called an interval cost
  table, and it has been moved to khe_sm_interval.c.  All done and
  documented, including replacing it in khe_sr_interval_grouping.c.

  I've written KheIgSolverAcceptsMTask, which scans one mtask and
  classifies it.  But I need to work out exactly what I am trying
  to return here.  Perhaps a fresh type holding the mtask, its
  interval, and its hetero class?

  Added type KHE_IG_MTASK which will carry the mtask and associated
  information.  I've updated the time groups to hold these objects
  rather than pure mtasks.  And I've added all the boilerplate
  code to convert from using KHE_MTASK to using KHE_IG_MTASK.

2 March 2025.  Working on interval grouping.  I seem to have
  finished the code that builds KHE_IG_MTASK objects and
  distributes them to KHE_IG_TIME_GROUP objects, although it
  needs an audit.  Next up is using the new info in solving.

3 March 2025.  Working on interval grouping.  Audited the
  KHE_IG_MTASK setup code and started work on using locations.
  I am successfully forbidding KHE_LOCATION_FIRST_ONLY tasks
  from not being first, and I've written code forbidding
  KHE_LOCATION_LAST_ONLY tasks from not being last.  It
  all needs an audit but it should be right.

  Added KheTaskGrouperInterval and KheTaskGrouperEntryInterval.
  All done and documented.

  Removed the dummy_entry flag from the task grouper.  I've checked
  whether interval grouping has some code that does not take account
  of the possibility of dummy entries, and I've fixed all that up.

  Could we move optional into the task grouper?  It would be a
  true reflection of the cost of the group.  But that is arguably
  beyond what task grouping is supposed to be about, so I've
  decided not to do this.

  Added an interval to each task grouper entry.  Can I remove
  any fields now from type KHE_IG_TASK_GROUP?  Hetero and history
  make it hard.  So I've decided against it.

  I've thought about getting rid of the inheritance structure
  in KHE_IG_TASK_GROUP, but it's too hard.  So I'm living with it.

4 March 2025.  Finished auditing the new interval grouping code.
  It's ready to test.

8 March 2025.  I got sick of coding so for the last three days I
  have been writing a potential PATAT paper caled `Pre-Solving in
  Nurse Rostering', which covers combinatorial grouping, weekend
  grouping, assign by history, and interval grouping.  I've done
  it all now except the Results section, a few diagrams that it
  needs for livening it up, and perhaps a few more references.

9 March 2025.  Finished the pre-solving paper, including diagrams,
  all except the results section, which I will do later.

12 March 2025.  I've been working on refereeing a paper, all done
  now but they day is over, so back to real work tomorrow.

13 March 2025.  Testing the new pre-solve code today.  There was a
  problem with days_frame needing to be non-NULL now, but after fixing
  that things seem to be working well.  First results:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.02750 ]

  No wonder, the output of interval grouping is all wrong.

14 March 2025.  Fixed the problem with weekend grouping, just a bug.
  Also fixed a problem that was causing many task domains to have
  NULL Id's (they were just lists of resources in the prefer
  resources constraints, hence nameless).

  I've checked through the debug output for combinatorial grouping,
  it seems to be working well, grouping Saturday and Sunday night
  tasks, and also Saturday and Sunday late tasks.  Also checked
  weekend grouping, it is working well now, grouping Saturday and
  Sundays tasks of the same type (Early, Day, etc.).

  I've worked out why some of the tasks are being put into
  separate mtasks.  It's because their assignments are fixed:

    KheIgSolverAddMTasks considering mtask:
    [ MTask Fixed 1Mon Caretaker
      n1.00000 a0.00000 1Mon:Early.10
    ]
    KheIgSolverAddMTasks considering mtask:
    [ MTask 1Mon Caretaker
      n1.00000 a0.00000 1Mon:Early.11
      n1.00000 a0.00000 1Mon:Early.12
      n1.00000 a0.00000 1Mon:Early.13
      n1.00000 a0.00000 1Mon:Early.14
      n0.00030 a0.00000 1Mon:Early.15
      n0.00030 a0.00000 1Mon:Early.16
      n0.00000 a0.00001 1Mon:Early.17
      n0.00000 a0.00001 1Mon:Early.18
      n0.00000 a0.00001 1Mon:Early.19
      n0.00000 a0.00001 1Mon:Early.20
      n0.00000 a0.00001 1Mon:Early.21
      n0.00000 a0.00001 1Mon:Early.22
      n0.00000 a0.00001 1Mon:Early.23
      n0.00000 a0.00001 1Mon:Early.24
      n0.00000 a0.00001 1Mon:Early.25
      n0.00000 a0.00001 1Mon:Early.26
    ]

  So that's all right then.  Fixed a few other bugs and now I have
  actually seen a grouping timetable with a Late + Night task in
  it (for 3Sat-3Sun).  Hurrah!  Also assign by history seems to
  be correct in that grouping timeable.  First result:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01635 ]

  Not a bad result actually.  I should do some testing and looking
  carefully at what I'm getting before getting carried away.

15 March 2025.  Worked out that weekend grouping was not taking
  account of the domains of the tasks.  I've fixed that now, all
  documented, implemented, and tested, and working well.

  I spotted a couple of places in the interval grouping timetable
  where swapping two tasks would produce larger domains.  So I've
  added a count of the number of complete groups with non-homogeneous
  domains to the solution, and used it to guide dominance testing
  when costs and signatures are equal.  It seems to have worked.
  All documented and tested.

  After all these improvements, of course things have got worse:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01755 ]

16 March 2025.  Worked out that resource matching is making the
  extra groups that I was puzzling over before.

17 March 2025.  The problem with Constraint:17 is that it has
  one resource group, RG:All, and that can only have one
  resource type.  So the numbers are wrong.  I need to go
  one resource at a time through the group.  Fixed this and
  another bug and I'm now running through to the end:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01800 ]

  Hopeless!  What's wrong now?

18 March 2025.  In khe_sr_interval_grouping.c, I've implemented code
  that takes account of the busy days limit (see KheTaskExtraDuration).
  But whether it's right or not I still have to decide.  There
  is some debug code there to show me what is being done.

19 March 2025.  I think the code is working correctly now, but
  the results are not great:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01795 ]

  I've checked the yourself output and the time seems to be being
  divided up fairly, except that arguably too much is being given
  to dynamic.  So I'll alter that and rerun:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01740 ]

  That's a bit better, what about even more to rec:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01790 ]

  Apparently not.  Back to 2 and try best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01600 0.01600 0.01790 0.01790 0.01795 0.01820 0.01830 0.01855
      0.01860 0.01870 0.01885 0.01885
    ]

  The best is not bad (rel = 1.28).  But there is a big gap from 2 to 3.
  On 3 November 2024 the best 5 minute run was producing

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.1 mins:
      0.01560 0.01700 0.01710 0.01740 0.01745 0.01755 0.01755 0.01810
      0.01830 0.01845 0.01855 0.01895
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1190
    Limit Active Intervals Constraint (3 points) 	 75     200
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1600

  which seems to be the benchmark.  So I guess I can grind down
  from here.  There are 6 Constraint:17 defects, 3 Nurse (45 points)
  and 3 Trainee (60 points).  I need to compare them with the LOR
  Constraint:17 defects and try to work out what is going on.  But
  a total of 105 points is not too bad compared to the LOR 75.  The
  problem really is the other 95 points in the total of 200.

  Made a change to yourself, removing the third repair stage, so I
  am back to the taditional two stages.  Got this horrible result:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01720 0.01720 0.01820 0.01830 0.01835 0.01870 0.01870 0.01905
      0.01905 0.01910 0.01920 0.01925
    ]

  So let's keep away from that, heaven knows why.  Here we are back
  at what we had before:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01610 0.01735 0.01750 0.01765 0.01775 0.01790 0.01810 0.01865
      0.01875 0.01875 0.01885 0.01925
    ]

  The best result is marginally worse; the second best is much worse.
  Now for a 30-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01505 0.01545 0.01640 0.01645 0.01660 0.01670 0.01680 0.01690
      0.01700 0.01700 0.01740 0.01755
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      60
    Cluster Busy Times Constraint (28 points) 	   	950    1130
    Limit Active Intervals Constraint (3 points) 	 75     195
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1505

  This is rel = 1.20.  KHE's best 30-minute result is 1485, obtained
  on 30 Nov 2024.  This is very close to that, which is encouraging.
  But the Constraint:17 defects are unchanged:  Nurse 45 and Trainee 60.
  I guess the first step is to work out why we are not at the LOR
  level (75) for Constraint:17 defects, given the priority we are
  giving them.

20 March 2025.  Actually we are at the LOR level at the start of
  the cycle for Constraint:17 defects.  (In fact the KHE solution
  has cost 60 at the start of the cycle, whereas the LOR solution
  has cost 75).  KHE's extra problems are further along the cycle.

  I tried a simple way of adding randomness (adding the mtasks in
  a different order) but it did not seem to do anything.  So then
  I switched to calculating a "randomizer" integer for each solution,
  and that did seem to work.  First results:
  
    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01655 0.01680 0.01705 0.01715 0.01740 0.01740 0.01765 0.01800
      0.01825 0.01865 0.01875 0.01875
    ]

  The best result is worse than before, but the second best is a lot
  better, as indeed are the others.  Here's a 30-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01605 0.01620 0.01640 0.01640 0.01650 0.01655 0.01665 0.01680
      0.01720 0.01735 0.01755 0.01755
    ]

  The best results here are not nearly as good as in yesterday's
  30-minute run.  Hmmm.  Here's a 60-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 60.0 mins:
      0.01595 0.01600 0.01605 0.01620 0.01625 0.01640 0.01650 0.01715
      0.01720 0.01735 0.01755 0.01765
    ]

  For a 60-minute run, it's pretty hopeless.  Back to a 5-minute run
  with more conventional settings throughout:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01705 0.01765 0.01770 0.01815 0.01850 0.01855 0.01855 0.01855
      0.01885 0.01900 0.01910 0.02020
    ]

  Now a 5-minute run with rs_drs_seq_frac=0.2:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01800 0.01820 0.01825 0.01840 0.01845 0.01850 0.01880 0.01885
      0.01885 0.01925 0.01970 0.01970
    ]

  Now a 5-minute run with rs_drs_seq_frac=0.8:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01690 0.01710 0.01760 0.01780 0.01810 0.01840 0.01845 0.01850
      0.01855 0.01880 0.01920 0.02055
    ]

  Actually there is very little in this.

  Turning now to viewing the solution after phase 1:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01940 0.01945 0.01980 0.02075 0.02080 1.01760 1.01770 1.01810
      1.01930 1.02035 1.02040 2.01925
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     270
    Avoid Unavailable Times Constraint (7 points)    	 70      70
    Cluster Busy Times Constraint (28 points) 	   	950    1360
    Limit Active Intervals Constraint (3 points) 	 75     240
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1940

  Surprisingly good.  But the main point is to look into the details.
  And in fact we've done better than LOR at the start of the cycle,
  just 45 points of history-related Constraint:17 stuff.

  Saved khe_sr_dynamic_resource.c in save_khe_sr_dynamic_resource.c.
  Added code so that solutions with more task duration dominate
  solutions with less, when everything else is equal.  But decided
  that cover constraints are doint

20 March 2025.  Decided that yesterday's idea of adding code so
  that solutions with more task duration dominate solutions with
  less, when everything else is equal, was a bad idea, because
  cover constraints do it anyway.  So I took the code away again.

  KheDynamicResourceBalanceWorkloads skeleton all done and documented
  (file khe_sr_dynamic_workload.c).  Need to flesh it out now.  At
  present it is separating resources into the three classes but
  nothing more.

22 March 2025.  Tested KheDynamicResourceBalanceWorkloads.  It seems
  to be working but it is finding no improvements at all.  This is
  quite significant, it shows that utilizing these underloaded
  resources more fully is very hard indeed.  Anyway I've deleted
  it from khe_sr_combined.c,

  Now back to the basic five-minute solve with two repair phases:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01705 0.01720 0.01740 0.01745 0.01795 0.01840 0.01860 0.01865
      0.01890 0.01910 0.01955 0.01990
    ]

  Moving KheTaskingEnlargeDomains to the second repair:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01665 0.01725 0.01775 0.01780 0.01805 0.01820 0.01830 0.01830
      0.01840 0.01915 0.01935 0.01955
    ]

  It's a bit better.  Removing all but the last call to rdv,
  which should give rec a lot more time to run, produces this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01640 0.01675 0.01690 0.01700 0.01705 0.01715 0.01725 0.01735
      0.01770 0.01835 0.01855 0.01865
    ]

  Better again.  Ejection chains carries most of the weight, as usual.

  Off-site backup today.  The backup version is giving this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01640 0.01655 0.01680 0.01690 0.01695 0.01715 0.01735 0.01770
      0.01780 0.01835 0.01835 0.01865
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     210
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1200
    Limit Active Intervals Constraint (3 points) 	 75     150
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1640

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       44
    Y  Unnecessary assignments         1       10
    X  Unassigned tasks                5        7
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       13
    Y  Unnecessary assignments        18       21
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  This 1640 is rel = 1.31.  I seem to have done a lot of work and
  not got very far.  Here's a 30-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01565 0.01600 0.01635 0.01635 0.01670 0.01685 0.01710 0.01725
      0.01730 0.01760 0.01780 0.01795
    ]

  This is rel = 1.25.  Time for some regular grinding down.

  Also, there is a group in the interval grouping printout with has
  length 6.  This should not happen!  How did it happen?  No there
  isn't, it starts with a Late shift.

23 March 2025.  Decided to add "history entries" alongside "dummy
  entries" in the task grouper.  Done and audited the doc, and
  implemented it and have a clean compile.  So I should be ready
  now to start using history entries.

24 March 2025.  Working on using history entries in interval
  grouping.  I've made sure that (a) an assigned task can
  always extend an assigned group when the assignments are
  equal, and (b) that an assigned task cannot extend any
  other group when there is a case of (a).

  Updated the group display so that it can display history,
  and updated the interval grouper to use this feature when
  displaying its groups.

  I had to re-organize the symmetry breaking in mtasks - we were not
  taking account of the fact that *assigned* tasks in mtasks are not
  symmetrical.  But I'm now getting solutions in which the two
  nurses with history 4 are being grouped with another task, and
  not being grouped with another task.  So that seems good.

25 March 2025.  Realized overnight that history tasks are not
  optional.  So I'm trying making them required now.

  Found a bug, it seems to be in KheIgExtenderGroupCost.  It is
  manifesting as a memory bug but I think it is really a logic
  bug in that KheIgExtenderGroupCost is not expecting a task
  group containing (or consisting entirely of) a history entry.
  So I've done quite a lot of work to KheIgExtenderGroupCost,
  and also to the interval cost table, so that now history
  groups should be handled properly.  I've now got a clean
  run with what looks like a better interval grouping for
  nurses' night shifts.

26 March 2025.  Written code to print intervals for other solutions
  that might be lying around.  The idea is to make it easier to
  compare the LOR17 solution with our solution.  It seems to be
  working now, so the next step is to make the comparison.

  Redone KHE_FRAME to take away its access to a solution.  A frame
  is a sequence of time groups and so is derived from an instance,
  not from a solution.  This is clearer for all concerned and will
  help with interval grouping debugging.  All done and documented.

  I found that the task Id's in the LOR solution did not match
  the task Id's in KHE's solution.  This is because the meet
  index field of tasks varies depending on how the solution
  was constructed.  I've now changed the definition of KheTaskId
  so that it uses the index of the task's event resource in the
  enclosing event.  This does not change like the meet index does.

27 March 2025.  I finally reached a point where I could compare
  the LOR solution with the KHE soln wrt interval grouping of
  night shifts.  LOR is doing better, and I don't know why.

4 April 2025.  Got a bit sick of fiddling with interval grouping
  so I took a few days off to referee a couple of papers.  All
  done now and back to interval grouping today.

5 April 2025.  Found the bug which was causing the debug print of
  the interval table to come out wrong.  Sadly it was just the
  debug that was wrong, so that's not the cause of the problem.

    cost of 3Sat-4Tue = 0.00000 + 0.00000 + 0.00120 = 0.00120
    cost of 3Sat-4Wed = 0.00000 + 0.00000 + 0.00150 = 0.00150

  So we should be preferring the 3Sat-4Tue group, but in fact
  we have the 3Sat-4Wed group, ending in an optional task.  Why?

  The problem is not the clever restriction, because that
  applies only when g1 is undersized, and here g1 has length 4.

  I've found the problem:  I had MAX_KEEP set to 100, which was
  too low.  I've now increased it to 2000 and the problems have
  gone away.  But 2000 runs more slowly than I would like.

    [ "INRC2-4-100-0-1108", 1 solution, in 115.9 secs: cost 1.01880 ]

  This solution has just one violation of Constraint:17 beyond
  what LOR has, and it is a long way away from the start of the
  cycle.  So I rate that a good result, and now I need to do
  a full 5-minute run and see what that produces:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01670 ]

  But 12 threads were taking far too long, I've got the debug
  output to prove it.  So I'm reducing MAX_KEEP now from 2000
  to 1000, still too slow.  Reduce to 500.  I'm getting stuff
  like this:

    IgTimeGroupSolns(4Fri4, 1136216 made, 3539 undominated, 500 kept)

  No wonder it is taking so long.  It needs a rethink; it's too slow
  for a pre-solver.

6 April 2025.  Some ideas for tightening up interval grouping:

  * Make sure that finished groups are treated as finished as soon
    as possible.  They should be out of bounds for adding to, and
    their costs should be added in immediately.  I'm not sure what
    good this will do, but it is surely the right thing to do.
    Perhaps change the last_only attribute to a finished attribute.

    But then we have different numbers of unfinished groups, so
    what becomes of the dominance test?  Do we have to put in
    fake groups of length 0?  But then they will not dominate?
    Or do we just declare that there is no dominance?  It should
    not matter, lengths would have been different anyway.  And
    this way we convert them into costs.

    OK, we could do this independently of any other changes, and
    it would be clearer to do so, even if it doesn't really change
    the running time.

    But now, a task group could be finished in some solutions
    and unfinished in others.  So we can't use a flag in it
    for case (1).  We can use a flag for (2), (3), and (4).

  * Can we extend the l(g) = l(g') condition to also include
    C_min <= l(g) <= l(g')?  Arguably we can still end g at
    the same place as g' and there will be no cost.  But
    there might be a cost if g starts on a Sunday and g' starts
    on a Saturday.

    The point is to make sure that whatever we extend g' with,
    we can also extend g with, and when the group is finished
    we will still have c(g) <= c(g').

    Perhaps we need to find the cost of each interval of length
    up to C_max before we start - not hard to do - and put it
    in the cache.  Perhaps store just the task grouper entry
    cost.  We can add the other two very easily later, they
    just depend on the interval length.

    Then we require c(g) <= c(g') for each extension up to
    C_max in order for dominance to hold.  For each pair
    of starting positions we can calculate a boolean
    before we begin.

7 April 2025.  Changed "must_finish_here" to "last_task_in_group".
  Audited everything and got it into good working order.  Along
  the way, I reorganized KheIgSolnDominates and fixed a bug in
  it.  This might just fix my slow run times - I'll have to see.

8 April 2025.  Fixed a few bugs in the new code, it seems to be
  running OK now.  There was a nasty out by one bug that was
  causing the interval costs to be wrong, all fixed now.  There
  are no optional tasks in the final solution, which is great.
  The fix also seems to have fixed the running time problems:

    KheConstraintClassSolve returning (groups_count = 61, 0.6 secs)

  By the way the result was

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01695 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01585 0.01605 0.01630 0.01670 0.01675 0.01695 0.01720 0.01745
      0.01750 0.01775 0.01790 0.01835
    ]

  which is not too bad (rel = 1.27).  On 3 November 2024 the best 5
  minute run was producing

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.1 mins:
      0.01560 0.01700 0.01710 0.01740 0.01745 0.01755 0.01755 0.01810
      0.01830 0.01845 0.01855 0.01895
    ]

  and we are arguably back to this or better (look at second best).
  Here's a 30-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 30.0 mins:
      0.01540 0.01575 0.01585 0.01600 0.01635 0.01640 0.01690 0.01695
      0.01695 0.01710 0.01790 0.01805
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150      90
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1250
    Limit Active Intervals Constraint (3 points) 	 75     120
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1540

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       46
    Y  Unnecessary assignments         1        8
    X  Unassigned tasks                5        3
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       12
    Y  Unnecessary assignments        18       22
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  So we're in a reasonably good position (rel = 1.23).  But
  as usual we have too many unnecessary assignments, in fact
  (8 - 1) + (22 - 18) = 11 * 20 = 220, which explains most
  of the the 300-point difference in cluster cost.

9 April 2025.  Found out that when no optional tasks need to be
  added, I am already not rerunning.

  Found that yesterday's good run had optionals turned off.  If
  I turn them on again I'm back to 21.3 seconds and 7 optionals
  in the final solution.  Also trainees seem to be taking an
  inordinately long time, given MAX_KEEP = 4000.  Let's start
  by showing the daily totals.

  Trying to work out why 2Thu goes wrong.  I have printouts
  of the two 2Wed solns that are compatible with LOR, now I
  need to eyeball them to see what the problem is.

10 April 2025.  Worked out that in the KHE solution, everyone
  on 2Thu includes at least one new group, whereas in the LOR
  solution, every group gains exactly one task so everyone's
  length increases by exactly 1, given smallest group size 2.
  Why was that not an option for KHE?

  Because it would have involved intersecting the Nurse and
  Caretaker domains.  And yes, there are nurses that are

     Nurse but not Caretaker    e.g.  HN_1
     Caretaker but not Nurse    e.g.  CT_35
     Both Nurse and Caretaker   e.g.  NU_16

  So there we have it - there is the problem.  The solution
  is to allow intersections (as long as they are non-empty)
  and to cache them.  The task grouper has to do this.
  Perhaps specify a maximum number of resource groups that
  can be intersected.

  Also take account of assigned resources.  In fact, replace
  field assigned_resource by field domain_details, which can
  include an assigned resource and generally look after the
  domains, and have static functions

     bool KheDomainDetailsAddResourceGroup(DD dd, KHE_RESOURCE_GROUP rg,
       DD *new_dd)
     bool KheDomainDetailsAddAssignedResource(DD dd, KHE_RESOURCE r,
       DD *new_dd)

  which do the caching of intersected domains and return true
  when successful.  This can include checking that one resource
  group is a subset of another, but if that fails it can construct
  and cache a new domain.

  Is there a resource group symbol table anywhere?  Doubtful,
  but there might be an sset table.  Yes, there is a khe_set
  table in khe_set.h which maps a khe set (which is what a
  resource set is) to an arbitrary void *.  Miracle of
  miracles, this is already implemented in the KheSoln
  functions that build resource sets.  If it's the same
  resource set again, it grabs it from the table.

  So we can check for subsets, and if that doesn't work we
  can build the intersection and there will not be a big cost.

11 April 2025.  I looked into optional tasks being used unnecessarily
  at the end of task groups that are not undersized.  I though this
  needed fixing before I fix the domain problem, but there are
  no cases of it in the current optional tasks solution, neither
  for Nurse nor for Trainee.

  Wrote a new "Finding resource group intersections" subsection
  of the task grouping section.  This needs and audit but it is
  probably ready to implement.  So then I need to do that and
  then update the task grouper to use it.

12 April 2025.  Audited the new resource group intersections section

13 April 2025.  I've done a quick pass through the grouping documentation.
  The task grouper and interval grouping sections are now up to date, but
  the mtask grouping section is out of date and the other sections still
  need to be looked at carefully.  Also audited the resource intersector
  code but found nothing to change.

  Added resource intersector to task grouper and interval groupiing.
  The changes to interval grouping are trivial, but the task grouper
  needs a careful audit.

  Changed the names of things as follows:

    khe_sr_resource_intersector.c	khe_sr_task_group_domain.c
    KHE_RESOURCE_INTERSECTOR		KHE_TASK_GROUP_DOMAIN_FINDER
    KHE_RESOURCE_INTERSECT		KHE_TASK_GROUP_DOMAIN
    KheResourceIntersector		KheTaskGroupDomainFinder
    KheResourceIntersect		KheTaskGroupDomain
    ri					tgdf and tgd
    KHE_RI				KHE_TASK_GROUP_DOMAIN

  All done with a clean compile.  Documentation is changed too.

14 April 2025.  Making soln adjuster interface uniform, which involves

    What we have now                     What we want
    ------------------------------------------------------------------------
    KheSolnAdjusterMonitorEnsureAttached KheSolnAdjusterMonitorAttachToSoln
    KheSolnAdjusterMonitorEnsureDetached KheSolnAdjusterMonitorDetachFromSoln
    KheSolnAdjusterMonitorChangeWeight	 KheSolnAdjusterMonitorSetCombinedWeight
    KheSolnAdjusterTaskEnsureFixed	 KheSolnAdjusterTaskAssignFix
    KheSolnAdjusterTaskEnsureUnFixed	 KheSolnAdjusterTaskAssignUnFix
    ------------------------------------------------------------------------

  I've made these changes and have a clean compile.  Also documented them.

  I've audited the revised task grouper code.  I'm now assigning the
  task group domain to the leader task when building the group.  So
  it's all done and ready to test.

15 April 2025.  I've worked through the whole task grouping section
  this morning, working out what changes still needed to be made.
  All done and documented and clean compile.

  Did some testing, got rid of a few fiddly bugs.  The interval
  grouping result now has fewer undersized groups, but it still
  has two, both starting on 3Wed.  So I need to compare with LOR
  again and see what's wrong this time.  NB there are now some
  groups that group Nurse with Caretaker.  I did not have that
  before.  So there's progress.

16 April 2025.  Comparing what I'm getting with LOR.  At first
  sight, my solutions are compatible in sizes but they have
  different domains.  On 3Thu, LOR has two groups starting,
  their first tasks are Night.0 and Night.5, so that's HeadNurse
  and Nurse.  But in the KHE compatible solutions, Night.0 and
  Night.5 do not start any groups; the two groups starting on
  3Thu are both Caretaker groups.

17 April 2025.  Here is what weeking grouping is doing with night
  shifts on 3Sat-3Sun:

    KHE grouping
    -------------------------------------------------------
    3Sat:Late.0   (HeadNurse) and 3Sun:Night.0  (HeadNurse)
    3Sat:Night.3  (Nurse)     and 3Sun:Night.5  (Nurse)
    3Sat:Night.4  (Nurse)     and 3Sun:Night.12 (Caretaker)
    3Sat:Night.10 (Caretaker) and 3Sun:Night.11 (Caretaker)
    3Sat:Night.11 (Caretaker) and 3Sun:Night.10 (Caretaker)
    -------------------------------------------------------

    LOR assignment
    -------------------------------------------------------
    3Sat:Late.0   (HN_9)     and 3Sun:Night.0  (HN_8)
    3Sat:Night.3  (HN_1)     and 3Sun:Night.5  (HN_1)
    3Sat:Night.4  (HN_8)     and 3Sun:Night.12 (CT_58)
    3Sat:Night.10 (NU_17)    and 3Sun:Night.11 (NU_17)
    3Sat:Night.11 (CT_66)    and 3Sun:Night.10 (CT_66)
    -------------------------------------------------------

  So KHE grouped 3Sat:Late.0 with 3Sun:Night.0, but
  LOR did not group Late with Night at all.  LOR gave
  higher priority to grouping the same shifts over
  grouping the same domain.  But so are we!

  Actually LOR does group a Late with a Night, but it
  is assigned CT_58, whereas KHE requires it for a
  HeadNurse.  So when we go for different offsets
  can we then prefer larger domains?

    KHE grouping
    --------------------------------------------------------
  ! 3Sat:Late.0   (HeadNurse)  and 3Sun:Night.0  (HeadNurse)
    3Sat:Late.5   (Nurse)      and 3Sun:Late.3   (Nurse)
    3Sat:Late.11  (Caretaker)  and 3Sun:Late.9   (Caretaker)
  * 3Sat:Night.4  (Nurse)      and 3Sun:Night.12 (Caretaker)
    --------------equivalent below here---------------------
    3Sat:Late.10  (Caretaker)  and 3Sun:Late.8   (Caretaker)
    3Sat:Late.12  (Caretaker)  and 3Sun:Late.10  (Caretaker)
    3Sat:Night.3  (Nurse)      and 3Sun:Night.5  (Nurse)
    3Sat:Night.10 (Caretaker)  and 3Sun:Night.11 (Caretaker)
    3Sat:Night.11 (Caretaker)  and 3Sun:Night.10 (Caretaker)
    --------------------------------------------------------

    LOR grouping
    --------------------------------------------------------
  * 3Sat:Late.0   (HeadNurse)  and 3Sun:Late.3   (Nurse)     HN_9
  * 3Sat:Late.5   (Nurse)      and 3Sun:Late.9   (Caretaker) NU_28
  ! 3Sat:Late.11  (Caretaker)  and 3Sun:Night.11 (Caretaker) CT_58
  * 3Sat:Night.4  (Nurse)      and 3Sun:Night.0  (HeadNurse) HN_8
    --------------equivalent below here---------------------
    3Sat:Late.10  (Caretaker)  and 3Sun:Late.8   (Caretaker) NU_20
    3Sat:Late.12  (Caretaker)  and 3Sun:Late.10  (Caretaker) CT_70
    3Sat:Night.3  (Nurse)      and 3Sun:Night.5  (Nurse)     HN_1
    3Sat:Night.10 (Caretaker)  and 3Sun:Night.10 (Caretaker) NU_17
    3Sat:Night.11 (Caretaker)  and 3Sun:Night.12 (Caretaker) CT_66
    --------------------------------------------------------

  In the LOR grouping, the sole entry with different shifts has
  Caretaker for both domains.  In the KHE grouping the sole entry
  with different shifts has HeadNurse for both domains.  Can we
  argue that when shifts are different it is very important for
  domains to be large?  Or for domains to be ones that appear
  frequently on the days in question?  More important than for
  domains to be the same?

18 April 2025.  Here's an idea:  run interval grouping before
  weekend grouping, but check weekends first and include a few
  optional tasks e.g. if there are more Night shifts on Sunday
  than Saturday, include a few optional Saturday shifts.  Have
  to decide (heuristically, presumably) how many optional shifts
  to add and which ones (i.e. Day or Late, Caretaker or Nurse etc.).
  Since we add mtasks, not tasks, we may have to limit the
  number of tasks within each mtask.

  These new tasks are not heterogeneous in the current sense,
  they have no night shifts at all in them.  So they will
  need different treatment from heterogeneous tasks, perhaps
  a new task type.

  Then run weekend grouping; it will have to cope with more
  grouped tasks than it did before, but presumably it can
  do that without any problems.  Anything already grouped
  will be left out.

  Rather than start and end etc, what about anything goes
  but we measure sequences of marked tasks, where the mark
  means that you are a Night task or whatever.  You can
  tip any tasks into the solver that you like, but you
  have to build sequences of consecutive days, and you
  have to satisfy both consecutive days constraints and
  consecutive night shifts constraints.   Hmmm.  OK, you
  have to build sequences of consecutive days, and if
  they happen to contain Night shifts or whatever,
  you measure their lengths.  You also do local cost;
  the task grouper can do that for you, although not
  so well if there are non-Night shifts in the mix.
  Need a cache where you can look up something like

     3Sat:Late + 3Sun:Night + 3Mon:Night + 3Tue:Night

  What sort of cache is this?  Not easy to design.
  Could be a trie going backwards, yes that would work
  because the elements are always on consecutive days.
  A sequence of offsets into days frame time groups.
  The cache can contain local costs plus sequence costs.
  It basically does that now; it contains complete costs.
  Have to include offset into time group, and then for
  each offset there is a set of constraint classes that
  times at that offset apply to.  So you need to count
  these sequences for each offset you encounter.

  What if you just let the task grouper work out the cost
  without worrying about where it came from?  You would
  still need some cutoffs.  The max and min limits are
  used now as follows:

     max_limit - upper limit on group length
     min_limit - skipped_undersized_count, adding optional
                 tasks adjacent to undersized groups

  In principle we could load both Late tasks and Night
  tasks and handle sequences of both simultaneously;
  but in practice we would rather just handle Night
  plus sequences of days, which we are already giving
  some attention to in history, but which could do
  with attention throughout the cycle, although max
  days limits tend to vary by resource (min is 2-4,
  max is 5-7) - but not when tied to a specific resource
  by history.

  Alternatively these new ones can be marked "at start or
  end only" and used in that way.  Each task can have

                                             Ord.  Extra
     ----------------------------------------------------
     bool           allowed_at_start         Yes    Yes  
     bool           allowed_inside           Yes     No 
     bool           allowed_at_end           Yes    Yes 
     KHE_INTERVAL   relevant_interval        All   Empty
     ----------------------------------------------------

  Heterogeneous tasks are Yes No No or No No Yes.  If we
  do it this way there is not much new code to write.  So
  this might be the most practical way forward.

  Added KHE_LOCATION_FIRST_OR_LAST and am now using it
  properly, but not defining any tasks that have it yet.

19 April 2025.  Moved the complete weekends code from the
  weekend grouper to the constraint classes module.  From
  there it can be accessed by the interval grouper, which
  will be needing it.

  KheIgSolverAddWeekendMTasks is working now, it finds the
  weekends and it finds the number of tasks starting on
  each day.  At present it does not do anything about that
  except print a debug saying that they differ.  These are
  now *running* mtasks, not merely *starting* mtasks.

  I'm now generating a good set of optional extra mtasks,
  all those that end on Saturday or start on Sunday and
  are one different in offset from the Night shifts.
  Perhaps its simply a matter of making one task
  available from each of these mtasks?  No, as many
  as we are short, or the lot, whichever is smaller.

20 April 2025.  Still refining interval grouping.  I
  have to worry about cost calculations, and especially
  caching, today.

  Implemented a generic trie module today.  I can use it
  to cache costs.  It's implemented and tested but not
  yet documented.

  Field class_in is used only to get its length, which
  becomes task_class_durn, and its location, which becomes
  ANYWHERE, FIRST_ONLY, or LAST_ONLY.  Then task_class_durn
  feeds into duration, which is compared with igsv->max_limit
  and igsv->min_limit to find out if a group is oversized
  or undersized.  So I've replaced class_in with class_durn,
  and made it 0 for weekend tasks.

  I'm now generating a good set of optional extra mtasks,
  all those that end on Saturday or start on Sunday and
  are one different in offset from the Night shifts.  The
  number of tasks available from each of these mtasks is
  the number we are short, or the lot, whichever is smaller.

  Now checking that weekend mtasks don't contain any Night
  shifts; because if they do they will be added twice, which
  would be disastrous.  See KheTimeSetContainsOffset.

21 April 2025.  Working on the interval grouping documentation.
  I've revised it to be more careful about terminology, and
  I've audited the revision.  It's ready to be implemented.

22 April 2025.  Ready to revise the implementation of interval
  grouping.

23 April 2025.  Started work on the implementation, but then
  decided that the documentation was not sufficiently concrete.
  So I'm redoing the documentation now.

24 April 2025.  Did some reorganizing of the code, changing
  "location" to "placement" and moving helper functions into
  better submodules and giving them better names.

25 April 2025.  Audited khe_sr_interval_grouping.c.  All done
  except some specific points that I've listed under ToDo.

27 April 2025.  Had yesterday and most of today off doing
  odd jobs.

28 April 2025.  Sorted out the extension field, including
  renaming it overhang.  It is the number of days in the
  future that the task is running, not the number of times
  in the future that it is running within C.

  Rewrote the description of how cost is calculated.  I am
  basically doing it now, except for the marginal cost thing
  which needs a rethink.  But what I really need to worry
  about is how I am currently caching this cost.  Is that even
  viable, given the presence of day shifts and optional tasks?

29 April 2025.  Audited yesterday's cost documentation and
  tightened it up quite a lot.  It seems very good now.  Also
  worked out that adding the offset sequence to the signature
  should handle most of the signature problems.  Does adding
  the optional task assignment cost to the cost, even for
  unfinished groups, handle the rest?

30 April 2025.  Working on signatures today.  But first I've
  just revised the terminology.  It's much better but now I
  have to make sure that I use it.

1 May 2025.  Finished the cost documentation.  Next is the
  signatures documentation.  It's not looking easy.  I've
  made a start.

2 May 2025.  Documenting signatures and dominance testing.
  I've finished the whole section.  It needs a careful audit,
  and then it will be ready for implementing.  Except that
  I have not given any thought to how to cache costs.  Well,
  I can cache the stuff derived from resource constraints,
  and index it using the new trie structure.  The other
  stuff will probably require a scan of the group.

3 May 2025.  Audited what I wrote yesterday; it's great.
  Made a first attempt at "an audit of the conditions which
  determine whether a set of tasks can form a group, plus
  the costs that go to make up the cost of a finished group,
  will show that nothing has been left out."  It needs checking.

5 May 2025.  Audited the dominance test documentation.  It's
  in great shape now and ready to implement.

6 May 2025.  Added a KHE_IG_TASK type with the fields we need,
  accessible from the solver by task index.  Written
  KheIgTaskGroupNonMustAssignCost and am using it.
  KheIgTaskGroupSetIndexes written, not used yet.

7 May 2025.  I've improved the documentation to make it clearer
  how costs and dominance are calculated, including optimizations.
  So it should be easy to follow what it says when implementing.
  Audited the dominance test documentation.  It's in great shape
  now and ready to implement.

8 May 2025.  Nothing done.

9 May 2025.  Replaced the interval table with a trie table.  Also
  added KheTrieClear and implemented it using a free list of
  KHE_TRIE_U objects.  KheIgTaskGroupCost is done and audited.

10 May 2025.  Audited the changes to the trie table, all seems
  good.  DEBUG37 is 1 in khe_sr_interval_grouping.c, this will
  call KheTrieTest.

  Revised KheIgSolnDominates.  Have clean compile, so the whole
  module is ready for auditing and testing.

25 May 2025.  Back at work today after a bushwalking break.  I've
  audited the documentation of the interval grouping algorithhm,
  and made a few tiny changes.  Now for the code.

26 May 2025.  Auditing the interval grouping code.  Not a great
  job but it's all done and ready for testing.

27 May 2025.  Testing interval grouping.  Found one bug, which
  was that the last day of an interval could be -1, which was
  not acceptable to the trie module.  Patched that up.

  It's running right through now but it is too slow, the full
  run was 133 minutes and there was still one undersized group.

    IgTimeGroupSolns(4Fri4, 254277370 made, 10570 undominated, 4000 kept)

  That's 254,277,370 made.

  Did some tidying up of type KHE_IG_MTASK.  Improved one field
  and removed another.

  Replaced KheIgTaskGroupExtendable by KheIgTaskGroupIsExtendable,
  in preparation for building a matching whose edge cost is the
  negative of the domain size.

28 May 2025.  I've documented the new step (3) idea, of matching
  undersized groups with tasks before the main solve begins.  I
  now have to design the implementation of this new idea.  I think
  I need a single list of tasks.  There will be issues in working
  out whether the main algorithm can apply task symmetry.  So this
  is looking like a major change.

29 May 2025.  Revising the code, trying to bring it closer to the
  documentation.  Done it for constraint classes, now working on
  mtasks and tasks.

  Wrote KheIgExpanderSolve today, it needs an audit and test.

30 May 2025.  Still revising the code.  It's getting better.
  I've now checked how tasks and mtasks get added, to make sure
  all is in sync.  It's tricky so I've documented it in the
  header comment of type KHE_IG_TIME_GROUP.  The whole module
  is now audited and ready to test except KheIgExpanderSolve.

31 May 2025.  I've revised the code in preparation for matching
  undersized groups, although the matching itself is not done yet.
  The code has been audited, I'm testing it now.  It seems to be
  running all right, but surprisingly slowly.  It took practically
  forever to get through 4Tue, although that is perhaps not surprising:

    IgTimeGroupSolns(4Tue4, 7099272 made, 964 undominated, 964 kept)

  That's 7,099,272.  But so few are undominated!  All this is
  terribly boring but I have to slog through it.  Actually this
  is after adding optional tasks, which seems odd since there
  are only a few needed.  How many are actually added?

  Here are the lengthener tasks actually added:

    [ KheIgSolnAddLengthenerTasks(best_igs, igsv)
      IgTask(4Thu:Night.0, nma_cost 0.00031, optional true)
      IgTask(4Thu:Night.5, nma_cost 0.00031, optional true)
      IgTask(4Thu:Night.18, nma_cost 0.00031, optional true)
      IgTask(4Thu:Night.19, nma_cost 0.00031, optional true)
      IgTask(4Tue:Night.2, nma_cost 0.00031, optional true)
      IgTask(3Fri:Night.1, nma_cost 0.00031, optional true)
      IgTask(4Tue:Night.3, nma_cost 0.00031, optional true)
      IgTask(3Fri:Night.2, nma_cost 0.00031, optional true)
      IgTask(3Sat:Night.0, nma_cost 0.00031, optional true)
      IgTask(3Tue:Night.0, nma_cost 0.00031, optional true)
    ] KheIgSolnAddLengthenerTasks returning true

  These seem to be quite wrong, because we need tasks to
  lengthen 3Wed-3Fri (i.e. 3Tue and 3Sat) and tasks to
  lengthen 3Sat-4Mon (i.e. 3Fri and 4Tue).  Actually only
  the 4Thu ones are wrong, and they are probably present
  because of 4Fri-4Sun tasks being erroneously considered
  to be undersized.  It's fixed now:

    [ KheIgSolnAddLengthenerTasks(best_igs, igsv)
      IgTask(4Tue:Night.2, nma_cost 0.00031, optional true)
      IgTask(3Fri:Night.1, nma_cost 0.00031, optional true)
      IgTask(4Tue:Night.3, nma_cost 0.00031, optional true)
      IgTask(3Fri:Night.2, nma_cost 0.00031, optional true)
      IgTask(3Sat:Night.0, nma_cost 0.00031, optional true)
      IgTask(3Tue:Night.0, nma_cost 0.00031, optional true)
    ] KheIgSolnAddLengthenerTasks returning true

  Added expand_used flags to the task groups and tasks,
  replacing field task_group_used in the expander.  Seems
  to be working.  The full run is current taking 14 minutes.

  The current final solution has a duration 6 group, but it
  is one late plus 5 night, so that's fine, except it isn't.
  Also there is an undersized group that could have been
  corrected by adding an optional task which is there,
  all ready to go, on 3Tue.  So why wasn't it used?
  Also, why are there so few compatible solutions?  Also,
  there are still places where we could swap two tasks
  and improve the domains, e.g. 3Tue Night.15 and Night.4.
  Also, we are using three Lates on 3Sat but we only need
  to use one.  We added three to get a choice of domains,
  but we don't want to use three.  Can we discourage their
  use somehow?

1 June 2025.  Starting work on the undersized groups case.
  Handling continuing assignments separately now; the code is
  written and tested - it seems to be working.  Also I've written
  the undersized groups code (KheIgExpanderHandleUndersizedCases).
  It's ready for an audit and test.

2 June 2025.  Auditing and testing the undersized groups code.
  Tidied up the code for creating the various kinds of task groups,
  by defining KheIgTaskGroupMakeHistory, KheIgTaskGroupMakeInitial,
  KheIgTaskGroupMakeSuccessor, and KheIgTaskGroupMakeDummy, which
  together cover all cases.  Good move, that.  The code seems to
  be working, although the results are a bit flaky.

  Running time is still too slow:

    KheIgSolverSolveConstraintClass returning (groups_count = 60, 99.7 secs)

  But I'm not greatly concerned by this at the moment.

  The current problem is that we've added optional tasks on
  3Tue but they are not being used.  Why not?  Because the
  cost of an optional task is currently rated at 30, and
  the cost of an under-sized group is rated at 15.

  Here is the next problem:

      KheIgTimeGroupDebugCompare 3Sat4:  0 compatible

3 June 2025.  I'm now adding domains to the LOR solution, courtesy
  of a simple call to KheTaskTreeMake.  So the next step is to
  include domains in comparisons.

4 June 2025.  Working on adding domains to the compatibility comparison
  made by KheGroupedTasksDisplayCompatible.  All done and ready to test.

  Can we sort the display somehow so that compatible ones
  come out the same?  Perhaps sort by length?  I've made
  a start on this by storing the groups before printing them.

5 June 2025.  Improving the group display today.  It's now printing
  the groups in sorted order, which should make comparisons easier.

6 June 2025.  Improving the group display today.  I've implemented
  the multiset generator that I will need.  The next step is to
  define clusters of task groups and clusters of tasks.

7 June 2025.  Working on ig task classes and ig task group classes.
  Audited them both, and got some debug output showing that they
  are being assembled correctly.  Time to use them now.  Started
  work on KheIgExpanderHandleUndersizedAndOtherCases.  Have clean
  compile but KheIgExpanderHandleOtherCases is still to do.

8 June 2025.  Working on KheIgExpanderHandleOtherCases.  It's
  done, it needs an audit.

9 June 2025.  Audited KheIgExpanderHandleUndersizedAndOtherCases
  and KheIgExpanderHandleOtherCases and made the two functions as
  similar as possible.  It all looks pretty darn good.  Started
  testing, it seems to be working, but many solutions are still
  being kept (obviously) and it is still slowish.

11 June 2025.  Had yesterday off.  Today I am analysing the
  solutions produced by the new code.  There are a lot of them,
  but by the end of the cycle there are none.

12 June 2025.  Still analysing.  The second result produced by
  KheIgExpanderHandleUndersizedAndOtherCases seems to be on
  the way to a compatible solution.  Why didn't it finish off?

13 June 2025.  Still analysing.  Have a much better system now
  for finding, displaying, and keeping track of compatible
  solutions.  Sadly, it's running out of memory.  I need to
  do a careful analysis of how memory for it is being used
  and what I need to keep and what I can throw away.

14 June 2025.  Memory-using display and comparison functions:

  KheIgSolverSolveConstraintClass:  This builds igsv->other_gtd
  by calling KheGroupedTasksDisplayBuild.  We should be able to
  wear the memory cost here, because it's a one-off.

  KheIgSolnDebugTimetable is another one but it grabs and frees
  its own arena, so it should not be a source of memory problems.

  KheIgExpanderHandleOtherCases is the problem.  It builds a local gtd
  by calling KheIgSolnDisplay, then calls KheGroupedTasksDisplayCompatible.

  Found and fixed a bug in the memory handler, where two_to_index
  was being assigned an int value that should have had type size_t.
  Still not sure why such a large chunk was being requested though;
  and in fact I am now running out of memory, which needs looking into.

15 June 2025.  Fixing the memory leak in the group display module.
  Also found that I had to share the domain finder, so doing that
  now.  The current test suggest that the memory problems are fixed.
  But I'm still getting an awful lot of solutions:

    IgTimeGroupSolns(3Fri4, 1454094 made, 7139 undominated, 4000 kept)

  So there is more analysis to do here.

16 June 2025.  The immediate task is to make the multiset generator
  run faster.  After that I can get back to interval grouping.

17 June 2025.  I've added a list of task classes to each task group
  class.  These are the task classes that can extend the task group
  class's task groups.  Also added expand_used_count fields.

18 June 2025.  Getting somwhere.  KheIgExpanderAssignUndersizedTaskGroups
  and KheIgExpanderDoAssignUndersizedTaskGroup are written.  They need a
  careful audit, then carry on to replace KheIgExpanderHandleOtherCases.

19 June 2025.  Audited KheIgExpanderAssignUndersizedTaskGroups and
  KheIgExpanderDoAssignUndersizedTaskGroup.  Made a few changes.
  Finished off KheIgExpanderDoAssignRemainingTasks and
  KheIgExpanderAssignRemainingTasks.  They need an audit, then
  I will be ready for testing.  Also removed some old code,
  including the matching and KHE_IG_EXPAND_INFO with its
  multiset enumerator.

20 June 2025.  Completed an audit of submodule KHE_IG_EXPANDER.
  Ready to test.

21 June 2025.  Testing.  First results:

    IgTimeGroupSolns(3Fri4, 1862164 made, 6 compatible, 16078 undominated)
    KheIgSolverDoSolve starting to solve for 3Sat4
    IgTimeGroupSolns(3Sat4, 354196 made, 0 compatible, 5367 undominated)

  It looks as though not keeping all the undominated solutions is
  causing us to wander off the compatible track.  So let's try
  again, keeping up to 10000.  No, again we go incompatible:

    IgTimeGroupSolns(3Fri4, 3189356 made, 3 compatible, 9450 undominated)
    KheIgSolverDoSolve starting to solve for 3Sat4
    IgTimeGroupSolns(3Sat4, 29912 made, 0 compatible, 45 undominated)

  I need a close look at what is going on here.  Run time is 63.5
  minutes, but that is with both phases.  I'm getting this:

    new compatible solution dominated by this incompatible solution

  I need to look into this to see whether my definition of
  compatibility needs adjusting.

  According to my current debug output, there were 3 compatible
  solutions found for 3Fri, but all 3 were dominated by
  incompatible solutions.  So I need to look into that now.

  Yes, the supposedly incompatible solution was slightly
  more useful than the compatible one.  So it should have
  been marked as compatible.  I've also now removed length
  5 groups from the compatibility test.  But now I have
  458 compatible solutions on 3Fri, and 0 compatible
  solutions on 3Sat.  This is presumably owing to the
  Late shifts on 3Sat, which are not present in LOR.
  So what to do about them?  Ignore optional groups
  in compatibility tests, presumably.

22 June 2025.  Still testing.  Ignoring optional tasks in
  the compatibility test now, but I still have no compatible
  solutions on 3Sat.  I'm going to have to build an ig soln
  for LOR17 and use dominance instead of compatibility.

23 June 2025.  Working on building an ig soln for LOR17 and
  using dominance instead of compatibility.

24 June 2025.  Testing the code for building an ig soln for LOR17.
  It seems to be working, so now I have to remove other_gtd and
  use other_igs instead, with a dominance test.  Done that and
  I am now getting some (allegedly) compatible solutions.  But
  on the first few days there are no compatible solutions.  So
  now I need to look closely at what makes them incompatible.

24 June 2025.  The incompatible solutions near the start were
  caused by assign by history making an arbitrary different
  choice to LOR17, as I've now proved by removing assign by
  history.  I'll put it back again when compatibility is
  no longer an issue for me.

  The bug seems to be because we seem to be comparing a
  solution with optional tasks in it against a solution
  without optional tasks in it.  Are we clearing out
  the solutions from the first run correctly?  Look at this:

    KheheIgSolnDominates failing (10 != 8)
    [ Soln(0.00000, varying_domain_groups 4, randomizer 0, 10 groups) run 2
      ... 10 task groups ...
    ] 
    [ Soln(0.02970, varying_domain_groups 14, randomizer 0, 8 groups) run 0
      ... 8 task groups ...
    ]

  How can that be?  Because it was comparing with the original LOR17
  solution, which we don't need or want.  Fixed now.

  Currently compatible all the way.  But we are still getting
  some undersized groups.  Why?

26 June 2025.  I now have an accurate print of the LOR17 solution, one
  which includes three groups of length 1, holding three night tasks that
  LOR17 did not assign resources to.  There are two of these on 4Mon, and
  one on 4Sat.  Arguably I should not be duplicating these non-assignments
  in my solution, so I can't expect compatibility after 4Mon.

  Commented out a lot of now unused code.

  Got right through the run (52 minutes) with these results:

    solution without optional tasks:
    [ GroupedTasks(Constraint:17, cost 0.00030, undersized_durn 2,
      oversized_durn 0)
      ...
    ]

    solution with optional tasks:
    [ GroupedTasks(Constraint:17, cost 0.00060, undersized_durn 0,
      oversized_durn 0)
      ...
    ]

  The undersized duration ones are two on 3Wed - 3Fri.  The run is
  also much too slow:

    IgTimeGroupSolns(2Thu4, 115846 made, 1 compatible, 3754 undominated)

  The strangely large solution cost for the LOR17 solution seems to
  build up steadily day by day:

    [ KheIgSolverBuildOtherSoln
      KheIgSolverBuildOtherSoln init soln cost 0.00000
      KheIgSolverBuildOtherSoln 1Mon4 soln cost 0.00000
      KheIgSolverBuildOtherSoln 1Tue4 soln cost 0.00330
      KheIgSolverBuildOtherSoln 1Wed4 soln cost 0.00585
      KheIgSolverBuildOtherSoln 1Thu4 soln cost 0.00810
      KheIgSolverBuildOtherSoln 1Fri4 soln cost 0.00930
      KheIgSolverBuildOtherSoln 1Sat4 soln cost 0.01035
      KheIgSolverBuildOtherSoln 1Sun4 soln cost 0.01275
      KheIgSolverBuildOtherSoln 2Mon4 soln cost 0.01320
      KheIgSolverBuildOtherSoln 2Tue4 soln cost 0.01695
      KheIgSolverBuildOtherSoln 2Wed4 soln cost 0.01935
      KheIgSolverBuildOtherSoln 2Thu4 soln cost 0.02190
      KheIgSolverBuildOtherSoln 2Fri4 soln cost 0.02280
      KheIgSolverBuildOtherSoln 2Sat4 soln cost 0.02415
      KheIgSolverBuildOtherSoln 2Sun4 soln cost 0.02625
      KheIgSolverBuildOtherSoln 3Mon4 soln cost 0.02655
      KheIgSolverBuildOtherSoln 3Tue4 soln cost 0.02970
      KheIgSolverBuildOtherSoln 3Wed4 soln cost 0.03225
      KheIgSolverBuildOtherSoln 3Thu4 soln cost 0.03450
      KheIgSolverBuildOtherSoln 3Fri4 soln cost 0.03615
      KheIgSolverBuildOtherSoln 3Sat4 soln cost 0.03705
      KheIgSolverBuildOtherSoln 3Sun4 soln cost 0.03855
      KheIgSolverBuildOtherSoln 4Mon4 soln cost 0.03930
      KheIgSolverBuildOtherSoln 4Tue4 soln cost 0.04440
      KheIgSolverBuildOtherSoln 4Wed4 soln cost 0.04725
      KheIgSolverBuildOtherSoln 4Thu4 soln cost 0.04965
      KheIgSolverBuildOtherSoln 4Fri4 soln cost 0.05025
      KheIgSolverBuildOtherSoln 4Sat4 soln cost 0.05055
      KheIgSolverBuildOtherSoln 4Sun4 soln cost 0.05445
    ] KheIgSolverBuildOtherSoln returning

  What's that about?  Could I be using monitors from the
  wrong solution?

27 June 2025.  Setting expand_used = true while building other
  soln has reduced the cost to 225.  Now is that the true cost?
  Here are the five task groups that are attracting non-zero
  cost in the LOR17 solution:

    (A) this non-self-finished igtg has cost 0.00045:
        [ OrdinaryTaskGroup(4Mon:Night.1, HeadNurse, class_durn 1) ]

    (A) this non-self-finished igtg has cost 0.00045:
        [ OrdinaryTaskGroup(4Mon:Night.23, Caretaker, class_durn 1) ]

    (B) this non-self-finished igtg has cost 0.00030:
        [ OrdinaryTaskGroup(4Wed:Night.20, Caretaker, class_durn 4) ]

    (A) this non-self-finished igtg has cost 0.00075:
        [ OrdinaryTaskGroup(4Sat:Night.6, Nurse, class_durn 1) ]

    (B) this self-finished igtg has cost 0.00030:
        [ OrdinaryTaskGroup(4Sun:Night.12, Caretaker, class_durn 1, last) ]

  I've verified that cases (A) are groups of length 1 which are left
  unassigned in LOR17, which would have cost 30 usually, but they are
  being charged as undersized sequences here; and that cases (B) are
  groups which start on a Sunday, so are being penalized for incomplete
  weekends; but in the true LOR17 solution they start with a day shift
  on the Saturday immediately before the Sunday.

  Tried a run with compatibility testing turned off, and also the
  second run with optional tasks turned off (on the grounds that it
  takes a lot of time, makes very little difference, and the usual
  ejection chains repair might do just as well).  The cost was 30
  and the running time was

    [ "INRC2-4-100-0-1108", 1 solution, in 9.0 mins: cost 825.89400 ]

  There are certainly too many solutions right now, e.g.

    IgTimeGroupSolns(2Fri4, 2261456 made, 9542 undominated, 9542 kept)

  I'm pretty sure that one groups dominates another when its class
  duration is strictly smaller but still at least the minimum limit.
  So adding that in we find 

    IgTimeGroupSolns(2Fri4, 1651772 made, 5699 undominated, 5699 kept)

  It has turned out quite a lot smaller, but not a game changer.
  Final cost was 60, and running time was

    KheIgSolverSolveConstraintClass returning (groups_count = 60, 270.5 secs)

  The extra cost is because there is a group starting on 3Sun.
  Why didn't it link up with a Late on 3Sat?  Is it because
  the cost was not taken into account at that point?  Yes,
  same times mean same cost, different times mean different
  cost, and smaller is not necessarily not worse.

28 June 2025.  Working on the design of what I have decided to call
  "early dominance".  I've documented something and I'm now starting
  to implement it.  I've added link objects and I'm using them, but
  I'm not yet doing anything new with them.  I've written and am
  using KheIgLinkUseBegin, KheIgLinkUseEnd, and KheIgLinkIsOpen,
  but I'm not calling KheIgLinkAddBlock yet, so every link is
  always open at present.  Also written KheIgLinkDebug.

29 June 2025.  Revised yesterday's doc, making it more general,
  which is good.

30 June 2025.  Reached a tipping point with the documentation, so
  I've decided to move it to an appendix and do it properly.  I've
  made a start.

2 July 2025.  Still working on the documentation.  Going well;
  it's all done except that "Early dominance testing" needs an
  audit to bring its terminology closer to the other sections',
  and "Time complexity" needs a general (but probably quick) audit.

3 July 2025.  The new documentation is in good shape now.  The
  next step is to bring the implementation into conformity with
  it, especially the new "Early dominance testing" section.

4 July 2025.  Finished a careful audit of the entire new doc,
  including adding a new proof that the number of solutions
  produced by a single expansion is polynomial.  I've started
  auditing and revising the code to make it conform to the
  documentation.  New code will be neeed to implement the
  "Early dominance testing" section.  Currently I'm up to
  the start of KHE_IG_TASK_GROUP_CLASS.

5 July 2025.  I got to the end of auditing the code, except that
  I left behind a list of things still to do.  Commented out
  varying domains, because early dominance testing should do
  the equivalent, probably better.

6 July 2025.  Worked out the (non-)connection between o(g) and
  n(g) > 0, and documented it.  All the documentation is in
  good shape now and ready to be implemented.  So I started by
  checking KheIgTaskSameClass and KheIgTaskGroupSameClass again;
  they're all good.

7 July 2025.  Did a bit more work on the documentation, listing
  the attributes of tasks explicitly.  Tried a run, everything
  still seems to be working after various small adjustements.
  Also wrote KheIgLinksAreBlocking, so I'm ready to test early
  dominance testing, except that KheIgLinksAreBlocking needs a
  careful audit.

8 July 2025.  Audited KheIgLinksAreBlocking code and documentation.
  I found several problems but it seems to be in good shape now.
  I've added some debug output and started testing.  Found a logic
  error in KheIgLinksAreBlocking, not sure about it yet.

9 July 2025.  I've revised the documentation to correct yesterday's
  logic error.  I was blocking the links that were doing the blocking,
  not the ones that were being blocked.  I've implemented it and
  tested it, and I seem to be getting the same solution that I got
  before (undersized_durn 2) only in 139 seconds rather than 10
  minutes.  Not good enough yet, but better.

  Moved trie indexes into task groups and am now using task
  groups as keys.  Had to have one trie per time group, but
  that's OK.  All implemented and tested, it's working well.
  Running time is now 125 seconds, so this helped (a bit).

10 July 2025.  Down to 119 seconds by requiring the subset
  tests to be either proper or in the right sequence.

  Added links to the empty task group class.  I got the usual
  solution, in 115 seconds, so it seems to be working.

  Moved KheIgTaskGroupNonMustAssignCost to a field of
  KHE_IG_TASK_GROUP.  This consumes memory but is probably
  a good idea, because KheIgTaskGroupNonMustAssignCost
  is called during dominance testing.  And indeed I got
  the usual solution, this time in 114 seconds.  Not sure
  why there wasn't a bigger improvement.  I ran it again
  and got the usual solution in 124 seconds.

  Discovered I was building classes of finished task
  groups.  Got rid of that.  Same solution, 119 secs.
  But there was a bug, something to do with overhang
  cases, so I've returned to what I had before.

11 July 2025.  Now caching initial task groups.  This
  should be quite helpful, given that 20% or more of
  task groups are initial.  It was a bit buggy at first,
  but all fixed now and testing.  Same solution found,
  running time is 134 seconds.  Why does it take longer?
  The only additional code is testing that prev != NULL
  when freeing.  Could that take all this time?

  Removed leader tasks from the task grouper, and leader mtasks
  from the mtask grouper.  All done and documented.  Also removed
  KHE_TASK_GROUP_DOMAIN_TYPE, which is no longer needed.
  
  Trying unchecked task group making, that should save some
  time.  139 seconds.  Every time I add something, it takes
  longer.  How is that possible?

12 July 2025.  I've decided to take a step back and ponder things
  generally.  Getting down from 2 minutes to 5 seconds will not
  be easy.

14 July 2025.  Working on early dominance in the case where
  one task is NULL.  Working on the documentation right now.
  It seems to be in good shape, it needs an audit.

15 July 2025.  I've redone the linking code according to a much
  clearer plan that I thought up overnight.  It's all implemented
  and audited and seems to be ready to test, including making
  sure that links with NULL fields influence the recursion.

  Started testing.  Fixed a couple of small bugs and now it
  is running, but it seems very slow - stuck on 1Fri4.  And
  no wonder, look at this:

    IgTimeGroupSolns(1Fri4, 1875710 made, 15890 undominated, 15890 kept)
    IgTimeGroupSolns(2Thu4, 1956849 made, 28064 undominated, 28064 kept)

  Is the dominance testing working properly?  Perhaps not.  If
  we can get through to the end, we'll start by looking at the
  final solution.

  OK, it finished in 237 minutes and found a solution of cost 75 -
  inferior but not hopeless.  Needs looking at.

16 July 2025.  The senior nurses lost their history stuff - why?
  Something to do with the change of domain?  Apparently not,
  I've checked, and the domain is not used enough.

  Despite the bug I have added a solution cache which partitions the
  solutions by KheIgSolnNonSelfFinishedGroupCount(igs).  This should
  save a lot of time when dominance testing, that is if there are
  several different counts on any day.  It seems to be working:

    KheIgSolnCacheFinalize(9: 11, 10: 8, 11: 4)
    KheIgSolnCacheFinalize(9: 16, 10: 30)
    KheIgSolnCacheFinalize(10: 135, 11: 373)
    KheIgSolnCacheFinalize(11: 3186)

  This shows that we are usually getting at least two partitions,
  which should be enough to be helpful.  And look at this one:

    KheIgSolverDoSolve starting to solve for 1Fri4
    KheIgSolnCacheFinalize(0: 1, 1: 8, 2: 38, 3: 159, 4: 468,
      5: 1044, 6: 1851, 7: 2706, 8: 3335, 9: 3519, 10: 2761)

  This must be seriously helpful.

17 July 2025.  Working on the poor start today.  Actually everything
  except the start looks pretty good.

18 July 2025.  I worked out yesterday that the problem seems to be
  that the algorithm is evaluating the cost of history intervals
  wrongly.  I've fixed this and the fix seems to be working, but
  it is still running very slowly.  For example:

    Final solutions(2Fri4, 10811280 made, 29229 undominated, 29229 kept)

  This is 10,811,280 made.  There was also a MAX_KEEP cutoff:

    Final solutions(3Thu4, 4460904 made, 46344 undominated, 40000 kept)

  The run ended after 94 minutes with a solution with cost 75.  It
  starts off correctly now, with history handled properly.  But there
  are still some unnecessary short groups.  The first starts on 3Wed
  so could be caused by the MAX_KEEP cutoff on 3Thu.  So I'll need
  another run now, with no cutoffs.

  But first I've installed the new trie soln cache:

    IgTimeGroupSolns(2Fri4, 10811280 made, 2 compatible, 29229 undominated,
      29229 kept)

  So I'm getting the same numbers of solutions.  Final solution
  had cost 75, as before, but run time is now 134 seconds.  So
  the next question is, why does my solution include those three
  undersized groups?

19 July 2025.  There is definitely something wrong on 3Wed.  I have
  a duration 2 group starting then, but by moving one Caretaker task
  (e.g. 3Fri:Night.18) from its current group to the duration 2 one,
  I can increase the duration to 3 with no other penalties.  Have I
  overdone the early dominance testing?  Does it take length into
  account?  No, I've reread the early dominance testing stuff and
  it should be fine, because the two task groups have different
  lengths so should be in different task group classes.  And the
  short one is undersized, so it should get first bite of all tasks.

  Let's test, at the moment we transition from filling undersized
  task groups to the rest, whether all undersized task groups have
  been used.  I've done this and there seem to be an awful lot of
  undersized and unused groups around.

  Even history task groups at the start are undersized and unused:

    undersized and unused before 1Mon4:
      [ HistoryTaskGroup(2, HN_1) ]
      [ HistoryTaskGroup(3, HN_8) ]
    undersized and unused before 1Mon4:
      [ HistoryTaskGroup(2, HN_1) ]
    undersized and unused before 1Mon4:
      [ HistoryTaskGroup(3, HN_8) ]

  So I need to look into why this is.  Undersized task groups
  are supposed to be completely used by this point.

20 July 2025.  Found and fixed the "undersized and unused" problem,
  it was just a bug.  Now, incredibly, I am getting a solution of
  cost 75 in 0.9 seconds.  The three undersized groups are all in
  the vicinity of where the LOR17 solution has unassigned tasks, so
  they may be inevitable.  Hurrah, this is what I get after 5 mins:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01660 ]

  These are the biggest days:

    IgTimeGroupSolns(2Fri4, 75603 made, 2 compatible, 7058 undominated)
    IgTimeGroupSolns(3Thu4, 27239 made, 1 compatible, 6582 undominated)
    IgTimeGroupSolns(3Fri4, 68169 made, 7 compatible, 5305 undominated)

21 July 2025.  Working on the balance solver today, hoping to get a
  better (or rather, correct) value out of it for marginal cost.
  I've written a new version of the documentation.  I need to
  implement that version now.

22 July 2025.  Working on the balance solver.  Places where it is used:

  khe_sm_yourself.c
    When supply < demand, install redundancy.  This does not make
    any reference to supply or demand costs, so it will remain
    unaffected by whatever else I do with the balance solver.

  khe_sr_interval_grouping.c
    When supply < demand, include resource cost whenever we choose
    to group an optional task with a compulsory task.  At present
    I'm including task cost, not resource cost, which is what has
    prompted this whole review of the balance solver.

  khe_sr_single_resource.c
    In this code the balance solver is used only when debugging.
    So it doesn't constrain what I do at all.

  khe_sr_weekend_grouping.c
    In this code the balance solver is used to set variable over_cost,
    the cost of using one optional task.  This is discussed in the
    weekend grouping section of the Guide, where it is said that the
    cost is the cost in resource monitors, but the code is finding a
    task cost - the same mistake as the one I made in interval grouping.

  Do I actually need a balance solver object?  What about a function
  that returns the four values (total supply, total demand, task
  cost, and resource cost) all together?

  OK, I've written a very nice new balance solver section.  It's
  audited and ready to implement.

23 July 2025.  Working on the balance solver.  I've done it, and
  also I'm using the new interface throughout KHE now.  I've
  audited it and it's ready to test.

24 July 2025.  Had a last look over the balance solver and changed
  one or two small things.  Test is working well, did not change
  the solution.

  Removed KheTaskAssignmentCostReduction.

  Removed KHE_TASKING from the implementation and documentation.
  Did a test, everything still seems to be working.

  I've decided that we do need task bound objects.  They could be
  replaced by resource groups, but then we would lose the analogy
  between how task domains are handled and how meet domains are
  handled.  Meet bounds cannot be replaced by time groups, because
  of the complications that arise when meet durations change.

25 July 2025.  Started documenting the cost of a compulsory group
  when it remains unassigned.

26 July 2025.  Revising the documentation to allow compulsory groups
  to remain unassigned.  Surprisingly, it is simplifying things.  Can
  I get rid of the optional flag now?  All groups are optional if
  compulsory groups can remain unassigned.  Yes, the only real
  use of optional is in KheIgTaskGroupUndersized, and we could
  replace it by igtg->primary_durn != 0.

    * Remove optional flags

    * Replace non_must_assign_cost by task_cost and total_task_cost,
      where task_cost is the new (i) + (ii):

      res->task_cost = (asst_cost - non_asst_cost);
      if( asst_cost >= non_asst_cost )
        res->task_cost += igsv->marginal_cost * KheTaskTotalDuration(task);

    * Calculate the group cost and then

      if( a(g) == NULL && res > 0 )
        res = 0;

    * To start with, get rid of special case for undersized.  Let's
      see if we can solve efficiently without it.  Good riddance if so.

  There is a problem here when we include optional Late tasks on
  Saturday.  We should not incur any cost if they are not assigned.
  Still to work this out.  Perhaps we need

    res->task_cost = res->primary_durn * (asst_cost - non_asst_cost);

  or something.  Anyway it's only the primary parts of tasks whose
  assignment we have to worry about.  Perhaps the passed-in
  non_must_assign parameter could be a secondary parameter.

27 July 2025.  Still revising the documentation to allow compulsory
  groups to remain unassigned.  Some puzzles to work through.  I've
  sorted out c(g), now I need to sort out non-must-assign cost.
  Do I even use it?  If so, what for?

  Removed all reference to the terms "optional" and "compulsory"
  from the documentation.  But can I do it in the code?

29 July 2025.  Coded up the changes I've made (gulp).  Ready to test,
  although a full audit of the code, including updating the comments,
  would be good to do first.

30 July 2025.  Audited the documentation again, it's pretty darn good.
  Also audited the code.  It's ready for testing.

  Started testing.  It's running, but it's very slow.  I need to look
  into what is slowing it down.  I may have to replace v(s) in the
  dominance test with o(s).  All costs get included in the end,
  arguably, so they don't matter.  No, not if they go into a group
  where non-assignment is preferred.  Does this apply to equivalence
  as well?  Anyway it all needs looking into.

    KheIgSolverDoSolve starting to solve for 1Mon4
    IgTimeGroupSolns(1Mon4, 60 made, 27 undominated, 27 kept)
    KheIgSolverDoSolve starting to solve for 1Tue4
    IgTimeGroupSolns(1Tue4, 378 made, 249 undominated, 249 kept)
    KheIgSolverDoSolve starting to solve for 1Wed4
    IgTimeGroupSolns(1Wed4, 16912 made, 6104 undominated, 6104 kept)
    KheIgSolverDoSolve starting to solve for 1Thu4

  at which point it basically died.  I eventually got this:

    IgTimeGroupSolns(1Thu4, 653091 made, 1 compatible, 186153 undominated,
     50000 kept)
    KheIgSolverDoSolve starting to solve for 1Fri4

  and then killed it.

31 July 2025.  Working on yesterday's running time debacle.  I was
  assigning very wrong task costs, so I fixed that, and now the
  costs seem to be right.  But still the algorithm is running very
  slowly.  It seems to be because we are taking v(g) into account
  now, which is more detailed than the old o(g).  The first and
  third solutions in the current printout illustrate the issue:

  [ GroupedTasks(Constraint:17, cost -2.00000, undersized_durn 22, oversized 0)
    +--------------+--------------+--------------+--------------+--------------+
    |       HN_1 2<|       HN_8 3 |      NU_19 4 |      NU_32 4 |      CT_72 5 |
    |              |              |              |              |              |
    +              +              +              +              +--------------+
    |   7: Night.6<|   8: Night.0 |  9: Night.12 | 10: Night.13 |   0: Night.5<|
    |     Nurse/30<|  HeadNurse/H |  Caretaker/H |  Caretaker/H |      Nurse/H<|
    +--------------+--------------+--------------+--------------+--------------+
  ]

  [ GroupedTasks(Constraint:17, cost -2.00000, undersized_durn 22, oversized 0)
    +--------------+--------------+--------------+--------------+--------------+
    |       HN_1 2<|       HN_8 3 |      NU_19 4 |      NU_32 4 |      CT_72 5 |
    |              |              |              |              |              |
    +              +              +              +              +--------------+
    |   7: Night.5<|   8: Night.0 |  9: Night.12 | 10: Night.13 |   0: Night.6<|
    |      Nurse/H<|  HeadNurse/H |  Caretaker/H |  Caretaker/H |     Nurse/30<|
    +--------------+--------------+--------------+--------------+--------------+
  ]

  I've deleted most of what is identical in both solutions.

  What happens if we ignore v(s) and v(g) when building classes?
  Or just separate into positive and non-positive?  It would be
  hard to justify this as is.  The smallest cost reductions have
  to go into the smallest groups.  Is this a linking thing?

  What about this:  if the primary durations of both groups are
  greater than one, then we don't have to compare their costs.
  I tried this and got a pretty good solution (two undersized
  groups) in 13.3 seconds.  There are still some big days,
  notably this one:

    IgTimeGroupSolns(3Fri4, 1539039 made, 710 compatible, 6181 undominated)

  We don't seem to be generating any unassigned groups though.

  The final solution was pretty good:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01550 ]

  which is rel = 1.24.

  Running with DEBUG_COMPATIBLE = 0 gave running time 12.2 seconds,
  which is arguably not far off what we want.  Reducing MAX_KEEP to
  4000 gave running time 9.5 seconds, although the solution was
  marginally worse (undersized_durn = 3 instead of 2).

2 August 2025.  I've been working on a more systematic way to
  handle unassigned groups and v(s) efficiently.  It's all
  documented now, audited, implemented, and ready to test.
  First test produced the usual (good) solution in 3.7 seconds.

  Trying not treating undersized as special.  It's running but
  a lot more slowly.  For example:

    IgTimeGroupSolns(1Thu4, 2905631 made, 98883 undominated, 50000 kept)

  I aborted after that.  I'm going to have to do something where I
  treat undersized with 2 or more tasks as special, but undersized
  with 1 task as not special.  Done that and started testing.  It's
  faster but not very fast:

    IgTimeGroupSolns(1Thu4, 59560 made, 34393 undominated, 34393 kept)
    IgTimeGroupSolns(1Fri4, 2715256 made, 229576 undominated, 50000 kept)

  Not really satisfactory, but worth running to the end to see what
  solution we get.  There are a lot of undominated solutions - why?
  I've tightened up the dominance test (it no longer checks task
  cost) and it's better but there are still a lot of undominated
  solutions.  For example:

    IgTimeGroupSolns(3Thu4, 1108998 made, 68045 undominated, 50000 kept)

  So there is more thinking to do here.  The final solution has
  quite a few duration 1 groups, which is something.  What I'm
  getting may be inevitable if undersized taks groups are allowed.
  Incredible, I changed the undersized test from

    igtg->prev != NULL && igtg->primary_durn < igsv->min_limit

  to

    0 < igtg->primary_durn && igtg->primary_durn < igsv->min_limit
  
  and it made a huge difference to the running time, right down
  to 1.1 seconds.  What is going on?  Yes, this would make a big
  difference.  The second test includes single time required tasks,
  the first does not.  Some careful thought needed about this.

3 August 2025.  Added a "rs_interval_grouping_complete" option to
  express to the user the issue of whether to allow undersized
  groups a fair trial or not.  I've documented the option but
  now I need to document its implementation and implement it.

4 August 2025.  Doing some testing.  With ig_complete=false, we
  get some pretty good results (undersized_durn = 2) in 3.8 seconds.
  With ig_complete=true, we get a much slower run, including this:

    IgTimeGroupSolns(1Thu4, 2905631 made, 99643 undominated, 20000 kept)

  Furthermore, at the end we got a horrible solution with
  undersized_durn 44, and the running time was 38 minutes.
  So this seems like a vindication of my decision to add the
  ig_complete option and set its default value to false.  I
  could try to improve the running time in this case, but at
  present I don't have any ideas.

  Sorted out the case of optional tasks that are assigned a
  resource initially.  These are now classified as must-assign
  tasks, in the documentation and also in the code.  Actually
  the code was already doing it, although not very lucidly.

  "Avoid unavailable times monitors are unwanted" - but they are
  wanted if there is only one choice for r.  All done and
  documented.

  Not implemented, since there seems to be no need for it:
  Could identical task groups be shared by multiple solutions?  It
  would need a cache, indexed by a (prev task group, new task)
  pair.  It would dramatically reduce the memory footprint, and
  there would be a time saving on the cost calculation too.  In
  fact it would be worth storing the cost in the task group.  Could
  the cache be built into the task grouper module?  Actually it
  would need to be (prev, task, type), and also history info.  But
  it could work.  Thertime_structurale is a problem though knowing when to free
  such shared objects.  How many are there?  A smallish number
  for each interval.  Keep this in back pocket for if needed.
  Use links as the cache (although there are multiple task
  groups per link), so that is easy.  But we would need
  some kind of reference count to decide, at the end of
  expansion, whether to keep a given task group or not.  At
  present all links are recycled at the end of each expansion.

  Now describing busy days processing in the interval grouping
  documentation.

  I'm basically done implementing interval grouping.  I'm not
  sure when I started it, it seems like 14 December 2024 is the
  first reference to "profile grouping" using dynamic programming.
  So that's between 7 and 8 months.  But I seem to have worked on
  other pre-solvers as well during that time.

  Ready to test more widely now.  Here is best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01580 0.01605 0.01605 0.01620 0.01625 0.01645 0.01660 0.01670
      0.01700 0.01705 0.01735 0.01735
    ] 

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150     120
    Avoid Unavailable Times Constraint (7 points)    	 70      80
    Cluster Busy Times Constraint (28 points) 	   	950    1140
    Limit Active Intervals Constraint (3 points) 	 75     240
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1560

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       25
    O  Available times (negative)     36       43
    Y  Unnecessary assignments         1        2
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       11
    Y  Unnecessary assignments        18       19
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  This 1580 is rel = 1.26, which is not really any better than
  what were getting some time ago.  Here is what I wrote on
  8 April 2025:

    "On 3 November 2024 the best 5 minute run was producing

      [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.1 mins:
	0.01560 0.01700 0.01710 0.01740 0.01745 0.01755 0.01755 0.01810
	0.01830 0.01845 0.01855 0.01895
      ]

    and we are arguably back to this or better (look at second best)."

  What we have today is similar in best but far better in second
  best and indeed all other scores.  If this 3 November 2024 result
  is the benchmark, then we are doing very well.

  Just for fun, here is a 10-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01570 0.01585 0.01605 0.01610 0.01615 0.01640 0.01645 0.01660
      0.01675 0.01680 0.01690 0.01725
    ]

  Slightly better, nothing remarkable.

5 August 2025.  Investigating how interval grouping interacts now
  with weekend grouping and assign by history.  It subsumes assign
  by history for those constraints that it handles, so we should
  call assign by history after weekend grouping.  If we give
  this value to the first phase of rs:

    rin!do rwp rcm rcg rwg rig rah (rcx!rds, rfs, rrm, rec)

  then we get this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01530 0.01535 0.01570 0.01570 0.01610 0.01625 0.01645 0.01645
      0.01670 0.01680 0.01715 0.01745
    ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (5 points) 	   	150      60
    Avoid Unavailable Times Constraint (7 points)    	 70      90
    Cluster Busy Times Constraint (28 points) 	   	950    1200
    Limit Active Intervals Constraint (3 points) 	 75     180
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1530

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       22
    O  Available times (negative)     36       46
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        1
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        6
    O  Available times (negative)      7       11
    Y  Unnecessary assignments        18       21
    X  Unassigned tasks                0        1
    ---------------------------------------------
    U - O + Y - X                     15       15

  This is very impressive indeed (best so far?), rel = 1.22.

  I need to look carefully at the remaining problems with limit
  active intervals defects.  There are two there (HN_14 and NU_18)
  which look like the two undersized groups I've accepted in my
  best solution.  There are three there that LOR also has.  That
  leaves 3 cases (CT_52, TR_75, TR_92) that need looking into.

  Checked the solution produced by interval grouping when it's
  run after weekend grouping.  It's a good solution, no problems.

  Crash in weekend grouping.  It seems to have something to do
  with required tasks vs optional tasks.  Yes, we should not
  be handling the obscure case when the less busy day has no
  optional tasks.  Documented, implemented the fix, and tested it.

  Completed a full run, saved results in res_2025_08_05.xml and
  op2_2025_08_05.  Something wrong with the last few solves.

6 August 2025.  Today's job is to look into the poor results
  from the last few solves of yesterday's complete run.  I
  expect they will be due to slow running times in interval
  grouping.

  Working on INRC2-4-110-0-1428.  The Nurse run was slow
  (140 seconds) but not totally unreasonable, and the final
  cost was 3045, which is much better than I was getting
  yesterday.  What's going on?  Is there a memory problem?
  I'll now try running 12 in parallel.  Yes, that has
  given me what I was getting yesterday.

  Tried removing the task cost comparison from the dominance
  test.  The Nurse interval grouping then ran in 47 seconds,
  as opposed to 140 seconds previously.  No loss of quality.
  Final cost 2790.  So it's an improvement but not a radical
  improvement.  I really need more.

7 August 2025.  Defined and documented compatibility set dominance
  testing.

8 August 2025.  Implemented yesterday's plan (compatibility set
  dominance testing), including debug output, and tested it.
  After various stupid travails it seems to be working.  Now I
  just need to call the new dominance test.  On previous run,
  interval grouping took 7.4 seconds plus 0.0 seconds.  On
  the run with the new dominance test it took 6.2 seconds.
  The solution seemed to be more or less the same.

  Back to instance INRC2-4-110-0-1428, previously the run took
  either 140 or 47 seconds, depending on the cost test in the
  dominance test.  Today the run took XX seconds including
  the cost test.  There were also frequent overruns of the
  MAX_KEEP limit.  Look at this shocker:

    IgTimeGroupSolns(2Wed4, 258841 made, 125013 undominated, 20000 kept)

  How can things get this bad?  Actually I think the number of
  undominated solutions just got worse.  On 1Tue4 right now it is

    IgTimeGroupSolns(1Tue4, 172 made, 74 undominated, 74 kept)

  whereas previously it was

    IgTimeGroupSolns(1Tue4, 172 made, 89 undominated, 89 kept)

  Hmm, so actually it is now slightly better.  Just not better enough.

  Investigating dodgy dominance results:

    [ GroupedTasks(Constraint:17, cost -4.00060, undersized_durn 21)
                    +--------------+--------------+
      History       |      NU_29 3 |              |
      5 (+5, -0)    |              |              |
                    +              +--------------+
      1Mon          |     Night.14 |      Night.6<|
      9 (+4, -0)    |  Caretaker/H |     Nurse/30<|
                    +              +              +
      1Tue          | 11: Night.21 |   4: Night.7<|
      13 (+4, -13)  | Caretaker/30 |      Nurse/H<|
                    +--------------+--------------+
    ]

  should dominate the fourth solution:

    [ GroupedTasks(Constraint:17, cost -4.00060, undersized_durn 21)
                    +--------------+--------------+
      History       |      NU_29 3 |              |
      5 (+5, -0)    |              |              |
                    +              +--------------+
      1Mon          |      Night.5 |     Night.14<|
      9 (+4, -0)    |      Nurse/H |  Caretaker/H<|
                    +              +              +
      1Tue          |  11: Night.7 |  4: Night.16<|
      13 (+4, -13)  |      Nurse/H |  Caretaker/H<|
                    +--------------+--------------+
    ]

  This turned out to be a task cost thing elsewhere, it has led me to
  try removing the task cost test again.  It's still quite slow, taking
  7.6 minutes, and it has hit the 20,000 limit twice along the way.
  And although the solution has no undersized groups, it does have
  many groups that end on Saturday and start on Sunday.

9 August 2025.  More thinking needed about how to speed up interval
  grouping on instance INRC2-4-110-0-1428.

  Updated the soln set trie to index by the lengths of runs of
  equal-duration groups.  It seems to be working and gives a
  smaller trie structure.
  
  Found a logic problem in that the soln set trie was indexing
  by the primary durations of all the task groups, not just
  the non-self-finished ones.  Not sure how longstanding that
  problem was.  Anyway it's fixed now.  Running time is 7.5
  minutes, so no improvement there.

10 August 2025.  Inspired by yesterday's debug output, I have
  been thinking more deeply about the cost of solutions.  I
  need to separate task cost from group cost.  Today's job
  is to do a complete rewrite of the documentation concerning
  how cost is calculated.

11 August 2025.  Finished the new solution cost section of the
  doc, and updated everything before then to suit.  The next
  step is to redesign and document the dominance test.  I've
  finished that and it has come out pretty well, although the
  proof is tedious.  It needs a careful audit.

14 August 2025.  Not really a gap in the work, I've just been
  slogging along getting the new documentation right.  It's all
  done and audited and ready to implement.

15 August 2025.  Implementing the revised plan today.  Made a
  save_khe_sr_interval_grouping.c file first thing.  Also done
  some foundational changes, including revising the relevant
  record fields and implementing KheIgTaskGroupHandleAsAssigned.
  Now for the hard part.

16 August 2025.  Implementing the revised plan today.  I'm now
  adding the correct costs when groups are finished, and adding
  and removing the correct task costs while building solutions.
  All done and audited and ready to test.

  Sorted out KheResourceGroupSubset vs KheTaskGroupDomainDominates,
  including making link domains have type KHE_TASK_GROUP_DOMAIN
  rather than type KHE_RESOURCE_GROUP.  Also sorte out what
  "proper" means for task group domains.

  Previously I asked "Does our revised cost analysis assign the
  correct task costs to non-primary tasks?"  But now I can see
  no reason to ask this question.

  Previously I asked "Could we replace current randomness by
  generating solutions in a different order?  That might be
  faster and simpler."  I've found that I am already generating
  task classes in a random order.  That should do it, so I have
  removed the randomizer field from solutions.

17 August 2025.  Testing today.  Got the usual 1108 solution in
  0.7 seconds.  That is pretty darn fast.

  Back to testing instance INRC2-4-110-0-1428.  Previously
  it was taking 140 or 47 seconds.  Today the Nurse run
  took 216 seconds and produced a result with cost 0, i.e.
  perfect.  There was just one day when we exceeded the
  20000 solution limit, and only marginally:

    IgTimeGroupSolns(2Wed4, 46892 made, 20058 undominated, 20000 kept)

  So that's an improvement (on 8 August there were "numerous
  overruns"), but 216 seconds is still too slow.  I've looked
  through a list of 12 solutions for 1Wed that have the same
  trie index, and there don't seem to be any cases where we
  could reduce the number of solutions by proving dominance.
  Although the solutions are very similar and morally some
  of them could be omitted.

  Look at the first and second.  Here they are, with all tbe
  equal parts deleted:

    [ GroupedTasks(cost 0.00000, undersized_durn 15, oversized_durn 0)
                    +--------------+--------------+
      History       |      HN_13 3 |              |
      5 (+5, -0)    |              |              |
                    +              +--------------+
      1Mon          |      Night.0 |              |
      9 (+4, -0)    | HeadNurse/30 |              |
                    +              +--------------+
      1Tue          |  10: Night.0 |      Night.7<|
      13 (+4, -3)   |  HeadNurse/H |      Nurse/H<|
                    +--------------+              +
      1Wed          |   0: Night.1<|   2: Night.7<|
      11 (+1, -11)  |  HeadNurse/H<|      Nurse/H<|
                    +--------------+--------------+
    ]
    [ GroupedTasks(cost 0.00000, undersized_durn 15, oversized_durn 0)
                    +--------------+--------------+
      History       |      HN_13 3 |              |
      5 (+5, -0)    |              |              |
                    +              +--------------+
      1Mon          |      Night.0 |              |
      9 (+4, -0)    | HeadNurse/30 |              |
                    +              +--------------+
      1Tue          |  10: Night.0 |      Night.7<|
      13 (+4, -3)   |  HeadNurse/H |      Nurse/H<|
                    +--------------+              +
      1Wed          |   0: Night.9<|   2: Night.1<|
      11 (+1, -11)  |     Nurse/30<|  HeadNurse/H<|
                    +--------------+--------------+
    ]

  The first solution does not dominate the second, because there is
  a continuation that works for the second solution but not the first:

                    Nurse            Nurse
		    Nurse            Nurse
		    Nurse            Nurse
		    Caretaker

  This works for the second solution, as presented, but there is
  no way to make it work for the first solution.

  The second solution does not dominate the first, because there is
  a continuation that works for the first solution but not the second:

                    Nurse            Caretaker
		    Nurse            Caretaker
		    Nurse            Caretaker
		    HeadNurse

  This works for the first solution, as presented, but there
  is no way to make it work for the second solution.

  I've done some tests to see how spread out the costs are in
  INRC2-4-110-0-1428.  The answer is not very.  For example:

    KheIgSolverDoSolve starting to solve for 3Wed4
    [ KheTimeGroupDebugSolnCostHisto(3Wed4):
       0.00000 -  0.00004  10139
       0.00004 -  0.00008    0
       0.00008 -  0.00012    0
       0.00012 -  0.00016  4936
       0.00016 -  0.00020    0
       0.00020 -  0.00024    0
       0.00024 -  0.00028    0
       0.00028 -  0.00032  2352
       0.00032 -  0.00036    0
       0.00036 -  0.00040    0
    ]

  So an A* search would not save much time, because it would only
  miss out on the small number of solutions whose cost is non-zero.
  It might still be worth doing but it will not solve our problem.

  I've looked at better partitioning of the undominated solutions.
  Again there is not much joy there.  I'm already doing it by
  the durations of the unfinished groups, and apart from that
  there is cost (not much use, see above), domains (messy to
  do anything), assignments and fixed non-assignments (easy
  to do but so few groups have them it would not help)

18 August 2025.  Thinking about where to go from here.

19 August 2025.  Thinking about where to go from here.

  Added reference counting to ig task groups, let's see if
  that makes things faster.  It's running correctly, judging
  by the number of solutions I'm getting.  Running time is
  229 seconds.  That seems to be slower.  So I flew the
  old code back in.  Its running time is 226 secs, so there
  is no difference really.  Here is a snippet of what the
  old code is finding:

    IgTimeGroupSolns(4Thu4, 14180 made, 5786 undominated, 5786 kept)

  And here is what the new code is finding:

    IgTimeGroupSolns(4Thu4, 14180 made, 5786 undominated, 5786 kept)

  So there is no difference.  Even though there is no speedup
  we are using a lot less memory this way, so let's keep it.

20 August 2025.  I think the reason we got no improvement from
  reference counting was that KheIgTaskGroupCopy was called
  from only one place:  KheIgSolnCopy.

  Removing inheritance from task grouping today.  Files

    inherit_khe_solvers.h
    inherit_khe_sr_interval_grouping.c
    inherit_khe_sr_task_grouper.c

  contain the old code.  I've got rid of a lot of stupid things.

  Now excluding tasks with a fixed non-assignment from interval
  grouping and from task grouping in general.  They can never be
  part of a group that gets assigned a resource.

21 August 2025.  Designed a new interface for the task grouper,
  and have a clean compile using the new interface.  Revising
  the task grouper doc to describe the new interface.  All done
  and audited.

22 August 2025.  Audited what I did yesterday and reorganized
  file khe_sr_task_grouper.c so that it follows the order in
  khe_solvers.h, which itself follows the order in the doc,
  which is a pretty logical order.  All neat and tidy now.

  Now freeing each task grouper entry when the ig task group
  that created it is freed.

  Reorganized so that only genuine grouping functions take a
  KHE_TASK_GROUP_DOMAIN_FINDER parameter, and only the one
  domain finder is created, in yourself (exception:  there is
  one created in khe_sr_resource_matching.c, which is OK).
  All done including the documentation.

23 August 2025.  Making the mtask grouper compatible with
  how the task grouper is now.  Interface, implementation,
  and doc are all done, and the comb grouping application
  (the only one) is up to date.

24 August 2025.  Did an audit of the mtask grouper documentation,
  and a quick audit of the entire task grouping section of the
  documentation.  All good.

25 August 2025.  Testing new code today.  First bug was that
  sharing the domain finder was a problem for adding task
  domains.  Second was grouping without informing an active
  mtask finder.  Then it worked:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01565 ]

  including the usual very good solution for Nurse night
  shifts, found in 0.3 seconds.  Now for 1428.  It found a
  solution in 47 seconds, which is an improvement, but (a) it
  included one oversized group (1Sat to 2Thu).  How did that
  come about?  It is be avoidable, by making its last shift
  be the first of another group and pinching Night.14 from
  the end of another group to be the second, and so on.
  Plus it says that the cost is 0.0000, how can that be?
  And in Trainee, we have (b) a group of length 3 immediately
  preceding a group of length 2, they can be and should
  be united.  And (c) the cost is 1.00090, where is the hard
  cost violation?  So there are several problems here.

26 August 2025.  Looking into yesterday's problems with the
  results I'm getting on 1428.  (a) The oversized duration
  group is not a problem because it starts with a Late shift.
  (b) The first short duration sequence is not a problem
  because it ends with a Late shift.  The other short duration
  shifts look as though they are unavoidable.  So that
  just leaves (c) the 1.00090 cost.  The hard cost of 1 is
  coming from a Late task following a Night task.  This is
  in the Trainee table at 1Sat-1Sun.  It would be better to
  not group them, but we are avoiding undersized groups as
  first priority, which explains why it's there.  So all
  of yesterday's apparently nasty problems have evaporated.

  Revised the display code to accept a primary duration and
  compare that with the limits.  All done and documented.

28 August 2025.  Still thinking about how to speed up
  interval grouping.  I also need to factor in the need
  to give it a reliable running time.  So I've added an
  expand_limit which limits the next day to one solution
  per expansion when the previous day had over 1000
  solutions.  1428 ran in 4.7 seconds but had one
  undersized group.  Can we do better, somehow?

  Added a macro feature so that I can easily invoke
  pre-solves or not invoke them.

  Done a run with two solution groups, one +RPS (i.e.
  pre-solvers on) and one -RPS with them off.  It seems
  to be working.  I should be able to do a longer run
  soon.  I should run the whole archive with presolves
  on first.  With pre-solves:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01545 0.01585 0.01595 0.01595 0.01605 0.01620 0.01635 0.01640
      0.01645 0.01660 0.01730 0.01735
    ]

  Without pre-solves:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01515 0.01540 0.01605 0.01625 0.01655 0.01680 0.01685 0.01735
      0.01735 0.01740 0.01770 0.01820
    ]

  The two best are without pre-solves!  Grrr.  1515 is rel 1.21.
  However the pre-solve data set is definitely more robust.  Run
  it again.  With pre-solves:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01585 0.01595 0.01595 0.01605 0.01620 0.01630 0.01635 0.01640
      0.01640 0.01660 0.01710 0.01725
    ]

  Without pre-solves:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01515 0.01550 0.01600 0.01625 0.01640 0.01655 0.01680 0.01705
      0.01725 0.01740 0.01805 0.01820
    ]

  I also did a run of the whole archive (es_2025_08_28.xml).
  One soln per instance led to an average rel = 1.21 for with
  RPS and res = 1.19 for without RPS.  Not good news.  But I
  also need to look at average costs, not just minimums.

29 August 2025.  Gone back to grinding down INRC2-4-100-0-1108.
  Here is the current best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01545 0.01585 0.01595 0.01605 0.01605 0.01605 0.01620 0.01640
      0.01660 0.01670 0.01710 0.01735
    ] 10 distinct costs, best soln (cost 0.01545) has diversifier 8

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      60
    Avoid Unavailable Times Constraint (10 points)    	 70     100
    Cluster Busy Times Constraint (39 points) 	   	950    1220
    Limit Active Intervals Constraint (7 points) 	 75     165
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1545

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       44
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        2
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        6
    O  Available times (negative)      7       14
    Y  Unnecessary assignments        18       23
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  12 * 20 = 240 points of cluster busy time defects caused by
  available times (positive) and unnecessary assignments.  This
  is basically everything preventing us from catching up to LOR.
  And most of that is unnecessary assignments (10 * 20 = 200 points).

  Fixed a bug that was making the last KheSolnTryTaskUnAssignments
  do nothing:  it was creating a task finder, which needed an event
  timetable monitor, which had previously been deleted by DIY solver
  rem.  It has made virtually no difference, deleting exactly one task.

  There still seem to be two sequences of length 3 in the best
  result for 1108.  Is that inevitable, does it correspond with
  LOR's two unassigned Night shifts?  Probably yes.  Getting rid
  of max_beam does not remove them.  And they are there right to
  the end of the solve.  Also, rs_interval_grouping_complete=true
  does not help.

  The two short sequences cost 15 each (total 30).  Leaving two
  shifts unassigned costs 30 each (total 60).  But omitting two
  shifts will save 40 points in overloads, so it's a slightly
  better option overall.  Suppose we add the assign resource
  and limit active intervals constraint costs, we get this:

    LOR = 150 +  75 = 225
    KHE =  60 + 165 = 225

  So arguably we are even stevens on that.

  I've tried beefing up RRD so that redundant tasks have hard
  cost 0 during the first phase.  I got this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01540 0.01595 0.01595 0.01600 0.01610 0.01620 0.01685 0.01690
      0.01695 0.01720 0.01730 0.01750
    ] 11 distinct costs, best soln (cost 0.01540) has diversifier 10

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      90
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1160
    Limit Active Intervals Constraint (7 points) 	 75     210
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1540

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       23
    O  Available times (negative)     36       43
    Y  Unnecessary assignments         1        3
    X  Unassigned tasks                5        3
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        6
    O  Available times (negative)      7       12
    Y  Unnecessary assignments        18       21
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15


  It's not a bad result, 1540 is a slight improvement on 1545.  But
  I've taken away the changes that produced it, at least for now.

30 August 2025.  Trying two-three classes today.  Got this fairly
  amazing error:

    KheTaskGrouperEntryMakeTask failing (not proper root)
    task:
      1Sat:Day.0{1Sun:Early.0}
    task's proper root:
      2Tue:Early.0{2Mon:Early.0, 1Sat:Day.0{1Sun:Early.0}}

  Who has made this enormous group, and why?  And why was
  the mtask finder not told?  Interval grouping made it
  when grouping Early shifts, and the mtask finder was
  not told because it was made just once but re-used on
  different constraints.  I've fixed it now.

  Here's a queer bug:  task 1Thu:Day.31 appears in two groups:

                  +              +              +              +
    1Thu          |    2: Day.33 |       Day.31 |    1: Day.31 |
    3 (+0, -2)    |   Trainee/30 |    Trainee/H |    Trainee/H |
                  +--------------+              +--------------+

  It's causing mayhem (naturally) when the groups come to
  be built.

31 August 2025.  I'm now getting lots of examples, found by
  bug-huntng function KheIgTaskGroupCheck, where

    igtg->ig_task->task != KheTaskGrouperEntryTask(igtg->tg_entry)

  This more or less proves that igtg->tg_entry is being freed
  and reassigned to another job when igtg itself is not freed.
  It may involve dummy task groups.  This could get messy.

1 September 2025.  Finally I understand the 30 Aug bug.  It
  happens when I copy a task group from the working soln to
  a saved solution.  At that point I lose all ability to
  remove a tg entry when it's no longer referenced.  So now
  I have to ponder the way forward.

  * Should I swallow my pride and go back to the inheritance
    system?  It is at least reliable.

  * Or what about one tg entry for each ig task group?  It can
    be created when the task group is created and deleted when
    it is deleted.  But what about dummies?  Can we share for
    dummies?  How do we delete then?  Using the method that
    failed before?  And what happens when we copy a dummy?
    Could we use a NULL tg_entry for dummies?  Messy?

  * What about treating the task grouper as a strictly
    utilitarian thing - ask a question, get an answer.
    This amounts to passing the five fields (prev, task,
    interval, domain, assigned_resource) on every call,
    but also what type does prev have then?  I guess
    we don't pass prev.

       KheTaskGrouperSeparateAddTask(tg, task, prev_interval, prev_domain,
         prev_assigned_resource, &new_interval, &new_domain,
	 &new_assigned_resource)

    This might actually work.  Need a way to initialize for
    history, and also for nothing, but that can be done.

       KheTaskGrouperSeparateAddHistory(tg, r, durn, &new_interval,
         &new_domain, &new_assigned_resource)

  Added these new "separate" functions to task grouper.  All
  documented and implemented, ready to use.  Next step, use them
  in interval grouping.  Final step, make group entries private.

2 September 2025.  Have a clean compile of all code.  Still to
  update documentation, but I've tested first.  And it's working:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01585 ]

  Best of 12 with rs_interval_grouping_two_three=true:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01680 0.01695 0.01725 0.01725 0.01780 0.01780 0.01790 0.01815
      0.01825 0.01860 0.01890 0.01890
    ] 9 distinct costs, best soln (cost 0.01680) has diversifier 5

  Best of 12 with rs_interval_grouping_two_three=false:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01565 0.01600 0.01600 0.01600 0.01605 0.01620 0.01630
      0.01645 0.01665 0.01710 0.01750
    ] 10 distinct costs, best soln (cost 0.01500) has diversifier 6

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      90
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1160
    Limit Active Intervals Constraint (7 points) 	 75     150
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1500

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       42
    Y  Unnecessary assignments         1        4
    X  Unassigned tasks                5        3
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        6
    O  Available times (negative)      7       11
    Y  Unnecessary assignments        18       20
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  So rs_interval_grouping_two_three=false is better.  This is a
  pretty good solution, incidentally:  rel = 1.20.

  And now I have updated the documentation as well.  I've checked
  all the interfaces carefully to make sure that the documentation
  agrees with khe_solvers.h in every detail, except for a few points
  noted below.  I've also added an extra condition for interval
  grouping task admissibility, to the code and documentation:  the
  task must have a non-empty domain.

3 September 2025.  Did a run of 1108, found that Night shifts are
  being handled in 0.3 seconds, with one day having expand_limit 1.
  I could increase the expand_limit threshold a bit without harm.

  For 1428 Nurse night, lots of expand_limit stuff going on, but
  final cost was 0.00000 and found in 9.3 seconds, which is
  about what we want to spend (at the max).  But for Trainee
  the cost was 1.00090, which I had before and wrote this about:

    "The hard cost of 1 is
    coming from a Late task following a Night task.  This is
    in the Trainee table at 1Sat-1Sun.  It would be better to
    not group them, but we are avoiding undersized groups as
    first priority, which explains why it's there."

  Did a long run.  Here is the report from file res_2025_09_03.xml,
  showing best of 12 for KHE24 with and without pre-solves:

    Instances (20) 	LOR17 		KHE24x12 	KHE24x12-RPS
   			Cost 	Rel. 	Cost 	Rel. 	Cost 	Rel.
    ----------------------------------------------------------------
    INRC2-4-030-1-6291 	0.01695 1.00 	0.01935 1.14 	0.01945 1.15
    INRC2-4-030-1-6753 	0.01890 1.00 	0.02065 1.09 	0.02100 1.11
    INRC2-4-035-0-1718 	0.01425 1.00 	0.01620 1.14 	0.01680 1.18
    INRC2-4-035-2-8875 	0.01155 1.00 	0.01295 1.12 	0.01335 1.16
    INRC2-4-040-0-2061 	0.01685 1.00 	0.01990 1.18 	0.02000 1.19
    INRC2-4-040-2-6106 	0.01890 1.00 	0.02155 1.14 	0.02145 1.13
    INRC2-4-050-0-0487 	0.01505 1.00 	0.01675 1.11 	0.01830 1.22
    INRC2-4-050-0-7272 	0.01500 1.00 	0.01755 1.17 	0.01805 1.20
    INRC2-4-060-1-6115 	0.02505 1.00 	0.02825 1.13 	0.02870 1.15
    INRC2-4-060-1-9638 	0.02750 1.00 	0.03170 1.15 	0.03210 1.17
    INRC2-4-070-0-3651 	0.02435 1.00 	0.02765 1.14 	0.02790 1.15
    INRC2-4-070-0-4967 	0.02175 1.00 	0.02515 1.16 	0.02485 1.14
    INRC2-4-080-2-4333 	0.03340 1.00 	0.03805 1.14 	0.03785 1.13
    INRC2-4-080-2-6048 	0.03260 1.00 	0.03775 1.16 	0.03730 1.14
    INRC2-4-100-0-1108 	0.01245 1.00 	0.01500 1.20 	0.01600 1.29
    INRC2-4-100-2-0646 	0.01950 1.00 	0.02435 1.25 	0.02415 1.24
    INRC2-4-110-0-1428 	0.02440 1.00 	0.02670 1.09 	0.02675 1.10
    INRC2-4-110-0-1935 	0.02560 1.00 	0.03130 1.22 	0.02900 1.13
    INRC2-4-120-1-4626 	0.02170 1.00 	0.02475 1.14 	0.02365 1.09
    INRC2-4-120-1-5698 	0.02220 1.00 	0.02475 1.11 	0.02570 1.16
    ----------------------------------------------------------------
    Average 	        0.02089	1.00 	0.02401 1.15 	0.02411 1.16

  But I also need to see the average costs, since they might show a
  clearer improvement.

4 September 2025.  Working on HSEval today, getting it to print
  average costs as well as best costs.  I've decided that a big
  rewrite of EvalTableBuild is in order.  Looking at the current
  code, the thing to do is to replace EvalTableBuild by a separate
  module with a separate interface.  EvalTableBuild is called
  once from main.c and three times from summary.c:

     main.c - LaTex or HTML table to stdout for "hseval -t" option
     summary.c - HTML cost table, HTML running time table, LaTex cost table.

  I've made files eval_table.h and eval_table.c and done the
  boilerplate.  Now to implement the main function, EvalTableMakeTable.

5 September 2025.  In the middle of eval_table.c.  Reviewing yesterday
  and carrying on.  I've written everything except the concluding
  average and best rows.  I could test now.  Incredible - it worked
  first time.  EvalTableAddAveragesRow is next, I've made a start.

6 September 2025.  Pressing on with eval_table.c.  Tidied up the type
  and field names, they are better now.  The average and best count
  rows are the main things still to do.

7 September 2025.  Pressing on with eval_table.c.  Revised the
  type structure, got the new version working.

8 September 2025.  Pressing on with eval_table.c.  The code seems
  to be working (except show_bests_row is still to do), but the
  archives contain only one solution per instance, so I'm not
  getting any difference between average cost and best cost.

  Done a full run saving all 12 solutions.  Results are in
  file res_2025_09_08.xml ready to be evaluated.

9 September 2025.  Used HSEval to evaluate yesterday's big
  run's result (file res_2025_09_08.xml).  There is nothing
  in the error log, which is good, but the results themselves
  are very disappointing.  For best costs we have

                   LOR             KHE             KHE-RPS
      Average 	0.02089 1.00 	0.02403 1.15 	0.02406 1.16

  and for average costs we have

                   LOR             KHE             KHE-RPS
      Average 	0.02089 1.00 	0.02514 1.21 	0.02511 1.21

  There is basically nothing in it.  To three places the
  rels are 1.152 and 1.158, and 1.207 and 1.211.  So the
  relative costs are better but by very little, 0.006 and
  0.004.

  Trying to work out why pre-solving makes INRC2-4-110-0-1935 worse.
  Here is the summary with pre-solving (rel = 1.20):

    Summary 					Inf. 	Obj.
    --------------------------------------------------------
    Assign Resource Constraint (6 points) 	   	 180
    Avoid Unavailable Times Constraint (32 points)    	 390
    Cluster Busy Times Constraint (59 points) 	   	2240
    Limit Active Intervals Constraint (13 points)    	 270
    --------------------------------------------------------
      Grand total (110 points) 	   			3080 

  And here is the summary without pre-solving (rel = 1.13):

    Summary 					Inf. 	Obj.
    --------------------------------------------------------
    Assign Resource Constraint (6 points) 	   	 180
    Avoid Unavailable Times Constraint (32 points)    	 380
    Cluster Busy Times Constraint (54 points) 	   	2120
    Limit Active Intervals Constraint (9 points)    	 225
    --------------------------------------------------------
      Grand total (101 points) 	   			2905 

  There is a problem with total worklods and sequences, but why?
  The LOR solution has cost 2560:

    Summary 					Inf. 	Obj.
    --------------------------------------------------------
    Assign Resource Constraint (1 point) 	   	  30
    Avoid Unavailable Times Constraint (24 points)    	 250
    Cluster Busy Times Constraint (52 points) 	   	2250
    Limit Active Intervals Constraint (2 points)    	  30
    --------------------------------------------------------
      Grand total (79 points) 	   			2560 

  Perhaps the best way forward would be to just work on
  INRC2-4-110-0-1935 generally, starting with pre-solving,
  to see whether we can improve what we are getting.

  Working on show_bests_row.  This is quite different from
  average_is_min, because that depends on comparisons along
  the row, where as show_bests is about how many is_mins
  there are in the column.

10 September 2025.  Written no_of_bests code and tested
  it.  It seems to be working.

  I inceased the beam width from 1500 to 3000, running
  time increased from 0.9 seconds to 6.1 seconds, and
  the undersized duration decreased from 9 to 5.  So
  I increased it again, to 4000, no improvement.  So
  then I increased it to 10000, which got rid of all
  beam days, running time 8.3 seconds.

  The cost is 1.00075, the hard cost of 1 would be because
  a Late follows a Night (2Thu - 2Sun), which in turn is
  because we make avoiding undersized groups be the first
  priority.  The undersized groups are probably unavoidable.

  Maybe add Late shifts only on days where they would not
  incur a big cost if used?  Or maybe not include them at
  all, leaving it for later solvers to pick up the pieces?

    2Sat has 7 night shifts
    2Sun has 4 night shifts

  So what, in fact, can we do?  Just leave it to interval
  grouping to do its best?

  I tried with complete on, got some enormous numbers that
  are not helping at all.

  There still seems to be something wrong with the undersized
  1Sun-2Tue group.  There is a 1Sat task available to add to
  it with the same domain (a Late Caretaker).  So why not use it?
  It would not remove the undersize but it would reduce cost
  by making a complete weekend.

  Final cost 3045.  This is better than my previous best of
  12 with presolving, which was 3080.

  On 3Sun we could move a HeadNurse from the start of one group
  to the end of another and remove two incomplete weekends.  So
  why isn't that happening?  This seems to be clear evidence that
  I'm not getting an optimal result, even allowing for the fact
  that undersized groups have highest priority.

  I'm beginning to suspect that my interval cost function is
  not working correctly.  It needs some careful debugging.
  To start with we could print out the results the first
  time we calculate them (i.e. before any caching).

    [ KheTaskGrouperCost(tg)
      [ KheTaskGrouper
	2Tue:Night.1
	2Mon:Night.0
	1Sun:Night.0
      ]
      using in 1Sat-2Wed and resource HN_0
      [ G0 16434 comb grouping             
	[ A1 12698 Constraint:3/HN_0/20    
	[ A1 06194 Constraint:1/HN_0/1Sat3 
	[ A1 09164 Constraint:2/HN_0/1Sat4 
	[ A1 12699 Constraint:3/HN_0/24    
	[ A1 06195 Constraint:1/HN_0/1Sun3 
	[ A1 09165 Constraint:2/HN_0/1Sun4 
	[ A1 12700 Constraint:3/HN_0/28    
	[ A1 06196 Constraint:1/HN_0/2Mon3 
	[ A1 09166 Constraint:2/HN_0/2Mon4 
	[ A1 12701 Constraint:3/HN_0/32    
	[ A1 06197 Constraint:1/HN_0/2Tue3 
	[ A1 09167 Constraint:2/HN_0/2Tue4 
	[ A1 12702 Constraint:3/HN_0/36 
      ]
    ] KheTaskGrouperCost returning 0.00000

  This all looks wonderful, but guess what:  despite
  covering 1Sat and 1Sun, complete weekends are not being
  included in the cost here, because they are Constraint:4
  and Constraint:5.  But now that I look closely at the
  complete weekends constraints, they exclude 20% resources,
  which probably explains why they have been omitted.  The
  selected resource, HN_0, is not a 20% resource, but still.

  OK, fixed the problem by requiring the constraint to
  cover more than half the resources, not all the resources.
  Now doing a fresh run of 12 solves of INRC2-4-110-0-1935,
  with and without pre-solving.  Here they are with pre-solving:

    [ "INRC2-4-110-0-1935", 12 threads, 12 solves, 5.0 mins:
      0.03005 0.03045 0.03050 0.03070 0.03095 0.03115 0.03125 0.03160
      0.03180 0.03190 0.03200 0.03225
    ]

  The best has rel = 3005 / 2560 = 1.17.  This is better than the
  previous value for pre-solving, which was 3080 (rel = 1.20).
  Here are the results without pre-solving:

    [ "INRC2-4-110-0-1935", 12 threads, 12 solves, 5.0 mins:
      0.02905 0.02960 0.02965 0.02970 0.02975 0.03005 0.03020 0.03085
      0.03095 0.03095 0.03175 0.03190
    ]

  The best has rel = 2905 / 2560 = 1.13.  This is still better
  than with pre-solving.  Just keep going, I guess.

11 September 2025.  Working on instance INRC2-4-110-0-1935.
  The best of 12 KHE solutions has five short night sequences,
  but the LOR solution has only two:

     Short seq    KHE    LOR
     -----------------------
     1Sun-1Tue    Yes
     2Thu-2Sat    Yes    Yes
     3Mon-3Wed    Yes    Yes
     3Mon-3Wed    Yes
     3Wed-3Fri    Yes
     -----------------------

  So I need to look into this.  After all, the KHE solution
  is supposed to be optimal.  Also the LOR solution has only
  one unassigned shift, while the KHE solution has 5.  So
  there is quite a lot going wrong for KHE.

  Got the other_igs print working again (the code needed some
  renovation, it had fallen behind).  Now I need to compare
  the LOR groups with the KHE groups, to work out why LOR
  is doing better than the supposedly optimal KHE.

12 September 2025.  For some reason we are matching nurses
  (NU_29, NU_34, and NU_35) up with Caretaker tasks and making
  that the sole undominated solution.  That can't be right!
  Why is it happening?  It says "1 made", but more than that
  should have been made.

  All the right links seem to be there.  So is there something
  wrong with the logic in KheIgExpanderAssignUndersizedTaskGroups
  and KheIgExpanderDoAssignUndersizedTaskGroup?

  Written KheIgTaskGroupClassId, KheIgTaskClassId, and KheIgLinkId
  for use in a detailed debug print of the first expansion, using
  DEBUG57(ige).  Some precise indenting would be good.

13 September 2025.  I've got some serious debug output now.  It
  seems that the solver does assign Night.5 to NU_34 (this is
  what LOR does) but the path onwards from there is blocked
  by blocked links.   I don't really know why yet, I need to
  look into it.  But there is a lot of detail.  We do correctly
  assign all the undersized ones (the ones with history) and
  then for the remaining tasks we get this:

    [ KheIgExpanderAssignRemainingTasks(ige, 0)
      [ KheIgExpanderAssignRemainingTasks(ige, 1)
	[ KheIgExpanderAssignRemainingTasks(ige, 2)
	  [ DoAssignRemainingTasks(ige, 1Mon:Night.12 2, 0, wanted 4)
	    [ DoAssignRemainingTasks(ige, 1Mon:Night.12 2, 1, wanted 4)
	      [ DoAssignRemainingTasks(ige, 1Mon:Night.12 2, 2, wanted 4)
		[ DoAssignRemainingTasks(ige, 1Mon:Night.12 2, 3, wanted 4)
		  [ DoAssignRemainingTasks(ige, 1Mon:Night.12 2, 4, wanted 4)
		    KheIgLinkIsOpen(NULL->1Mon:Night.12) ret. false (blocked 2)
		  ] DoAssignRemainingTasks
		] DoAssignRemainingTasks
	      ] DoAssignRemainingTasks
	    ] DoAssignRemainingTasks
	  ] DoAssignRemainingTasks
	] KheIgExpanderAssignRemainingTasks
      ] KheIgExpanderAssignRemainingTasks
    ] KheIgExpanderAssignRemainingTasks

  So we are blocked by a link at some point, not sure  why.  Also
  there don't seem to be any calls to KheIgExpanderMakeOneAssignment
  in this trace, why not?  There are 6 tasks in the 1Mon:Night.12
  task class:

    [ TaskClass (expand_used_count 0)
      IgTask(1Mon:Night.12, Caretaker, non_asst_cost 1.00000, asst_cost+beta )
      IgTask(1Mon:Night.13, Caretaker, non_asst_cost 1.00000, asst_cost+beta )
      IgTask(1Mon:Night.14, Caretaker, non_asst_cost 1.00000, asst_cost+beta )
      IgTask(1Mon:Night.15, Caretaker, non_asst_cost 1.00000, asst_cost+beta )
      IgTask(1Mon:Night.16, Caretaker, non_asst_cost 0.00030, asst_cost+beta )
      IgTask(1Mon:Night.17, Caretaker, non_asst_cost 0.00030, asst_cost+beta )
    ]

  and the first two have been consumed by earlier calls, quite correctly,
  so we are left with 4 to assign.  This is exactly what LOR does, but
  somehow the way is blocked for KHE.  Why?

14 September 2025.  Carrying on trying to understand why KHE missed the
  solution that LOR found.  This is the blockage:

    KheIgLinkIsOpen(NULL->1Mon:Night.12) returning false
      (blocked by 2: NU_34->1Mon:Night.5 NU_35->1Mon:Night.5)

  Actually I think this is correct.  The problem is not that I'm
  mixing up Nurse and Caretaker.  So what is it?

  Made sure that groups with history elements have singleton
  domains.  If not, they are not truthful about what is going on.
  But guess what - they do have singleton domains.

  Having trouble finding a compatible 1Tue solution.  In fact
  the code says there are none:

    IgTimeGroupSolns(1Tue4, 17 made, 0 compatible, 14 undominated, 14 kept)

  and that seems to be true.  Why are there none?

15 September 2025.  I've marked the closest match in op2 with a
  +++++ marker in file op2_2025_09_15.  It would dominate the LOR
  solution if only its Night.20 (Caretaker) was swapped with Night.7
  (Nurse).  No it wouldn't.  A glimpse:  the new dominance test may
  not be quite right.  Maybe we don't need "non-empty intersection",
  maybe we really need "superset".  But why?  The bottom line is,
  given the two Mon solutions we have, there is no way for the Tue
  KHE soln to dominate the Tue LOR solution:  to do that, you have
  to move a Mon task as well as a Tue task.  So something is going wrong.

  It's not so much about dominance, but rather why we did not try two
  new Nurse groups on 1Tue.  It would work and it would be distinctive,
  so where is it?

  Changed USE_DOMAIN_DOMINATES from 1 to 0 and then I did indeed get
  one compatible solution on 1Tue.  So the fault may be there.  However
  the number of solutions I'm getting is now much larger, including
  two over the limit:

    IgTimeGroupSolns(2Mon4, 119027 made, 41450 undominated, 20000 kept)
    IgTimeGroupSolns(4Thu4, 39006 made, 20157 undominated, 20000 kept)

  There is also this:

    IgTimeGroupSolns(1Sat4, 8891 made, 7 compatible, 1016 undom, 1016 kept)
    IgTimeGroupSolns(1Sun4, 14241 made, 0 compatible, 4367 undom, 4367 kept)

  How do we go from 7 compatible to 0 compatible in one step?  Also
  the running time was 106 seconds, far too long.  So there is a lot
  to do here.  I've changed USE_DOMAIN_DOMINATES back to 1 for now.
  NB this is all on instance INRC2-4-110-0-1935.

16 September 2025.  What does this mean?

    KheIgLinkIsOpen(1Mon:Night.16->1Tue:Night.7) returning false
    (blocked by 2: 1Mon:Night.12->1Tue:Night.16 1Mon:Night.12->1Tue:Night.16)

  It seems to be saying that if you group Caretaker with Caretaker
  (twice over in this case), you can't group Caretaker with Nurse.
  This would not rule out the LOR solution (it does not group
  Caretaker with Nurse), but is it true?

    g1 1Mon:Night.16 (Caretaker)
    g2: 1Mon:Night.12 (NU_34)
    t1: 1Tue:Night.7 (Nurse)
    t2: 1Tue:Night.16 (Caretaker)

  Does (g1+t2, g2+t1) dominate (g1+t1, g2+t2)?  This comes to
  (Caretaker, NU_34) dominates (Caretaker * Nurse, NU_34),
  which is indeed true.  But there is no contradiction to LOR.

  The real problem is that LOR uses NULL->1Tue:Night.7 twice
  (1Tue:Night.7 and 1Tue:Night.9), but I don't seem to be
  generating solutions that do that.

  Can we write code that takes the LOR solution and the
  KHE groups and tries to build the LOR solution, giving
  an analysis if it fails to do it?

  In the KHE solution marked ++++, we could swap Tue:Night.8
  with Tue:Night.20 to produce a legal solution which would
  dominate the LOR solution.

  I've made various changes to the debug output, so that
  the task groups, tasks, links, and blocks are concisely
  presented.  It's good but I am no nearer to understanding
  what my problem is.

17 September 2025.  Am I taking durations into account when
  installing blocks?  Do they apply across different lengths?
  I'm generally pondering early dominance testing and whether
  it is as correct as it claims to be.

  It's looking like the right thing to do is to delete cases
  (3) and (4) from early dominance testing.  So let's try that.
  Strange, the code I've written seems to bear no relation
  to cases (1), (2), (3), and (4), although it does perform
  the superset test.

18 September 2025.  Did some work to reduce the number of
  places where I make a domain finder to one, or at least one
  per resource type.  Added KheTaskGroupDomainFinderDominanceClear
  to khe_sr_task_group_domain.c, also audited that file.  There
  may have been a bug or two in it.

  I've documented the many additions of a domain finder parameter
  to functions in khe_solvers.h.

19 September 2025.  Finished revising khe_sr_task_group_domain.c.
  Running 1935, taking 81 seconds with a couple of expand limits.
  But the result has undersized duration 5, whereas the LOR
  solution has undersized duration 10.  This seems to be a real
  improvement so I'm doing well as far as cost goes.

  Can we merge assignments into domains?  We are already
  reducing domains to singletons when there are assignments,
  is that enough to allow us to remove assignments?  No,
  because the task grouper needs assignments.

  What about the non_asst_cost part of the dominance test?
  I have not thought about it at all, so far.  So is there
  a whole whack of new analysis to do now?  Not really.

  Written KheIgExpanderMatchUndersizedTaskGroups and used it
  in the correct context (USE_MATCHING && expand_limit == 1).
  Audited and tested, and I got undersized_durn = 5 in 109
  seconds.  But I was expecting it to take longer.

  KheIgExpanderHandleSameResourceAsstCases - replaced a large
  whack of it with KheIgExpanderMakeOneAssignment.

  Here are some tests for different values of MAX_BEAM_DEFAULT:

    MAX_BEAM_DEFAULT   Running time (secs)   undersized_durn   
    --------------------------------------------------------
            10000            103                    5
             5000             56                    5
             3000             47                    4
             2000             18                    6
             1000              4                   11
    --------------------------------------------------------

  It's looking like MAX_BEAM_DEFAULT = 2000 is the best
  bet, although it would be good to come up with something
  totally different and better still.

  All these tests are on INRC2-4-110-0-1935.  The LOR solution
  has cost 2560.  The KHE solution with MAX_BEAM_DEFAULT = 2000:

    [ "INRC2-4-110-0-1935", 1 solution, in 5.0 mins: cost 0.02970 ]

  This is rel = 1.16, which is indeed an improvement on the 1.17
  that we had earlier.  Now best of 12:

    [ "INRC2-4-110-0-1935", 12 threads, 12 solves, 5.0 mins:
      0.02850 0.02890 0.02945 0.02970 0.02995 0.03005 0.03020 0.03030
      0.03055 0.03080 0.03120 0.03135
    ]

  which is rel = 1.11.  So we've improved on the solution we got
  without pre-solving, which is what we set out to do.  See also
  the most recent long run, on 3 September.

20 September 2025.  Tried 1108, got through Nurse in 4.2 secs with
  an undersized_durn of 2.  Which is probably good enough.  There
  were a few expand_limits along the way.  Final result was

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01725 ]

  which is very bad (rel = 1.38, why?).  And best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01560 0.01635 0.01655 0.01655 0.01680 0.01690 0.01695 0.01715
      0.01725 0.01725 0.01745 0.01995
    ]

  which is not very good (rel = 1.25), but typical enough of what
  we have been getting.

  Now for running times.  The Nurse:Trainee interval grouping runs
  all take 0.0 seconds.  Here are interval grouping running times
  for all Nurse calls:

    KheIgSolverSolveConstraintClass(INRC2-4-035-2-8875, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-035-0-1718, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-040-0-2061, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-050-0-7272, Nurse) in 0.1 secs
    KheIgSolverSolveConstraintClass(INRC2-4-040-2-6106, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-050-0-0487, Nurse) in 0.2 secs
    KheIgSolverSolveConstraintClass(INRC2-4-060-1-6115, Nurse) in 0.9 secs
    KheIgSolverSolveConstraintClass(INRC2-4-070-0-3651, Nurse) in 3.4 secs
    KheIgSolverSolveConstraintClass(INRC2-4-060-1-9638, Nurse) in 4.4 secs
    KheIgSolverSolveConstraintClass(INRC2-4-070-0-4967, Nurse) in 56.3 secs
    KheIgSolverSolveConstraintClass(INRC2-4-080-2-4333, Nurse) in 0.7 secs
    KheIgSolverSolveConstraintClass(INRC2-4-080-2-6048, Nurse) in 2.4 secs
    KheIgSolverSolveConstraintClass(INRC2-4-100-0-1108, Nurse) in 5.5 secs
    KheIgSolverSolveConstraintClass(INRC2-4-100-2-0646, Nurse) in 8.2 secs
    KheIgSolverSolveConstraintClass(INRC2-4-110-0-1935, Nurse) in 24.9 secs
    KheIgSolverSolveConstraintClass(INRC2-4-120-1-5698, Nurse) in 31.2 secs
    KheIgSolverSolveConstraintClass(INRC2-4-110-0-1428, Nurse) in 59.3 secs
    KheIgSolverSolveConstraintClass(INRC2-4-120-1-4626, Nurse) in 76.0 secs

  Most runs are fine but some are too slow.  So more work is needed to
  get the running time to be more robust.  For example, if we could
  limit it to 0.5 seconds per day, cumulative, that would be good.

21 September 2025.  Revised the new documentation about daily time
  limits to include two limits.  It's implemented and tested and
  it seems to be working.  Here's a fresh list of interval grouping
  running times:

    KheIgSolverSolveConstraintClass(INRC2-4-035-2-8875, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-040-0-2061, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-040-2-6106, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-035-0-1718, Nurse) in 0.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-050-0-7272, Nurse) in 0.1 secs
    KheIgSolverSolveConstraintClass(INRC2-4-060-1-6115, Nurse) in 0.5 secs
    KheIgSolverSolveConstraintClass(INRC2-4-050-0-0487, Nurse) in 0.5 secs
    KheIgSolverSolveConstraintClass(INRC2-4-060-1-9638, Nurse) in 8.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-070-0-3651, Nurse) in 12.9 secs
    KheIgSolverSolveConstraintClass(INRC2-4-070-0-4967, Nurse) in 14.0 secs
    KheIgSolverSolveConstraintClass(INRC2-4-080-2-4333, Nurse) in 1.4 secs
    KheIgSolverSolveConstraintClass(INRC2-4-080-2-6048, Nurse) in 6.1 secs
    KheIgSolverSolveConstraintClass(INRC2-4-100-0-1108, Nurse) in 10.2 secs
    KheIgSolverSolveConstraintClass(INRC2-4-110-0-1428, Nurse) in 13.6 secs
    KheIgSolverSolveConstraintClass(INRC2-4-100-2-0646, Nurse) in 13.5 secs
    KheIgSolverSolveConstraintClass(INRC2-4-120-1-4626, Nurse) in 13.5 secs
    KheIgSolverSolveConstraintClass(INRC2-4-120-1-5698, Nurse) in 13.5 secs
    KheIgSolverSolveConstraintClass(INRC2-4-110-0-1935, Nurse) in 13.9 secs

  This is much better balanced.  Now what about a long run, as I did
  on 3 September 2025?  Problem is, 1108 has gone bad:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01620 0.01625 0.01640 0.01655 0.01670 0.01700 0.01705 0.01710
      0.01720 0.01735 0.01755 0.01760
    ] 12 distinct costs, best soln (cost 0.01620) has diversifier 0

  We were getting 1500 on 3 September.  Presumably we are not
  getting a good interval grouping, but I need to look into that.
  Here is 1108 without pre-solving:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01600 0.01630 0.01635 0.01660 0.01665 0.01675 0.01690 0.01705
      0.01715 0.01770 0.01910 0.01915
    ] 12 distinct costs, best soln (cost 0.01600) has diversifier 10

  The best is better but on the whole we are still doing better
  with pre-solving than without it.

22 September 2025.  Yesterday I decided that I needed to take a close
  look at 1108.  It seems good, there are two undersized sequences,
  but LOR has unassigned tasks at those points I believe.  So not
  clear why the final cost is so poor:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01620 ]

  I think I just need to keep looking into 1108.  Compare with the
  LOR solution, for example.

23 September 2025.  Fixed the bug that was causing other to crash;
  it all seems to be working now except that other has some problems
  with unnecessary (?) undersized groups.

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01620 ]

24 September 2025.  Looking at one of the undersized groups in the
  LOR solution for INRC2-4-100-0-1108.  The problem seems to be due
  to the fact that the previous task is a grouped task.  It's a
  grouped task because it is in this soln, not in other_soln.

  Conclusion:  I really need to build groups and solns in other_soln.
  Perhaps I need to slog through it.  Build them from all assigned
  tasks, preferably:  other_mtasks, other_starting_tasks, etc.
  Or I could start off another solver on the other soln, and
  then "solve" it by being guided by the existing assignments.
  Slogging through it seems like the better course.

  I've added dummy task groups to the other soln, and this has
  reduced the number of undersized groups.  But there are still
  some, and so I need to look into those:

     3Sat:Late.0       HN_9
     3Sun:Night.0      HN_8
     4Mon:Night.1      HN_1

  So this is being screwed up by the fact that in the other
  solution we are building, 3Sat:Late.0 and 3Sun:Night.0
  are grouped.  For the record, here is what these resources
  are actually doing in the LOR solution on these days:

            HN_1                        HN_8                             HN_9
    -------------------------------------------------------------------------
    3Thu    Night.5 NA=h1|NWNurse=h1:1  Night.0 NA=h1|NWHeadNurse=h1:1  (free)
    3Fri    Night.5 NA=h1|NWNurse=h1:1  Night.0 NA=h1|NWHeadNurse=h1:1  (free)  
    3Sat    Night.3 NA=h1|NWNurse=h1:1  Night.4 NA=s30|NWNurse=h1:1      Late
    3Sun    Night.5 NA=h1|NWNurse=h1:1  Night.0 NA=h1|NWHeadNurse=h1:1   Late
    4Mon    Night.7 NA=h1|NWNurse=h1:1  Night.0 NA=h1|NWHeadNurse=h1:1   Late
    4Tue    (free)                      (free)                          (free)
    -------------------------------------------------------------------------

  The simplest way forward here might be to move weekend grouping to
  after interval grouping.  Yes, when we do that, undersized duration
  in the other solution is 9, caused by 3 ungrouped tasks.  And these
  are unassigned tasks in the LOR solution:  4Mon:Night, 4Mon:Night,
  and 4Sat:Night.  So we're OK really.  Let's call a halt to this
  silly episode.

  We've also got a better KHE solution by moving weekend grouping:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01615 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01605 0.01615 0.01635 0.01645 0.01645 0.01645 0.01670 0.01705
      0.01705 0.01710 0.01715 0.01815
    ] 9 distinct costs, best soln (cost 0.01605) has diversifier 2

  This is a bit better, rel = 1.28.  But on 3 September I was getting
  0.01500 (rel = 1.20).  So something went wrong, but what?  I just
  need to start grinding down again, I guess.

  Look at how interval grouping is performing on 1108, in detail.
  It's performing quite well but it does come up with two
  undersized groups on 3Wed-3Fri, whereas LOR has two unassigned
  tasks on 4Mon.  LOR's approach is probably better.  Need to
  look into this in detail.  But allowing ungrouped tasks will
  blow out the running time.

25 September 2025.  Comparing the LOR and KHE solutions around the
  3Fri - 4Mon area.  KHE is finding compatible solutions on all of
  those days, which is probably all we need to know about how the
  two solutions compare.  I guess the LOR solution is not competitive,
  and anyway KHE avoids undersized groups when it can.  The KHE
  running time is 11.6 seconds, which is fine.  It does not
  use matching at all.

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01615 ]

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      60
    Avoid Unavailable Times Constraint (10 points)    	 70      70
    Cluster Busy Times Constraint (39 points) 	   	950    1290
    Limit Active Intervals Constraint (7 points) 	 75     195
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1615

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       19
    O  Available times (negative)     36       44
    Y  Unnecessary assignments         1        7
    X  Unassigned tasks                5        2
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       13
    Y  Unnecessary assignments        18       21
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  The (7 - 1) + (21 - 18) = 9 extra assignments are costing KHE
  9 * 20 = 180 points.  Getting rid of those would take us down
  to 1615 - 180 = 1435, which is rel = 1.15.  Shaving 100 off
  the 195 limit active intervals cost should also be feasible.

  Tried running weekend grouping before interval grouping and
  excluding weekend tasks from interval grouping.  The idea
  being that if any weekend tasks are needed, weekend grouping
  will ensure that they are present.  Running time was somewhat
  faster (8.1 seconds, vs 11.8 seconds with weekend tasks included).
  Result was:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01620 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01620 0.01625 0.01640 0.01655 0.01670 0.01685 0.01690 0.01705
      0.01710 0.01720 0.01755 0.01760
    ]

  This is worse, really, than the 1605 I got before:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01605 0.01615 0.01635 0.01645 0.01645 0.01645 0.01670 0.01705
      0.01705 0.01710 0.01715 0.01835
    ]

  which is better most of the way through.

26 September 2025.  Now removing avoid unavailable times constraints
  from availability reports when they are redundant.  All implemented,
  tested, and documented.  And in the solving, "over" has increased
  from 10 to 20, and this has caused KheWeekendTaskTriggersFixing
  to return true.  And I have debug output showing that a large
  number of optional unassigned tasks on 1Sat are being fixed.
  First run produced solution

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01615 ]

  Promising.  Now here is best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01600 0.01620 0.01640 0.01685 0.01700 0.01705 0.01705 0.01720
      0.01735 0.01765 0.01775 0.01805
    ]

  There does seem to be some improvement here.  So that's nice.

27 September 2025.  LOR has two unassigned tasks on 4Mon:Night,
  KHE has two undersized groups on 3Wed-3Fri.  Assuming that these
  are alternative solutions to the same problem, which is better?

     LOR - two unassigned tasks     @ 30 = 60

     KHE - two undersized sequences @ 15 = 30
           two resource overloads   @ 20 = 40, total 70

  So LOR is saving 10 points in total by leaving two tasks
  unassigned.  It also gains freedom of action by having two
  less tasks to assign.  It's not a big deal.

  This is where we are at now with INRC2-4-100-0-1108:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01560 0.01580 0.01640 0.01685 0.01700 0.01720 0.01735 0.01740
      0.01750 0.01765 0.01775 0.01800
    ] 12 distinct costs, best soln (cost 0.01560) has diversifier 5

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     180
    Avoid Unavailable Times Constraint (10 points)    	 70      90
    Cluster Busy Times Constraint (39 points) 	   	950    1050
    Limit Active Intervals Constraint (7 points) 	 75     240
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1560

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       22
    O  Available times (negative)     36       39
    Y  Unnecessary assignments         1        3
    X  Unassigned tasks                5        6
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        6
    O  Available times (negative)      7       12
    Y  Unnecessary assignments        18       21
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  with rel = 1.25.

  This is a remarkable result, because for a minor increase in the
  number of unassigned tasks we have gained a major reduction in
  the cost of cluster busy times constraints.  If we could shave
  100 points off the cost of limit active intervals defects, that
  would be rel = 1.17.  And in fact we could shoot for saving
  150 points, which would be rel = 1.13, probably good enough.

  Started work on final improvement by swapping ig tasks, just
  before building the groups.

28 September 2025.  Working on swapping.  All done, working well,
  and a timer reported that the running time of swapping was 0.0
  seconds.  All the same it might pay to optimize.  Results are 

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01625 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01615 0.01615 0.01635 0.01650 0.01665 0.01665 0.01690 0.01725
      0.01765 0.01780 0.01795 0.01835
    ] 10 distinct costs, best soln (cost 0.01615) has diversifier 3

  Not great, we missed the 1560 solution we stumbled on before.
  Just in case there are threading problems, here is best of 4:

    [ "INRC2-4-100-0-1108", 4 threads, 4 solves, 5.0 mins:
      0.01615 0.01625 0.01665 0.01710
    ]

  Did some optimizing, all that is required.  Best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01615 0.01615 0.01655 0.01665 0.01665 0.01670 0.01675 0.01725
      0.01760 0.01765 0.01775 0.01790
    ] 10 distinct costs, best soln (cost 0.01615) has diversifier 4

  So we're all done with swapping now.  1615 is rel = 1.29.

  Started a long run, like 3 September.  Got a core dump:

    KheIgExpanderMakeOneAssignment internal error 3

  So there is work to do there.

29 September 2025.  Removed all trace of max_keep from the interval
  grouping documentation and implementation, and also removed a few
  leftover traces of max_beam.

  Sorting out the KheIgExpanderMakeOneAssignment core dump.  I now have
  it pinned:  instance INRC2-4-050-0-0487, diversifier 0.  And look:

    KheTaskGrouperSeparateAddTask returning false (task interval
    1Sun not disjoint from prev 1Fri-1Sun)

  Does this mean that something got linked in between the two
  calls?  How is that possible?  Need more debug output, focused
  on the tasks involved:

    [ OrdinaryTaskGroup(1Sat:Night.5, ? * Caretaker {1-3}, primary_durn 3,
	overhang, expand_used) ]
    IgTask(1Sun:Late.8, Caretaker, non_asst_cost 1.00000,
      asst_cost+beta 0.00000, expand_used:)

  The problem is that 1Sat:Night.5 is in fact a group,
  {1Sat:Night.5, 1Sun:Night.x}, and so it has an overhang
  and should not be being matched with a 1Sun task at all.
  So I need to look into why that's happening.  Yes, there
  seems to be remarkably little care taken about whether
  a task group class has an overhang or not.  But if it
  does, it should not link to anything, so there is a
  mystery there.

  OK, the bug is that two different task groups are being added
  to the same task group class, because KheIgTaskGroupSameClass
  is not checking overhangs.  We need to check that overhangs
  are equal and when there are overhangs we need to check that
  the times are equal; this will be before times have been
  converted into indexes.  Fixed and working again now, so
  I'm returning to 1108:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01615 0.01615 0.01625 0.01625 0.01650 0.01665 0.01690 0.01760
      0.01770 0.01775 0.01775 0.01835
    ] 9 distinct costs, best soln (cost 0.01615) has diversifier 4

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      60
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1280
    Limit Active Intervals Constraint (7 points) 	 75     195
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1615

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       24
    O  Available times (negative)     36       46
    Y  Unnecessary assignments         1        4
    X  Unassigned tasks                5        2
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       15
    Y  Unnecessary assignments        18       25
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  with rel = 1.29.  We could reduce the limit active intervals cost
  to 105 by getting rid of three consecutive free days defects that
  cost 30 each.  Do we have a good quality repair for this defect?

  Workload cost of trainees, KHE is behind by (15 - 7) * 20 = 160.
  If we could get rid of this we would be down to rel = 1.16.  We
  are being clobbered here by unnecessary assignments.  Can we do
  anything about them?  Why are they so bad?

30 September 2025.  Started looking over khe_sr_resource_matching.c
  with a view to handling optional tasks.  There is some weird
  lookahead stuff which tries all combinations of something, I
  could try all combinations of assignments of optional tasks.
  But still getting my head around the details.

1 October 2025.  Still thinking about khe_sr_resource_matching.c
  and handling optional tasks.  It could do it automatically (i.e.
  no new options or parameters required):  it could include all
  *assigned* optional tasks, and try edges with them assigned
  and not assigned, in all combinations.

  Simplifed KheTaskOffsetInDay in khe_sr_resource_matching.c 
  by calling KheFrameTimeOffset.

  Replaced task grouping in khe_sr_resource_matching.c by task
  sets.  It's all done with a clean compile and no apparent
  problems.  It needs an audit and test.

2 October 2025.  Sorted out freeing task sets.  It turns out
  that every task set goes into exactly one demand node, so
  when I free a demand node I am now freeing its task sets.

  Audit of khe_sr_resource_matching.c is complete.  I've started
  testing.  There was one silly bug where I was deleting task sets
  that should not have been deleted.  After fixing that it's working:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01565 0.01595 0.01595 0.01605 0.01650 0.01655 0.01680 0.01705
      0.01720 0.01720 0.01725 0.01765
    ] 10 distinct costs, best soln (cost 0.01565) has diversifier 4

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      90
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1170
    Limit Active Intervals Constraint (7 points) 	 75     225
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1565

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       22
    O  Available times (negative)     36       40
    Y  Unnecessary assignments         1        1
    X  Unassigned tasks                5        3
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       14
    Y  Unnecessary assignments        18       24
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  This seems to undermine my plan of reducing the number of
  unnecessary assignments, since the KHE number (1 + 24) is
  closer than before to the the LOR number (1 + 18).  The KHE
  Nurse result here is surprising:  previously I was typically
  getting 4 or more unnecessary assignments, now it's just 1,
  the same as LOR.

  Now if I could reduce the limit active intervals cost from 225
  to 105, say, that would be rel = (1565 - 120) / 1245 = 1.16.

  I did actually get a 1535 (rel = 1.23) solution at first, but
  1565 is good too.  Here is a 10-minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01535 0.01595 0.01595 0.01600 0.01640 0.01660 0.01680 0.01685
      0.01710 0.01720 0.01725 0.01775
    ]

  Best is 1535 again, the others are only slightly better.

  Removed the tgdf parameter from KheResourceMatchingSolverMake
  (it is no longer needed), and removed it from all functions
  which only have it because they call KheResourceMatchingSolverMake.
  Also removed it from the documentation as required; all done.

3 October 2025.  I've documented the new rules for which tasks to
  select and how to calculate edge costs.  They are ready to implement.
  It will be a bit tricky to record which version of an edge we want
  to implement.  Probably better to recalculate as needed.

  Replaced tasks with demand tasks and task sets with demand
  task sets.  All done, clean compile, ready to test.

4 October 2025.  Reviewed the new documentation.  Verified that
  all uses of type KHE_TASK_SET in khe_sr_resource_matching.c
  have been commented out.  Tested yesterday's code, it ran
  without crashing and produced

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01555 ]

  Now I have to make KheDemandTaskSetAssignResource take optional
  tasks into account during the edge cost calculation.  The code
  for that is written and audited.  Here is an interesting example
  of the debug output:

    [ KheDemandTaskSetAssignResource({2| 4Thu:Late.28, 4Fri:Late.33,
        4Tue:Day.38, 4Wed:Day.36}, TR_79) 2 required, 2 optional
    ] assigned 2 of 2 optional
    [ KheDemandTaskSetAssignResource({2| 4Thu:Late.28, 4Fri:Late.33,
        4Tue:Day.38, 4Wed:Day.36}, TR_80) 2 required, 2 optional
    ] assigned 0 of 2 optional
    [ KheDemandTaskSetAssignResource({2| 4Thu:Late.28, 4Fri:Late.33,
        4Tue:Day.38, 4Wed:Day.36}, TR_81) 2 required, 2 optional
    ] assigned 1 of 2 optional

  It shows that for this task set with two optional tasks in it,
  the best assignment to TR_79 assigned both, to TR_80 assigned
  neither, and to TR_81 assigned one.  So it seems to be working.

  Got a crash while testing (duplicate task sets).  I was sorting
  an array of demand tasks with a comparison function for tasks!
  That could be the problem.  Fixed now and testing.  First results:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01630 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01555 0.01590 0.01640 0.01665 0.01695 0.01740 0.01745 0.01755
      0.01800 0.01800 0.01850 0.01875
    ] 11 distinct costs, best soln (cost 0.01555) has diversifier 10

  Not a great score as things go now, but respectable.  Here is a
  10-minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01555 0.01590 0.01600 0.01635 0.01640 0.01645 0.01665 0.01700
      0.01740 0.01745 0.01820 0.01875
    ] 12 distinct costs, best soln (cost 0.01555) has diversifier 10

  The best and worst are the same, but things are better in between.
  Now back to the 5 minute run, sadly an inferior result, here is a
  detailed analsysis:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01555 0.01590 0.01660 0.01665 0.01680 0.01715 0.01730 0.01740
      0.01765 0.01800 0.01860 0.01875
    ] 12 distinct costs, best soln (cost 0.01555) has diversifier 10

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150      90
    Avoid Unavailable Times Constraint (10 points)    	 70      30
    Cluster Busy Times Constraint (39 points) 	   	950    1180
    Limit Active Intervals Constraint (7 points) 	 75     255
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1555

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       24
    O  Available times (negative)     36       43
    Y  Unnecessary assignments         1        2
    X  Unassigned tasks                5        3
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       10
    Y  Unnecessary assignments        18       18
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  with rel = 1.24.  This is much better on unnecessary assignments,
  suggesting that it is mission accomplished for today's code.  I've
  never got so close to LOR's number of unnecessary assignments as this.
  Now if I could just reduce the limit active intervals cost from 255
  to 105 (surely reasonable), that would be an improvement of 150 and
  I would have rel = (1555 - 150) / 1245 = 1.12.

5 October 2025.  Decided to to a big run while I was out gardening.
  It finished successfully and is now in res_2025_10_05.xml.

  Here are 3 September best of 12 results (* means KHE24x12 > KHE24x12-RPS):

    Instances (20) 	LOR17 		KHE24x12 	KHE24x12-RPS
   			Cost 	Rel. 	Cost 	Rel. 	Cost 	Rel.
    ----------------------------------------------------------------
    INRC2-4-030-1-6291 	0.01695 1.00 	0.01935 1.14 	0.01945 1.15
    INRC2-4-030-1-6753 	0.01890 1.00 	0.02065 1.09 	0.02100 1.11
    INRC2-4-035-0-1718 	0.01425 1.00 	0.01620 1.14 	0.01680 1.18
    INRC2-4-035-2-8875 	0.01155 1.00 	0.01295 1.12 	0.01335 1.16
    INRC2-4-040-0-2061 	0.01685 1.00 	0.01990 1.18 	0.02000 1.19
    INRC2-4-040-2-6106 	0.01890 1.00 	0.02155 1.14 	0.02145 1.13 *
    INRC2-4-050-0-0487 	0.01505 1.00 	0.01675 1.11 	0.01830 1.22
    INRC2-4-050-0-7272 	0.01500 1.00 	0.01755 1.17 	0.01805 1.20
    INRC2-4-060-1-6115 	0.02505 1.00 	0.02825 1.13 	0.02870 1.15
    INRC2-4-060-1-9638 	0.02750 1.00 	0.03170 1.15 	0.03210 1.17
    INRC2-4-070-0-3651 	0.02435 1.00 	0.02765 1.14 	0.02790 1.15
    INRC2-4-070-0-4967 	0.02175 1.00 	0.02515 1.16 	0.02485 1.14 *
    INRC2-4-080-2-4333 	0.03340 1.00 	0.03805 1.14 	0.03785 1.13 *
    INRC2-4-080-2-6048 	0.03260 1.00 	0.03775 1.16 	0.03730 1.14 *
    INRC2-4-100-0-1108 	0.01245 1.00 	0.01500 1.20 	0.01600 1.29
    INRC2-4-100-2-0646 	0.01950 1.00 	0.02435 1.25 	0.02415 1.24 *
    INRC2-4-110-0-1428 	0.02440 1.00 	0.02670 1.09 	0.02675 1.10
    INRC2-4-110-0-1935 	0.02560 1.00 	0.03130 1.22 	0.02900 1.13 *
    INRC2-4-120-1-4626 	0.02170 1.00 	0.02475 1.14 	0.02365 1.09 *
    INRC2-4-120-1-5698 	0.02220 1.00 	0.02475 1.11 	0.02570 1.16
    ----------------------------------------------------------------
    Average 	        0.02089	1.00 	0.02401 1.15 	0.02411 1.16

  Here are today's best of 12 results (* means KHE24x12 > KHE24x12-RPS):

    Instances (20) 	LOR17 		KHE24x12 	KHE24x12-RPS
   			Cost 	Rel. 	Cost 	Rel. 	Cost 	Rel.
    ----------------------------------------------------------------
    INRC2-4-030-1-6291 	0.01695	1.00 	0.01905 1.12 	0.01950 1.15
    INRC2-4-030-1-6753 	0.01890	1.00 	0.02070 1.10 	0.02040 1.08 *
    INRC2-4-035-0-1718 	0.01425	1.00 	0.01650 1.16 	0.01685 1.18
    INRC2-4-035-2-8875 	0.01155 1.00 	0.01310 1.13 	0.01335 1.16
    INRC2-4-040-0-2061 	0.01685 1.00 	0.01965 1.17 	0.02005 1.19
    INRC2-4-040-2-6106 	0.01890	1.00 	0.02170 1.15 	0.02125 1.12 *
    INRC2-4-050-0-0487 	0.01505	1.00 	0.01705 1.13 	0.01735 1.15
    INRC2-4-050-0-7272 	0.01500	1.00 	0.01775 1.18 	0.01760 1.17 *
    INRC2-4-060-1-6115 	0.02505	1.00 	0.02835 1.13 	0.02865 1.14
    INRC2-4-060-1-9638 	0.02750 1.00 	0.03115 1.13 	0.03160 1.15
    INRC2-4-070-0-3651 	0.02435 1.00 	0.02755 1.13 	0.02775 1.14
    INRC2-4-070-0-4967 	0.02175 1.00 	0.02510 1.15 	0.02495 1.15 *
    INRC2-4-080-2-4333 	0.03340 1.00 	0.03850 1.15 	0.03785 1.13 *
    INRC2-4-080-2-6048 	0.03260 1.00 	0.03775 1.16 	0.03710 1.14 *
    INRC2-4-100-0-1108 	0.01245 1.00 	0.01555 1.25 	0.01585 1.27
    INRC2-4-100-2-0646 	0.01950	1.00 	0.02600 1.33 	0.02385 1.22 *
    INRC2-4-110-0-1428 	0.02440	1.00 	0.02620 1.07 	0.02680 1.10
    INRC2-4-110-0-1935 	0.02560 1.00 	0.03020 1.18 	0.02960 1.16 *
    INRC2-4-120-1-4626 	0.02170 1.00 	0.02570 1.18 	0.02370 1.09 *
    INRC2-4-120-1-5698 	0.02220 1.00 	0.02590 1.17 	0.02555 1.15 *
    ----------------------------------------------------------------
    Average 		0.02089 1.00 	0.02417 1.16 	0.02398 1.15

  KHE24x12 has become slightly worse, from an average of 2401 to 2417.
  And it's now worse than KHE24x12-RPS.  So we have gone backwards.

  INRC2-4-100-0-1108 and INRC2-4-100-2-0646 stand out as easily the
  worst results, the only ones over 20% worse than LOR, either with
  or without RPS.  So we need to focus on those two instances.  I
  don't seem to have looked closely at INRC2-4-100-2-0646 yet.  I'll
  stick with INRC2-4-100-0-1108 for a while yet:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01590 0.01640 0.01660 0.01665 0.01730 0.01730 0.01735 0.01735
      0.01770 0.01785 0.01850 0.01875
    ] 10 distinct costs, best soln (cost 0.01590) has diversifier 3

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     150
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1180
    Limit Active Intervals Constraint (7 points) 	 75     180
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1590

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       41
    Y  Unnecessary assignments         1        4
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        8
    O  Available times (negative)      7       12
    Y  Unnecessary assignments        18       20
    X  Unassigned tasks                0        1
    ---------------------------------------------
    U - O + Y - X                     15       15

  and rel = 1.27.  Not a great result, but more typical of what we have
  been getting lately than yesterday's result was.  There is still a
  noticeable improvement in trainee unnecessary assignments.

  KHE is paying 1180 - 950 = 230 more than LOR for cluster defects,
  mostly overloads:  (41 - 36 + 12 - 7) * 20 = (5 + 5) * 20 = 200.
  Limit active intervals defects are the other problem.

  We've lost 60 points to complete weekends defects in TR_88 and
  TR_96.  LOR and other KHE solutions have avoided these defects,
  so what is going on?  An obvious repair is to add an optional
  Day task to TR_96 on 1Sun and take away the existing optional
  Day task on 1Thu.  This would be no change in workload, no
  change in sequence length, and would save 30 points in complete
  weekends defects.  So why wasn't it tried?  Even adding an
  optional task would save 30 points.  Tried a whynot run but
  this time the defect did not come up.  Or rather it did but
  it got fixed by the end - by the whynot run itself, in fact.
  I've verified that the final solution has it removed, and
  the final cost:

    [ "INRC2-4-100-0-1108", 1 solution, in 5.0 mins: cost 0.01560 ]

  suggests that this is the only change from the 1590 solution.
  Should we do something to make sure that simple operations
  are always tried?  Something like trying all unassignments,
  but using ejection chains of depth 1 at the end of the solve?

  There's another simple change too that would save 15 points:
  2Mon:Night TR_91 -> TR_99 (or same thing 1Wed would work too).
  Why not?  Perhaps we need a simple rec at the end of the run.
  I've made a slight change in khe_sr_combined.c to ensure that
  rec (ejection chains) comes last.  Best of 12:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 289.1 secs:
      0.01590 0.01610 0.01635 0.01665 0.01670 0.01685 0.01720 0.01735
      0.01790 0.01795 0.01830 0.01850
    ] 12 distinct costs, best soln (cost 0.01590) has diversifier 3

  I've checked, and the 1Sat incomplete weekend is still the culprit
  here.  Now trying again without rdv:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 250.9 secs:
      0.01590 0.01610 0.01665 0.01685 0.01695 0.01705 0.01790 0.01795
      0.01800 0.01800 0.01850 0.01885
    ] 11 distinct costs, best soln (cost 0.01590) has diversifier 3

  This seems to be worse, so I'm bringing rdv back.

6 October 2025.  Reorganized the ejection chain solver to receive
  a fresh schedule on every solve.  All documented and implemented,
  including free lists of major and minor schedul objects to recycle
  old schedules.  First run:

    [ "INRC2-4-100-0-1108", 1 solution, in 280.1 secs: cost 0.01620 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 295.1 secs:
      0.01590 0.01610 0.01665 0.01670 0.01700 0.01700 0.01705 0.01735
      0.01765 0.01795 0.01830 0.01860
    ] 11 distinct costs, best soln (cost 0.01590) has diversifier 3

  It seems to have terminated early.  But other runs did too.
  I still had the problem so I've changed the concluding schedule
  from "1+" to "2+", which I think is needed.  And look what came out:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01565 0.01610 0.01660 0.01665 0.01680 0.01685 0.01710 0.01775
      0.01785 0.01790 0.01805 0.01830
    ] 12 distinct costs, best soln (cost 0.01565) has diversifier 3

  But, curiously, the TR_96 incomplete weekend problem was not fixed!
  Time for a whynot, I deem.  Did some fairly meaningless fiddling
  (the * time weight, see just below) and then got this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01595 0.01610 0.01650 0.01665 0.01685 0.01700 0.01735
      0.01750 0.01795 0.01805 0.01815
    ] 12 distinct costs, best soln (cost 0.01500) has diversifier 3

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     120
    Avoid Unavailable Times Constraint (10 points)    	 70      90
    Cluster Busy Times Constraint (39 points) 	   	950    1080
    Limit Active Intervals Constraint (7 points) 	 75     210
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1500

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       41
    Y  Unnecessary assignments         1        4
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       10
    Y  Unnecessary assignments        18       18
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  This is rel = 1.20, which is very good, although the second best
  is a lot worse.  Still it's a good platform to build on.  The U+Y
  difference is ((21 + 4) - (20 + 1)) + ((7 + 18) - (4 + 18)) =
  (25 - 21) + (25 - 22) = 4 + 3 = 7, explaining cost 7 * 20 = 140.

  Extended the yourself synax to include time weight *, which means
  no time limit and not included in calculations.  This can be used
  for solves that are expected to take a negligible amount of time,
  although no-one is checking.

  How stable is this result?  Let's run it again:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01610 0.01645 0.01655 0.01660 0.01685 0.01730 0.01735
      0.01765 0.01785 0.01815 0.01845
    ] 12 distinct costs, best soln (cost 0.01500) has diversifier 3

  The best solution has the same cost and the same statistics as
  presented just above (I checked).  But the second best is even
  worse.  So the stability here is quite dubious.

  I think limit active intervals costs would be good to target
  next.  If we can reduce our current 210 by 100 to 110, which
  should be feasible, that would give a brilliant result:
  rel = (1500 - 100) / 1245 = 1.12.  And running it again:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01610 0.01645 0.01655 0.01660 0.01675 0.01685 0.01735
      0.01750 0.01785 0.01840 0.01845
    ] 12 distinct costs, best soln (cost 0.01500) has diversifier 3

  It would be nice to know what the probem is with the second-best
  solution.  But we mainly just need to keep grinding.  Here is a
  10-minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 9.8 mins:
      0.01500 0.01540 0.01600 0.01615 0.01630 0.01635 0.01645 0.01655
      0.01735 0.01775 0.01805 0.01815
    ] 12 distinct costs, best soln (cost 0.01500) has diversifier 3

  Best is no better, but second best is a lot better, and so on.

7 October 2025.  Not much time for work today.  I did audit the parts
  of the ejection chain chapter that have been affected by the changes
  just made.  They needed going over quietly.  They are fine now.

  Here is the current standard 5-minute best of 12 run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01610 0.01645 0.01655 0.01660 0.01685 0.01695 0.01735
      0.01755 0.01795 0.01805 0.01845
    ] 12 distinct costs, best soln (cost 0.01500) has diversifier 3

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     120
    Avoid Unavailable Times Constraint (10 points)    	 70      90
    Cluster Busy Times Constraint (39 points) 	   	950    1080
    Limit Active Intervals Constraint (7 points) 	 75     210
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1500

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       41
    Y  Unnecessary assignments         1        4
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       10
    Y  Unnecessary assignments        18       18
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  The clearest way forward would be to reduce the limit active
  intervals cost by 100, giving rel = (1500 - 100) / 1245 = 1.12.

8 October 2025.  Thinking about where to go next.  I would really
  like to see what happens when the two 3Wed-3Thu-3Fri night shift
  runs are replaced by two unassigned 4Mon night shifts (they
  are 4Mon:Night.1 and 4Mon:Night.23).  But how?  What about a
  one-off experiment?

  Trying an experiment where I intervene to omit 4Mon:Night.1 and
  4Mon:Night.23.  I've verified that this does cause the interval
  grouper to find a solution with undersized duration 0.  Results:

    [ "INRC2-4-100-0-1108", 1 solution, in 278.9 secs: cost 0.01595 ]

  and a full run shows that the limit active intervals cost is indeed
  better (reduced from 210 to 165), but the overall result is worse:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01585 0.01595 0.01610 0.01630 0.01635 0.01675 0.01680 0.01760
      0.01760 0.01780 0.01795 0.01880
    ] 11 distinct costs, best soln (cost 0.01585) has diversifier 3

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     180
    Avoid Unavailable Times Constraint (10 points)    	 70      90
    Cluster Busy Times Constraint (39 points) 	   	950    1150
    Limit Active Intervals Constraint (7 points) 	 75     165
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1500

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       24
    O  Available times (negative)     36       43
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        6
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        7
    O  Available times (negative)      7       10
    Y  Unnecessary assignments        18       18
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  I've also verified that the two omitted 4Mon:Night shifts remain
  unassigned in the final solution.

  Here is a 10-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 10.0 mins:
      0.01585 0.01595 0.01595 0.01610 0.01635 0.01650 0.01655 0.01675
      0.01680 0.01735 0.01745 0.01855
    ] 11 distinct costs, best soln (cost 0.01585) has diversifier 3

  It's arguably more consistent (three values under 1600) but not
  remarkable.

  Here is the ordinary 5 minute run, not omitting any tasks from
  interval grouping:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01610 0.01665 0.01675 0.01680 0.01700 0.01705 0.01735
      0.01755 0.01785 0.01805 0.01815
    ] 12 distinct costs, best soln (cost 0.01500) has diversifier 3

  Probably though the best here is a fluke.  Look at the second and
  third bests, they are quite poor compared with the 5 minute run
  omitting 4Mon:Night.1 and 4Mon:Night.23 from interval grouping.
  Here is a best of 6 run, just for fun:

    [ "INRC2-4-100-0-1108", 6 threads, 6 solves, 5.0 mins:
      0.01500 0.01640 0.01640 0.01645 0.01735 0.01815
    ] 5 distinct costs, best soln (cost 0.01500) has diversifier 3

  Nothing remarkable here.

  Starting a long run now.  But there is a problem, running times
  have blown out, in one case to 8.8 minutes.  So clearly the
  "negligible time" of the cleanup pass is not always negligible.
  I will have to bring it into the regular scheme of things and
  give it a small time limit.  It can soak up any leftover time.

    Instances (20) 	LOR17 		KHE24x12 	KHE24x12-RPS
   			Cost 	Rel. 	Cost 	Rel. 	Cost 	Rel.
    ----------------------------------------------------------------
    INRC2-4-030-1-6291 	0.01695 1.00 	0.01890 1.12 	0.01950 1.15 *
    INRC2-4-030-1-6753 	0.01890 1.00 	0.02065 1.09 	0.02040 1.08
    INRC2-4-035-0-1718 	0.01425 1.00 	0.01655 1.16 	0.01685 1.18 *
    INRC2-4-035-2-8875 	0.01155 1.00 	0.01325 1.15 	0.01315 1.14
    INRC2-4-040-0-2061 	0.01685 1.00 	0.01925 1.14 	0.01990 1.18 *
    INRC2-4-040-2-6106 	0.01890 1.00 	0.02145 1.13 	0.02135 1.13
    INRC2-4-050-0-0487 	0.01505 1.00 	0.01720 1.14 	0.01735 1.15 *
    INRC2-4-050-0-7272 	0.01500 1.00 	0.01755 1.17 	0.01760 1.17 *
    INRC2-4-060-1-6115 	0.02505 1.00 	0.02835 1.13 	0.02860 1.14 *
    INRC2-4-060-1-9638 	0.02750 1.00 	0.03155 1.15 	0.03185 1.16 *
    INRC2-4-070-0-3651 	0.02435 1.00 	0.02720 1.12 	0.02785 1.14 *
    INRC2-4-070-0-4967 	0.02175 1.00 	0.02590 1.19 	0.02495 1.15
    INRC2-4-080-2-4333 	0.03340 1.00 	0.03840 1.15 	0.03800 1.14
    INRC2-4-080-2-6048 	0.03260 1.00 	0.03675 1.13 	0.03680 1.13 *
    INRC2-4-100-0-1108 	0.01245 1.00 	0.01500 1.20 	0.01580 1.27 *
    INRC2-4-100-2-0646 	0.01950 1.00 	0.02515 1.29 	0.02300 1.18
    INRC2-4-110-0-1428 	0.02440 1.00 	0.02680 1.10 	0.02680 1.10
    INRC2-4-110-0-1935 	0.02560 1.00 	0.02975 1.16 	0.02900 1.13
    INRC2-4-120-1-4626 	0.02170 1.00 	0.02615 1.21 	0.02335 1.08
    INRC2-4-120-1-5698 	0.02220 1.00 	0.02555 1.15 	0.02490 1.12
    ----------------------------------------------------------------
    Average 		0.02089 1.00 	0.02406 1.15 	0.02385 1.15

  * means running with pre-solving is better.  So running without
  pre-solving is slightly better here.  The average run times are
  all at or under 300 seconds, despite the 8.8 minutes I saw for
  one solve.  But still I need to get that under control.  The
  result file here is res_2025_10_08.xml.

  I need to keep slogging away at INRC2-4-100-0-1108, and also
  at INRC2-4-100-2-0646 and INRC2-4-120-1-4626.  These instances
  have the worst results.

9 October 2025.  Tightened up the time limit so that 8.8 minutes
  is not possible.  Did a best of 12 run and got this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01500 0.01595 0.01640 0.01655 0.01675 0.01675 0.01695 0.01740
      0.01755 0.01785 0.01815 0.01840
    ] 11 distinct costs, best soln (cost 0.01500) has diversifier 3

  So nothing new there.

  What about extending backwards from the last weekend?  If we have
  say "Day Late" then we are going to need another Day, etc., and
  we could do some grouping there.  The fun facts show minimum
  limits of 2 for each type of shift, so if we have a "Day Late"
  then we need to group it with a Day before and a Late after,
  that is, if there is a before or after.

10 October 2025.  Worked on extending weekend grouping for Day Late
  cases.  Mostly done, needs and audit and finishing off.

11 October 2025.  Working on extending weekend grouping for Day Late
  cases.  All done and seems to be working well:

    weekend grouping {1Sun:Early.0, 1Sat:Day.0, 1Fri:Day.0, 2Mon:Early.0}
    ...
    weekend grouping {4Sat:Early.12, 4Sun:Day.11, 4Fri:Early.12}
    ...

  The second example shows that we are wise to not going off the
  start or end of the cycle.  First results:

    [ "INRC2-4-100-0-1108", 1 solution, in 262.5 secs: cost 0.01600 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01600 0.01610 0.01625 0.01640 0.01650 0.01665 0.01670 0.01700
      0.01705 0.01730 0.01730 0.01850
    ] 11 distinct costs, best soln (cost 0.01600) has diversifier 0

  This is inferior to what we had before - although the third best is
  better.  Here's a 10-minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 9.5 mins:
      0.01595 0.01600 0.01600 0.01605 0.01630 0.01645 0.01660 0.01670
      0.01685 0.01710 0.01720 0.01745
    ] 11 distinct costs, best soln (cost 0.01595) has diversifier 5

  Four solutions with cost under 1610 is actually pretty good.  It
  is just that wonderful 1500 solution that's missing - but it was
  always an outlier.  So let's go back to the 5-minute run and look
  in detail at what is going on.  I've changed the time limits slightly,
  since the above took 9.5 minutes rather than 10.

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01600 0.01610 0.01615 0.01615 0.01640 0.01660 0.01670 0.01675
      0.01700 0.01710 0.01730 0.01805
    ] 11 distinct costs, best soln (cost 0.01600) has diversifier 0

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     120
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1190
    Limit Active Intervals Constraint (7 points) 	 75     210
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1600

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       25
    O  Available times (negative)     36       46
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        8
    O  Available times (negative)      7       12
    Y  Unnecessary assignments        18       19
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  Incredibly, I have the same two Day Late defect that I had
  before:  TR_87 at 2Sat2, and TR_82 at 3Fri3.  No, the second
  one is not a Day Late; the first one is though.

25 October 2025.  Had two weeks off to go bushwalking.  Back at
  work today.

  KheDynamicResourceSequentialSolve is now selecting resources
  with higher workload limits.  Implemented and tested and
  working.  I've also written the code to bias the solver
  towards longer tasks when other things are equal, based
  on the durns_squared fields of expanders and solutions.
  This code needs an audit and test.

26 October 2025.  The revisions to dynamic are now running but
  they don't seem to be having much effect on the solutions,
  at least, there don't seem to be more night shifts being
  solved.  I should do some debugging to see what is going on.
  But then, look at these results:

    [ "INRC2-4-100-0-1108", 1 solution, in 290.1 secs: cost 0.01525 ]

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01515 0.01550 0.01555 0.01565 0.01565 0.01565 0.01615 0.01630
      0.01635 0.01645 0.01705 0.01715
    ] 10 distinct costs, best soln (cost 0.01515) has diversifier 7

  Although 1515 (rel = 1.21) is not my best ever result (that would be
  1500, as far as I can remember), I don't think I've ever had so many
  solutions under 1600 before.  Here's a 10 minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 9.3 mins:
      0.01515 0.01540 0.01550 0.01555 0.01565 0.01570 0.01615 0.01630
      0.01635 0.01665 0.01675 0.01715
    ] 12 distinct costs, best soln (cost 0.01515) has diversifier 7

  No improvement in the best solution but there are some differences
  further back in the pack.  Back to 5 minute runs, here we are
  with rs_drs_seq_frac=0.4:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01535 0.01545 0.01550 0.01565 0.01605 0.01625 0.01645 0.01655
      0.01655 0.01670 0.01670 0.01670
    ] 9 distinct costs, best soln (cost 0.01535) has diversifier 4

  And here we are with with rs_drs_seq_frac=0.6:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01535 0.01590 0.01600 0.01605 0.01610 0.01635 0.01635 0.01635
      0.01645 0.01680 0.01685 0.01810
    ] 10 distinct costs, best soln (cost 0.01535) has diversifier 11

  This is a lot worse.  So let's try rs_drs_seq_frac=0.3:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01525 0.01535 0.01540 0.01575 0.01585 0.01595 0.01635 0.01640
      0.01645 0.01650 0.01650 0.01650
    ] 10 distinct costs, best soln (cost 0.01525) has diversifier 9

  This is an excellent result, rel = 1.22 and nothing over 1650.
  Here is rs_drs_seq_frac=0.3 and a 10-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 9.2 mins:
      0.01525 0.01540 0.01555 0.01575 0.01585 0.01595 0.01595 0.01620
      0.01630 0.01640 0.01650 0.01650
    ] 10 distinct costs, best soln (cost 0.01525) has diversifier 9

  OK, let's stick with frac=0.5.

27 October 2025.  Back to the 5-minute frac=0.5 (rel = 1.21) solution:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01515 0.01555 0.01555 0.01565 0.01600 0.01615 0.01630 0.01630
      0.01640 0.01645 0.01715 0.01730
    ] 10 distinct costs, best soln (cost 0.01515) has diversifier 7

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     150
    Avoid Unavailable Times Constraint (10 points)    	 70      80
    Cluster Busy Times Constraint (39 points) 	   	950    1090
    Limit Active Intervals Constraint (7 points) 	 75     195
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1515

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       42
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       11
    Y  Unnecessary assignments        18       22
    X  Unassigned tasks                0        1
    ---------------------------------------------
    U - O + Y - X                     15       15

  So the improvement has come from cluster busy times constraints,
  basically, which means that we are doing a better job of using
  resources up to their limits.  Results on limit active intervals
  constraints could be better:  we are paying 120 for Constraint:17
  violations (OK considering that it includes 30 for the two 3Wed-3Fri
  short sequences), and 75 for violations of limit active intervals
  constraints other than Constraint:17.  If we could get rid of that
  75 we would be down to 1440, which is rel = 1.15.

  Working on the new ps_recombine option to parallel solver.  So
  far I have added the option and am passing it around properly.
  Now I need to make use of it.

28 October 2025.  To implement the new ps_recombine option, I
  have decided not to copy any solutions.  Instead I will extract
  the cost of constraints for each resource type at the end of
  each individual solve and store them.  Then after all solves
  are finished I will use these costs to guide the construction
  of the recombined solution.

  Implemented the ps_recombine option.  All done and tested,
  and I'm saving 40 points (1515 --> 1475) by recombining on
  the usual 5-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01515 0.01555 0.01555 0.01555 0.01565 0.01600 0.01630 0.01635
      0.01640 0.01645 0.01705 0.01725
    ] 10 distinct costs, best soln after recombining has cost 0.01475

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint (2 points) 	   	150     120
    Avoid Unavailable Times Constraint (10 points)    	 70      60
    Cluster Busy Times Constraint (39 points) 	   	950    1130
    Limit Active Intervals Constraint (7 points) 	 75     165
    ---------------------------------------------------------------
      Grand total (43 points) 	   		       1245    1475

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       21
    O  Available times (negative)     36       42
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       13
    Y  Unnecessary assignments        18       23
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  This 1475 solution is rel = 1.18.  Now we're getting somewhere.

29 October 2025.  Decided that I need a careful audit and
  reimplementation of khe_sm_parallel_solve.c, including
  checking after each solve ends whether the instance has
  ended, and doing everything requisite if it has.  It's
  all done with a clean compile and it looks great, but it
  needs a careful audit and test.

31 October 2025.  Colonoscopy yesterday.  Today I've audited
  khe_sm_parallel_solve.c and made quite a few changes.  So it
  needs another audit, then it should be ready to test.

  Testing now.  Have arena problems, there are two undeleted
  ones when I come to merge the arena sets.

1 November 2025.  Working on yesterday's very nasty memory bug.

  Does locking the solver really mean that nothing else is
  happening?  No, it doesn't - there could be a solve running
  in the thread that holds the solver.  This could lead to
  memory contention.  So this could be the cause of the bug.
  But I'm getting the bug even when there is just one thread.

  KheLimitActiveIntervalsMonitorCopyPhase1 was just calling
  HaMake.  In needed to call HaAlloc with the right length!
  Fixed now, and I have checked that the same problem does
  not afflict any other monitors.

2 November 2025.  Audited khe_sm_parallel_solve.c, all good.
  Now testing, I've found that PInstAllSolvesEnded is not
  being called.  This is because the last thread to finish
  finds that another thread has already ruled pinst out.
  Needs thinking about.

3 November 2025.  Revising khe_sm_parallel_solve.c to fix
  yesterday's bug and hopefully make its correctness more
  immediately clear.  It seems to be working:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 0.0 secs:
      0.01515 0.01555 0.01560 0.01565 0.01565 0.01600 0.01615 0.01630
      0.01640 0.01660 0.01690 0.01705
    ] best soln (after recombining) has cost 0.01475

  This is rel = 1.18.  And here is a 10-minute run, just for fun:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 0.0 secs:
      0.01515 0.01520 0.01540 0.01555 0.01565 0.01570 0.01615 0.01630
      0.01635 0.01665 0.01675 0.01715
    ] best soln (after recombining) has cost 0.01455

  And this is rel = 1.16.

  I'm now declaring ps_recombine and khe_sm_parallel_solve.c to
  be done.  This has taken 6 days altogether.  Back to grinding
  down the recombined 5-minute solution:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 0.0 secs:
      0.01520 0.01550 0.01555 0.01565 0.01565 0.01590 0.01615 0.01630
      0.01640 0.01645 0.01705 0.01715
    ] best soln (after recombining) has cost 0.01480

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint            	   	150     120
    Avoid Unavailable Times Constraint                	 70      60
    Cluster Busy Times Constraint             	   	950    1150
    Limit Active Intervals Constraint            	 75     150
    ---------------------------------------------------------------
      Grand total             	   		       1245    1480

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       22
    O  Available times (negative)     36       43
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        4
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       13
    Y  Unnecessary assignments        18       23
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  This is rel = 1.18.  Both resource groups are 6 worse for KHE,
  making extra points (6 + 6) * 20 = 240.  The reported difference
  is less than this (200) because LOR is worse on max busy weekends.

  Here's a run where interval grouping skips two tasks that lead
  to short sequences of consecutive night shifts:

    DEBUG74 skipping task 4Mon:Night.1
    DEBUG74 skipping task 4Mon:Night.23

  Let's see what that does to the final costs:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 0.0 secs:
      0.01500 0.01500 0.01500 0.01510 0.01570 0.01610 0.01620 0.01625
      0.01650 0.01680 0.01680 0.01695
    ] best soln (after recombining) has cost 0.01440

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint            	   	150     120
    Avoid Unavailable Times Constraint                	 70      70
    Cluster Busy Times Constraint             	   	950    1130
    Limit Active Intervals Constraint            	 75     120
    ---------------------------------------------------------------
      Grand total             	   		       1245    1440

  Yep, we've saved 40 points.  There is a saving of 15 + 15 = 30
  Limit Active Intervals points because the two short sequences
  are no longer there, plus 10 points from miscellaneous random
  differences.  This is rel = 1.15.  And here is a 10 minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 0.0 secs:
      0.01450 0.01475 0.01490 0.01520 0.01570 0.01605 0.01620 0.01630
      0.01635 0.01680 0.01680 0.01695
    ] best soln (diversifier 10) has cost 0.01450

  It's come out slightly worse, unluckily.  It ran for 8.8 minutes.

4 November 2025.  Thinking about how to avoid the two short sequences
  of Night shifts.  Can we make a few Night shifts optional, but not
  all?  Say, one from each group of equivalent tasks?

  The doc is a bit confusing between rs_interval_grouping_complete
  and rs_interval_grouping_daily_time_limit.  There seem to be
  several degrees of short-cutting:

    No short-cutting (rs_interval_grouping_complete)
    Avoiding undersized tasks (!rs_interval_grouping_complete)
    Bipartite matching of undersized tasks (after L1)
    Early termination of day's expansion (after L2)

  And now we're thinking of adding another:

    Bipartite matching of most undersized tasks

  So there is a design issue here that needs looking into.

    Selecting undersized tasks for must-assign status:

       Select no undersized tasks [ig_complete]
       Select most undersized tasks (all but one of each type) [new]
	 Assign in all possible ways (except non-assignment)
	 Assign in just one way using matching
       Select all undersized tasks [!ig_complete]
	 Assign in all possible ways (except non-assignment)
	 Assign in just one way using matching

    What to do with the selected tasks:

       Assign in all possible ways (except non-assignment)
       Assign in just one way using matching

    From most complete (and expensive) to least complete (and
    least expensive)

      typedef enum {
	KHE_IGU_NONE,
	KHE_IGU_MOST_ASSIGN,
	KHE_IGU_MOST_MATCH,
	KHE_IGU_ALL_ASSIGN,
	KHE_IGU_ALL_MATCH
      } KHE_IGU_TYPE;

    I've documented a time limit structure based on this, and I've
    started in on the implementation.

5 November 2025.  Working on structured time limits in interval
  grouping.  All audited and seems to be in good order, except
  I haven't implemented KHE_IGU_MOST_ASSIGN and KHE_IGU_MOST_MATCH
  yet.  I'll need to think about how to share the code in these
  cases with KHE_IGU_ALL_ASSIGN and KHE_IGU_ALL_MATCH.

6 November 2025.  Testing structured time limits in interval
  grouping.  It seems to be all working now, and I have also
  implemented KHE_IGU_MOST_ASSIGN and KHE_IGU_MOST_MATCH
  (very easy in the end).  Needs testing.

7 November 2025.  Found and fixed a bug when the time limit is
  KHE_NO_TIME.  Now doing a "none -" run, basically to see whether
  the two unassigned tasks will be included in the best soln or
  not.  It's slow, for example

     IgTimeGroupSolns(3Fri4, 69813202 made, 29590 undominated, 29590 kept)
     IgTimeGroupSolns(4Thu4, 89834656 made, 12983 undominated, 12983 kept)

  Yep, that's 69,813,202 and 89,834,656.

  OK, it finished (in 28.7 minutes), and it still has the two undersized
  sequences.  So what is going on with the single tasks?  Are they being
  costed correctly?  It needs to be carefully looked into.  We know
  that if they are deliberately left out we get no undersized groups.
  So the costing must be wrong, it seems.  And in fact I'm getting this:

    4Mon:Night.9 assigned cost 0.00045 + 0.00000 = 0.00045
    4Mon:Night.9 unassigned cost 0.00000 + 0.00030 = 0.00030
    OrdinaryTaskGroup(4Mon:Night.9 durn 1, Nurse {0-2}, primary_durn 1)

  The problem is that the "adjustment" (case 2 in the solution cost
  section of the interval grouping appendix) only applies at present
  when the task is a non-must-assign task.  We need it to apply to
  must-assign tasks for it to have an effect here.  But is that OK?

     if( group contains just one task [or is undersized?])
       add resource overload cost to assigned case.

  Might work.  Or better might be

     if( group contains just one task [or is undersized?])
       subtract resource overload cost from unassigned case.

8 November 2025.  Rewrote the "Solution cost" section of the "interval"
  appendix.  It's great.  It needs an audit, then it will be ready to
  implement and test.

9 November 2025.  Audited the rewritten "Solution cost" section of the
  "interval" appendix.  It's ready to implement.  Also rewrote the
  dominance testing section; there is a slight change to the test.
  Changed the "marginal_cost" field of the interval grouping solver to
  "demand_cost", and changed its value to min(resource_cost, task_cost),
  or 0 when demand does not exceed supply.

10 November 2025.  Implemented the 7-9 Nov plan for task costs.
  Testing now.  Incredibly, we got a solution with two unassigned
  4Mon:Night tasks on the first go.  And the final cost was

    [ "INRC2-4-100-0-1108", 1 solution, in 257.8 secs: cost 0.01590 ]

  Then using "0.3 most_match 0.5" got the same solution in 14 seconds,
  as expected.  Continuing with "0.3 most_match 0.5", did a 5-minute
  best of 12 run and got this:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01580 0.01595 0.01600 0.01600 0.01605 0.01640 0.01660 0.01665
      0.01670 0.01680 0.01710 0.01730
    ] best soln (after recombining) has cost 0.01520

  And another try:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01530 0.01595 0.01600 0.01600 0.01620 0.01640 0.01680 0.01685
      0.01690 0.01695 0.01710 0.01730
    ] best soln (after recombining) has cost 0.01525

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint            	   	150     150
    Avoid Unavailable Times Constraint                	 70      90
    Cluster Busy Times Constraint             	   	950    1120
    Limit Active Intervals Constraint            	 75     165
    ---------------------------------------------------------------
      Grand total             	   		       1245    1525

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       23
    O  Available times (negative)     36       43
    Y  Unnecessary assignments         1        5
    X  Unassigned tasks                5        5
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        5
    O  Available times (negative)      7       13
    Y  Unnecessary assignments        18       23
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  It's worse than 3 Nov (when I got 1440).  I guess I'll just have
  to grind it down.  I have verified that this solution now has two
  unassigned 4Mon shifts and no 3Wed-3Fri undersized sequences, so
  it is a better solution in that way.  But now there seem to be
  two other Constraint:17 violations:  NU_27 on 2Sat-3Mon, and
  TR_99 on 1Sat-2Mon.  So what's going on there?  If I could get
  rid of those two, the Constraint:17 cost would be 75, the same as
  the LOR solution has.  But I'm also paying dearly for a free days
  violation (30) and a busy days violation (30).  If I could get
  rid of all of those problems that would be 90 less, i.e. 1435.

  Just for fun, here is a 10-minute run:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 9.4 mins:
      0.01525 0.01595 0.01600 0.01600 0.01620 0.01640 0.01645 0.01650
      0.01675 0.01680 0.01705 0.01710
    ] best soln (after recombining) has cost 0.01520

  It's basically the same.  Returning to the 5-minute solution now:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01530 0.01600 0.01600 0.01605 0.01620 0.01640 0.01680 0.01680
      0.01685 0.01690 0.01695 0.01710
    ] best soln (after recombining) has cost 0.01525

  Now to start grinding down, here are the limit active intervals
  defects:

    Pt of application    Calculation                                 Cost
    ---------------------------------------------------------------------
    Constraint:17/HN_8   15 * Linear(4 - History 1 - 1Mon4 - 1Tue4)    15
    Constraint:17/NU_27  15 * Linear(4 - 2Sat4 - 2Sun4 - 3Mon4)        15
    Constraint:17/TR_88  15 * Linear(4 - History 1 - 1Mon4)            30
    Constraint:17/TR_98  15 * Linear(4 - History 2)                    30
    Constraint:17/TR_99  15 * Linear(4 - 1Sat4 - 1Sun4 - 2Mon4)        15
    Constraint:20/CT_61  30 * Linear(*3Wed+*3Thu+*3Fri+*3Sat+*3Sun-4)  30
    Constraint:23/HN_15  30 * Linear(3 - 3Mon - 3Tue)                  30
    ---------------------------------------------------------------------
								      165

  If we delete the three defects shared with LOR, we get this:

    Pt of application    Calculation                                 Cost
    ---------------------------------------------------------------------
    Constraint:17/NU_27  15 * Linear(4 - 2Sat4 - 2Sun4 - 3Mon4)        15
    Constraint:17/TR_99  15 * Linear(4 - 1Sat4 - 1Sun4 - 2Mon4)        15
    Constraint:20/CT_61  30 * Linear(*3Wed+*3Thu+*3Fri+*3Sat+*3Sun-4)  30
    Constraint:23/HN_15  30 * Linear(3 - 3Mon - 3Tue)                  30
    ---------------------------------------------------------------------
								       90

  So there are two Constraint:17 defects there that need looking into,
  plus the other two (which will be harder).  It's not easy to see how
  to look into this.  When I run a single solve, I get a solution with
  cost 1590 whose Constraint:17 defects have total cost 60, which is
  less than the Constraint:17 defects in the LOR solution (75).

  Did an off-site backup today, the first since 2025_03_22.  Yikes.

11 November 2025.  Wrote a function for checking whether sequences
  are assigned resources at the end of sequential/time sweep.  The
  answer is that somewhat less than half of the long sequences are
  assigned by the end of sequential, but all of them are assigned
  by the end of time sweep, which seems too good to be true; but
  the computer does not lie.

  The next question is whether frac affects final soln quality:

  frac = 0.0:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01580 0.01625 0.01630 0.01635 0.01640 0.01645 0.01685 0.01685
      0.01690 0.01695 0.01715 0.01770
    ] 0.01520 is cost of best soln (after recombining)

  frac = 0.1:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01520 0.01530 0.01535 0.01595 0.01610 0.01620 0.01625 0.01640
      0.01655 0.01720 0.01730 0.01775
    ] 0.01480 is cost of best soln (after recombining)

  frac = 0.2:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01590 0.01595 0.01600 0.01605 0.01610 0.01630 0.01650 0.01655
      0.01660 0.01685 0.01700 0.01755
    ] 0.01510 is cost of best soln (after recombining)

  frac = 0.3:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01490 0.01555 0.01555 0.01575 0.01575 0.01580 0.01585 0.01615
      0.01615 0.01630 0.01665 0.01670
    ] best soln (after recombining) has cost 0.01465

  frac = 0.4:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01535 0.01585 0.01585 0.01590 0.01605 0.01615 0.01630 0.01635
      0.01655 0.01670 0.01700 0.01715
    ] best soln (diversifier 10) has cost 0.01535

  frac = 0.5:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01530 0.01595 0.01600 0.01600 0.01620 0.01640 0.01665 0.01680
      0.01685 0.01690 0.01700 0.01705
    ] 0.01525 is cost of best soln (after recombining)

  frac = 0.6:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01590 0.01595 0.01600 0.01610 0.01680 0.01700 0.01705 0.01715
      0.01720 0.01730 0.01735 0.01780
    ] 0.01530 is cost of best soln (after recombining)

  frac = 0.7:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01595 0.01640 0.01650 0.01650 0.01655 0.01675 0.01675 0.01685
      0.01720 0.01785 0.01790 0.01820
    ] 0.01525 is cost of best soln (after recombining)

  frac = 0.8:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01575 0.01605 0.01610 0.01650 0.01665 0.01685 0.01690 0.01710
      0.01715 0.01725 0.01725 0.01790
    ] 0.01540 is cost of best soln (after recombining)

  frac = 0.9:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01590 0.01625 0.01635 0.01635 0.01655 0.01675 0.01680 0.01705
      0.01705 0.01710 0.01720 0.01780
    ] 0.01510 is cost of best soln (after recombining)

  frac = 1.0:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01540 0.01575 0.01595 0.01605 0.01675 0.01680 0.01680 0.01705
      0.01715 0.01720 0.01720 0.01740
    ] 0.01530 is cost of best soln (after recombining)

  There is no clear pattern here.  The best result is 1465,
  from frac = 0.3.  This also had the best uncombined solution
  (1490), the second best second-best uncombined solution
  (1555, second to frac = 0.1's 1530), and the best worst
  solution (1670).  But the 1465 value is just two cost 30
  defects away from 1525, which is a typical value.  So the
  variations are quite likely to be just noise.

  Anyway, for what it's worth here is a rerun with frac = 0.3:

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01490 0.01555 0.01565 0.01575 0.01580 0.01590 0.01595 0.01615
      0.01615 0.01615 0.01630 0.01670
    ] 0.01470 is cost of best soln (after recombining)

    Summary 						LOR     KHE
    ---------------------------------------------------------------
    Assign Resource Constraint            	   	150     180
    Avoid Unavailable Times Constraint                	 70      60
    Cluster Busy Times Constraint             	   	950    1110
    Limit Active Intervals Constraint            	 75     120
    ---------------------------------------------------------------
      Grand total             	   		       1245    1470

    Nurse                            LOR      KHE
    ---------------------------------------------
    U  Available times (positive)     20       25
    O  Available times (negative)     36       41
    Y  Unnecessary assignments         1        2
    X  Unassigned tasks                5        6
    ---------------------------------------------
    U - O + Y - X                    -20      -20

    Nurse:Trainee                    LOR      KHE
    ---------------------------------------------
    U  Available times (positive)      4        6
    O  Available times (negative)      7       13
    Y  Unnecessary assignments        18       22
    X  Unassigned tasks                0        0
    ---------------------------------------------
    U - O + Y - X                     15       15

  The usual (6 + 6) * 20 = 240 resource workload cost difference.  This
  solution also has Constraint:17 cost 75, like LOR, and it has the two
  4Mon night shifts unassigned.  The extra limit active intervals cost
  comes from two other limit active intervals constraints.  And rel = 1.18.

  If I could reduce the number of unnecessary assignments from 2 + 22
  to 1 + 18, that would be a reduction of 5 * 20 = 100 points, which
  would give a total cost of 1470 - 100 = 1370, and guess what:
  1370 is rel = 1.10.

  Alternatively, if I could reduce the number of available Nurse
  times from 25 to 20, that would be my 100 points right there.

12 November 2025.  Looking at available times (negative) in the
  Nurse:Trainee table.  If I could reduce this from 13 to 7 that
  would be 6 * 20 = 120 points less, which would be all I need.

13 November 2025.  tt/correspondents/krystallidis/GermanSchoolsForKHE.zip
  needs to be got working.  I've done one run and got this:

    KheAvailSolverSetResource internal error 1

  It refers to file src_platform/khe_avail.c, line 3386.  The
  problem seems to have crept in when I made the solution be
  the best of two runs, with and without unavailable times.
  I need to handle the cases where one or both of these
  runs produces nothing useful.

14 November 2025.  I seem to have fixed the problem with khe_avail.c.
  And I've also fixed quite a lot of problems with full copy.  I'm
  now up to the problem that krystallidis found:

    KheArchiveWrite: in instance ModularXHSST_test9_khe.xml,
      event LS_347400_1_0 with preassigned time TP_6 has meet
      with missing time assignment

  I've verified that this meet does have this preassignment.
  It was assigned, but a Kempe meet move during ejection chain
  repair unassigned it.  I think the solution is to not do
  moves of preassigned meets during Kempe meet moves.  So
  I've done that and documented it in the Kempe moves
  section of the time assignment chapter.

15 November 2025.  Looking into the khe_se_ejector.c, line 1433
  bug.  It seems that the repair fails but that the matching cost
  decreases by 1 anyway.  How can that happen, if the repair is
  undone?  No, I don't think it's the matching, I think it's
  something else.  How do I find out what that something is?

  A trace of the soln revealed this for ordinary demand monitors:

    1.00000 ->  5.00000 [ G1 51128 OrdinaryDemandGroupMonitor 
    3.00000 ->  1.00000 [ G1 51134 OrdinaryDemandGroupMonitor
   12.00000 -> 16.00000 [ G1 50050 OrdinaryDemandGroupMonitor
    3.00000 ->  1.00000 [ G1 51136 OrdinaryDemandGroupMonitor
    0.00000 ->  1.00000 [ G1 51132 OrdinaryDemandGroupMonitor
    8.00000 ->  4.00000 [ G1 50389 OrdinaryDemandGroupMonitor
    1.00000 ->  0.00000 [ G1 51130 OrdinaryDemandGroupMonitor
    3.00000 ->  1.00000 [ G1 50427 OrdinaryDemandGroupMonitor
    0.00000 ->  2.00000 [ G1 51194 OrdinaryDemandGroupMonitor
    0.00000 ->  1.00000 [ G1 51195 OrdinaryDemandGroupMonitor
    2.00000 ->  1.00000 [ G1 50278 OrdinaryDemandGroupMonitor
   ---------------------
   34.00000 -> 33.00000

  So this is the problem.  Could it be that the missing unassigned
  demand monitor is not in one of the groups being monitored?  If
  so, how did it miss out?  After all, we are tracing the soln here.

16 November 2025.  Curiouser and curiouser.  I've got this from the
  trace now:

    trace (active, matching 4569 -> 4571) [should say soln_defect_count]

  If this is supposed to return to the original state of the matching,
  well it hasn't.  No, this is KheSolnDefectCount, it is not
  KheSolnMatchingDefectCount.  We really need a way to find out
  what those two extra defects are, but how?  Can we somehow
  mark each defect by when it came into existence wrt marks?
  Or can we guess that they would be near the end?

    2.00000 -> 1.00000 [ G1 50428 OrdinaryDemandGroupMonitor
    0.00000 -> 1.00000 [ G1 50400 OrdinaryDemandGroupMonitor
    1.00000 -> 0.00000 [ G1 51515 OrdinaryDemandGroupMonitor
    1.00000 -> 2.00000 [ G1 50413 OrdinaryDemandGroupMonitor
    1.00000 -> 0.00000 [ G1 51492 OrdinaryDemandGroupMonitor
    ----------------------------------------------------------
    5.00000 -> 4.00000

18 November 2025.  Got this debug output today:

  trace (active, cost 1101.99999 -> 1100.99999, matching 543 -> 543)

  There is nothing wrong with the matching, the problem is that
  demand is shifting aroung and so we don't get back to the
  same exact state.  But what is failing is the soln cost,
  which strongly suggests that not all demand nodes are
  connected to the solution.  So I need to look into that.

  All the demand monitors seem to be attached, and the trace
  proves that the matching returns to its initial number of
  unmatched nodes, and yet we get this:

   2.00000 ->  1.00000 [ G1 50428 OrdinaryDemandGroupMonitor
   3.00000 ->  2.00000 [ G1 50389 OrdinaryDemandGroupMonitor
   6.00000 ->  7.00000 [ G1 51128 OrdinaryDemandGroupMonitor
   3.00000 ->  1.00000 [ G1 51134 OrdinaryDemandGroupMonitor
  12.00000 -> 16.00000 [ G1 50050 OrdinaryDemandGroupMonitor
   3.00000 ->  1.00000 [ G1 51136 OrdinaryDemandGroupMonitor
   0.00000 ->  1.00000 [ G1 51132 OrdinaryDemandGroupMonitor
   1.00000 ->  0.00000 [ G1 51130 OrdinaryDemandGroupMonitor
   3.00000 ->  1.00000 [ G1 50427 OrdinaryDemandGroupMonitor
   0.00000 ->  2.00000 [ G1 51194 OrdinaryDemandGroupMonitor
   0.00000 ->  1.00000 [ G1 51195 OrdinaryDemandGroupMonitor
   2.00000 ->  1.00000 [ G1 50278 OrdinaryDemandGroupMonitor
  --------------------
  35.00000 -> 34.00000

  When I print out the number of demand nodes with cost 1,
  I get 539 lines.  When I do it from within the trace, I
  get 540 lines.  And here is the diff:

  >     [ A1 40799 <ordinary_demand_monitor>     1.00000
          0x7214297c8780 LS_61500:1.20+0: {ST_S206} : {700} ]

  This is the very last defect according to the trace.  But
  in fact it appears twice in the trace listing, which occupies
  lines 120431 to 120972.  In file op2, these two places are
  lines 120539 and 120971.

19 November 2025.  Fiddled with unmatched_pos and ran again,
  this time the last element of the defect list had cost 0.00000.
  which is wrong, so clearly the problem is that monitors are
  being left on the list when they should be removed.

  OK, I'm zeroing in on the problem now.  I've managed to get this:

    KheMatchingMakeClean internal error 1

  which is saying that the condition

    m->unmatched_lower_bound <= HaArrayCount(m->unmatched_demand_nodes)

  is false when KheMatchingMakeClean is called.  It seems that my
  code is assuming this condition as a global invariant.  Why does
  it fail very occasionally like this?  I need to look at both
  variables and how they change and why it could go wrong.

  This failure follows KheMatchingMarkEnd resetting the lower bound.
  So it seems to be resetting the lower bound to a higher value than
  what we currently have.  How can that be?

  And now I have

    KheMeetMultiRepair ret false (match 545 -> 544)

  That is, the number of unmatched demand node changes from
  545 before KheMeetMultiRepair to 544 after, even though the
  operation fails, which should mean that nothing changes - or
  wait, don't we rely on the mark for that (KheMarkEnd,
  line 2196 of khe_se_ejector.c)?

  Now I have some debug information that suggests that the match
  cost dropped after a split match.  Could that be what is not
  being undone properly?

    after fuzzy match = 527
    after first split, match = 547
    after undoing first split, match = 526

  So undoing the first split does not get us back correctly.
  Here is the meet before the problematic split:

    [ Meet LS_65200(durn 2, children 0, tasks 23)
      domain: TG:NoId{TP_1..TP_9, TP_11..TP_19, TP_21..TP_29,
        TP_31..TP_39, TP_41..TP_49}
      -7-> LS_65200
      [ LS_65200.0$TP_38-TP_39: {TR_T164} := TR_T164 ]
      [ LS_65200.1$TP_38-TP_39: {ST_S9} := ST_S9 ]
      [ LS_65200.2$TP_38-TP_39: {ST_S52} := ST_S52 ]
      [ LS_65200.3$TP_38-TP_39: {ST_S53} := ST_S53 ]
      [ LS_65200.4$TP_38-TP_39: {ST_S54} := ST_S54 ]
      [ LS_65200.5$TP_38-TP_39: {ST_S57} := ST_S57 ]
      [ LS_65200.6$TP_38-TP_39: {ST_S59} := ST_S59 ]
      [ LS_65200.7$TP_38-TP_39: {ST_S56} := ST_S56 ]
      [ LS_65200.8$TP_38-TP_39: {ST_S55} := ST_S55 ]
      [ LS_65200.9$TP_38-TP_39: {ST_S58} := ST_S58 ]
      [ LS_65200.10$TP_38-TP_39: {ST_S60} := ST_S60 ]
      [ LS_65200.11$TP_38-TP_39: {ST_S63} := ST_S63 ]
      [ LS_65200.12$TP_38-TP_39: {ST_S61} := ST_S61 ]
      [ LS_65200.13$TP_38-TP_39: {ST_S62} := ST_S62 ]
      [ LS_65200.14$TP_38-TP_39: {ST_S64} := ST_S64 ]
      [ LS_65200.15$TP_38-TP_39: {ST_S67} := ST_S67 ]
      [ LS_65200.16$TP_38-TP_39: {ST_S66} := ST_S66 ]
      [ LS_65200.17$TP_38-TP_39: {ST_S68} := ST_S68 ]
      [ LS_65200.18$TP_38-TP_39: {ST_S86} := ST_S86 ]
      [ LS_65200.19$TP_38-TP_39: {ST_S90} := ST_S90 ]
      [ LS_65200.20$TP_38-TP_39: {ST_S99} := ST_S99 ]
      [ LS_65200.21$TP_38-TP_39: {ST_S95} := ST_S95 ]
      [ LS_65200.22$TP_38-TP_39: {RM_SH41} ]
    ]

  And here is the same meet after it has been split and
  merged again:

    [ Meet LS_65200(durn 2, children 0, tasks 23)
      domain: TG:NoId{TP_1..TP_9, TP_11..TP_19, TP_21..TP_29,
        TP_31..TP_39, TP_41..TP_49}
      -7-> LS_65200
      [ LS_65200.0$TP_38-TP_39: {TR_T164} := TR_T164 ]
      [ LS_65200.1$TP_38-TP_39: {ST_S9} := ST_S9 ]
      [ LS_65200.2$TP_38-TP_39: {ST_S52} := ST_S52 ]
      [ LS_65200.3$TP_38-TP_39: {ST_S53} := ST_S53 ]
      [ LS_65200.4$TP_38-TP_39: {ST_S54} := ST_S54 ]
      [ LS_65200.5$TP_38-TP_39: {ST_S57} := ST_S57 ]
      [ LS_65200.6$TP_38-TP_39: {ST_S59} := ST_S59 ]
      [ LS_65200.7$TP_38-TP_39: {ST_S56} := ST_S56 ]
      [ LS_65200.8$TP_38-TP_39: {ST_S55} := ST_S55 ]
      [ LS_65200.9$TP_38-TP_39: {ST_S58} := ST_S58 ]
      [ LS_65200.10$TP_38-TP_39: {ST_S60} := ST_S60 ]
      [ LS_65200.11$TP_38-TP_39: {ST_S63} := ST_S63 ]
      [ LS_65200.12$TP_38-TP_39: {ST_S61} := ST_S61 ]
      [ LS_65200.13$TP_38-TP_39: {ST_S62} := ST_S62 ]
      [ LS_65200.14$TP_38-TP_39: {ST_S64} := ST_S64 ]
      [ LS_65200.15$TP_38-TP_39: {ST_S67} := ST_S67 ]
      [ LS_65200.16$TP_38-TP_39: {ST_S66} := ST_S66 ]
      [ LS_65200.17$TP_38-TP_39: {ST_S68} := ST_S68 ]
      [ LS_65200.18$TP_38-TP_39: {ST_S86} := ST_S86 ]
      [ LS_65200.19$TP_38-TP_39: {ST_S90} := ST_S90 ]
      [ LS_65200.20$TP_38-TP_39: {ST_S99} := ST_S99 ]
      [ LS_65200.21$TP_38-TP_39: {ST_S95} := ST_S95 ]
      [ LS_65200.22$TP_38-TP_39: {RM_SH41} ]
    ]

  There are no child meets.  So ostensibly there is nothing wrong
  with the split and merge.  I did another run where it went right
  through.  The problem is intermittent, must be an uninitialized
  variable (or field) somewhere.

20 November 2025.  I've more or less decided to define a "matching
  snapshot" type which is basically a copy of the matching, and
  write code to take a snapshot during MarkBegin and another
  during MarkEnd, and compare them.  It's a desperate step but
  it should show whether and where the matching graph has changed.
  Made a start by defining the types.

21 November 2025.  KheSnapshotBuild and KheSnapshotCheckEqual
  are written.  I've also written the code to use them to build
  one snapshot before the mark and one after, and compare them.

22 November 2025.  Finally I am getting some meaningful debug:

    KheSnapshotDemandNodeCheckEqual failing at domain

  Finally got some solid debug:

    KheSnapshotDemandNodeCheckEqual(LS_323600.2, NULL) failing at
      domain ({53, 75, 78} != {0-309})

  Now LS_323600.2 is a Room slot, and there are indeed 310 rooms
  (I've just checked).  So the second domain is "any room", but
  the first domain is much more limited.  This suggests that
  something is going wrong with task bounds.

  Incredible.  Task splitting and merging does not set the task
  bound fields at all.  This could be it.  We need an invariant
  that says that task->domain is the intersection of the values
  of the task->task_bounds:

    * When the task is preassigned, the domain is set to a
      singleton without any task bound being added.  I've
      fixed this now, I'm adding a singleton task bound.

    * Ditto when the task is a cycle task, but we are leaving cycle
      tasks out of the invariant, because within KheTaskAddTaskBoundCheck
      it checks that task is not a cycle task.  Can cycle tasks split?

    * KheTaskDoSplit sets task2->domain but not the task bounds.
      Fixed now.

    * Calls on KheTaskDoSetDomain (static) set the domain, I
      need to check whether those do so safely.  Yes they do.

  So the problems with task bounds when splitting are fixed, and
  I've had no crashes yet.  Looking hopeful.  I started work on
  this bug on 15 November 2025, so it has consumed a week, although
  I did get a cold as well.

24 November 2025.  Worked on the Ortiz stuff yesterday, and today
  I finished it, ready for testing.  I've also written my reply to
  Ortiz, ready to send but not sent yet.

  Testing the krystallidis instances.  One of them runs out of
  memory, giving me a good opportunity for testing my out of
  memory code.  The main problem was getting KheSolnCopy to work
  on a soln that ran out of memory.  It seems to be OK now.

25 November 2025.  I've be reading through the documentation for
  structural and other time solvers, to see what the implications
  are for preassigned meets:

    * KheLayerTreeMake claims to add meet bounds to preassigned
      meets.  So what I've just done in KheMeetMake may be
      redundant.

    * There is no mention of preassigned meets in the description
      of how link events constraints are handled.  What happens
      when one or both of the events linked by a link events
      constraint is preassigned?

    * KheLayerTreeMake seems like a good place to assign a
      meet with a singleton domain to a cycle meet, but
      there is no mention of that.

    * At the end of section "Helper functions" in the time
      solvers chapter, it says "Even preassigned meets are
      unassigned, so some care is needed here".

    * KheEjectingMeetMove may unassign a meet.  Nothing is
      said in the documentation about what happens if that
      meet is preassigned.

    * KheNodeUnAssignTimes unassigns meets.

    * p36 has a discussion of preassigned meets.  It describes
      KheNodePreassignedAssignTimes, which seems to be key.  It
      is also where KheSolnTryMeetUnAssignments is documented, but
      that documentation says nothing about preassigned meets.
      This could be where the problem lies, practically speaking.

  So here is the plan:

  (1) Verify that everything before KheNodePreassignedAssignTimes
      (basically KheLayerTreeMake, but there could be other things)
      does nothing that would prevent a preassigned meet from
      being assigned by KheNodePreassignedAssignTimes.

  (2) Verify that KheNodePreassignedAssignTimes assigns every
      preassigned meet in the solution.  We could write some
      code to check this.

  (3) Make sure that all calls to KheMeetUnAssign other than
      those make by (1) do not unassign preassigned meets.

  With a bit of luck this will fix the problem.  If it
  does, document it and keep rolling.

26 November 2025.  The time solvers in order of calling them are

    KheLayerTreeMake
      The full details are tricky, but it does seem as though
      KheLayerTreeMake is taking careful note of preassigned
      times, including ensuring that any domain reduction is
      reflected in a domain reduction in ancestor nodes.  So
      I think we can assume that preassigned meets fully
      participate in layer trees and are reflected in meets
      with singleton domains at higher levels as required.
      But those meets with singleton domains may not themselves
      be preassigned meets.  To truly decide whether they are
      preassigned or not, we need to search the meets assigned
      to them to see whether they are assigned a preassigned meet.

        if( KheTimeGroupTimeCount(KheMeetDomain(meet)) == 1 &&
	    KheMeetIsPreassignedDirectOrIndirect(meet) )
	{
	  /* meet needs to be treated as preassigned */
	}

    KheCoordinateLayers
      This rearranges a layer tree, but ultimately it calls
      KheNodeRegular which calls KheMeetRegular, so it seems
      that every preassigned meet in a child node will have
      a corresponding preassigned meet in the parent node.

    KheBuildRunarounds
      This just merges nodes.  It should have no effect on
      whether preassigned meets can be assigned.

    KheNodeRecursiveAssignTimes(KheRunaroundNodeAssignTimes)
      Nothing special.  It could fail to make some assignment.

    KheNodePreassignedAssignTimes (in khe_st_basic.c)
      This basically does it all.  Actually it assigns all meets
      whose domains are singletons, which is not quite the same
      thing but does include all meets that are either preassigned
      or are assigned a preassigned meet.  But what happens if
      assigning the meet its preassigned time fails?  Can that
      happen?  It boils down to KheMeetMoveCheck:

      (1) asst is fixed - should not happen, but we can
          unfix it if it does.  Done.

      (2) meet is a cycle meet - cannot happen (meet in non-root node)

      (3) move changes nothing - cannot happen (meet is initially unassigned)

      (4) offset out of range - cannot happen (comes from a time)

      (5) domain problems - cannot happen (time comes from domain)

      (6) node rule - again cannot happen (meet in child node of root)

      So we can abort if the assignment doesn't happen.  Done.

    and then if times are not all preassigned, we need to check that
    these functions do not unassign the meets assigned by
    KheNodePreassignedAssignTimes:

      KheNodeLayeredAssignTimes
        Complicated.  At present I'm just hoping that this code
	(which implements layer matching) does not unassign any
	preassigned meets.

      KheEjectionChainNodeRepairTimes
        All good in khe_se_solvers.c.
	All good in khe_st_kempe.c.

      KheNodeFlatten
        Should be fine

      KheNodeDeleteZones
        Should be fine

  And there is also

    KheSolnTryMeetUnAssignments
      Should be fine

  Now trying all occurrences of KheMeetUnAssign (again).

    KheMeetFirstMovable
      Does not consider whether meet is preassigned, nor should it.

27 November 2025.  KheMeetIsAssignedPreassigned is now implemented
  and documented.

28 November 2025.  I've now checked all the places where KheMeetUnAssign
  is called, to see if the call is safe wrt preassigned meets.  Here
  are a few nodes that I made along the way:

    KheTreeSearchLayerRepairTimes called by KheTreeSearchRepairTimes,
    which is unused.  Perhaps I should withdraw this code.

    KheLayerAsstUndo undoes previous assignments, should be OK.
    KheNodeMeetUnAssign unassigns the meets of node.  Unused.

    KheLayerUnAssignTimes calls KheMeetUnAssign, but is unused.
    KheNodeUnAssignTimes calls KheMeetUnAssign, so is that a problem?
    KheNodeUnAssignTimes is called mainly by khe_ss_runarounds.c,
    which comes before preassigned assignment.  The only other call
    is from KheNodeSimpleAssignTimes, which itself is part of building
    runarounds:

      KheBuildRunarounds(cycle_node, &KheNodeSimpleAssignTimes, ...)

    as called by khe_sm_yourself.c.

  I've also replaced all solver calls to KheMeetIsPreassigned by
  calls to KheMeetIsAssignedPreassigned.

  Did some testing, but got this:

    KheArchiveWrite: in instance ModularXHSST_test2_Ungerade_Halbjahr1_khe.xml,
      event LS_100_2_0 with preassigned time TP_30 has meet with
      missing time assignment

  Oh dear, back to the drawing board.  I will have to do some
  analysis this time.  Could it be a memory overflow?  I can't
  guarantee the solution state if memory runs out.

  Now routinely getting debug information when memory runs out.
  And deleting solutions for which memory runs out, rather than
  trying to save them.

  Doing some more testing, but dropped back to 6 threads, to
  double the amount of memory per thread and hopefully avoid
  running out of memory.

29 November 2025.  Adjusted the memory allocator (hn_all.c) to
  return any sufficiently large chunk.  Audit and tested, and
  it seems to be working.

  When I ran 6 threads, 12 of the 18 instances ran out of memory:

    ModularXHSST_test1_Ungerade_Halbjahr1_khe.xml
    ModularXHSST_test2_Ungerade_Halbjahr1_khe.xml
    ModularXHSST_test6_Halbjahr1_khe.xml
    ModularXHSST_test9_khe.xml
    ModularXHSST_test10_khe.xml
    ModularXHSST_test11_khe.xml
    ModularXHSST_test12_Ungerade_khe.xml            (just one of six)
    ModularXHSST_test13_khe.xm                      (just one of six)
    ModularXHSST_test14_Ungerade_Halbjahr1_khe.xml
    ModularXHSST_test15_Ungerade_Halbjahr1_khe.xml
    ModularXHSST_test16_Ungerade_Halbjahr1_khe.xml  (just one of six)
    ModularXHSST_test17_Ungerade_Halbjahr1_khe.xml

  Or 9 out of 18 if we discount the three cases where one of
  the six solves ran out of memory and the other five didn't.

  Got some debug output going and it produced this:

    after KheNodeLayeredAssignTimes: found unassigned preassigned
      meet LS_185000_2_0

  And there was another check before KheNodeLayeredAssignTimes
  which produced nothing.  So KheNodeLayeredAssignTimes is the
  culprit.  It's going to take some serious work to find out
  what the problem is here and get rid of it.  I could start
  by getting a debug print of the final layer tree.  A debug
  of KheNodeLayeredAssignTimes shows

    KheNodeLayeredAssignTimes returning false

  which means that not all meets were assigned.  Why not?
  There is just one layer where not everything was assigned:

    [ Layer: 45 nodes, durn 80 meet 51 adur 76 [OVERSIZE]
      (CL_AG_PseudoStudent_0, TR_T105, TR_T114, TR_T115, ... , TR_T92)
      parent:
        Node_0_LS_80200_3_0
      children:
        Node_1_LS_187700              2   CL_AG_PseudoStudent_0
        Node_2_LS_184200              1   CL_AG_PseudoStudent_0
        Node_3_LS_186100	      2   CL_AG_PseudoStudent_0
        Node_4_LS_186600              2   CL_AG_PseudoStudent_0
        Node_5_LS_188800              1   CL_AG_PseudoStudent_0
        Node_7_LS_184100              1   CL_AG_PseudoStudent_0
        Node_8_LS_188100              1   CL_AG_PseudoStudent_0
        Node_9_LS_188500              1   CL_AG_PseudoStudent_0
        Node_11_LS_186000             1   CL_AG_PseudoStudent_0
        Node_12_LS_184000             2   CL_AG_PseudoStudent_0
        Node_13_LS_189000             2   CL_AG_PseudoStudent_0
        Node_14_LS_188700             1   CL_AG_PseudoStudent_0
        Node_15_LS_188000             1   CL_AG_PseudoStudent_0
        Node_16_LS_186500             2   CL_AG_PseudoStudent_0
        Node_17_LS_187900             2   CL_AG_PseudoStudent_0
        Node_18_LS_185900             2   CL_AG_PseudoStudent_0
        Node_19_LS_184400             1   CL_AG_PseudoStudent_0
        Node_60_LS_187400             2
        Node_65_LS_197500             2
        Node_66_LS_196900             2
        Node_67_LS_197000             1
        Node_68_LS_197400             2
        Node_69_LS_197200             2
        Node_70_LS_189600             2
        Node_71_LS_197100             2
        Node_228_LS_188200_2_0        2
        Node_229_LS_187800_2_0        2
        Node_230_LS_185200_1_0        1
        Node_231_LS_186900_2_0        2 
        Node_232_LS_187300_2_0        2
        Node_233_LS_184700_1_0        1
        Node_234_LS_184800_1_0        1
        Node_260_LS_185500_2_0        2
        Node_285_LS_185700_2_0        2
        Node_286_LS_186700_2_0        2
        Node_287_LS_184500_1_0        1
        Node_465_LS_191300_1_0        1
        Node_466_LS_185000_2_0        2
        Node_467_LS_188400_1_0        1
        Node_468_LS_185300_2_0        2
        Node_477_LS_187200_2_0        2
        Node_480_LS_185600_2_0        2
        Node_284_LS_197300_2_0        2
        Node_503_LS_195900_2_0        2
        Node_199_LS_196000_2_0        2
	                             --
				     29 * 2 + 16 * 1 = 74
    ]

  and this does include LS_185000_2_0, evidently.  So I need to
  look into what this node is about and what I can do about it.

  I agree with the 45 nodes, and they all contain CL_AG_PseudoStudent_0,
  but I make the duration 74, not 80 or 76.  But there may be other
  meets in these nodes besides the named ones, indeed it seems to be
  saying that there are 51 meets, which is 6 more than I have here.

30 November 2025.  Instance ModularXHSST_test18_Ungerade_Halbjahr1_khe.xml
  has 55 times.  The layer above is certainly oversized, and its events
  all contain preassigned resource CL_AG_PseudoStudent_0, so it's easy
  to see how things could go very wrong.  I need to work out what it's
  best to do, and do it.

1 December 2025.  There is just one oversize layer.  The debug output says
  "after assigning layer 0 of 251 (76 assigned of 80 - not all assigned)".
  The two unassigned meets are LS_186100 and LS_186600.  But the
  unassigned preassigned meet in the end is LS_185000_2_0, which has
  a preassigned time but is shown as unassigned "after repairing layer 0",
  using KheLayerRepair, which calls KheEjectionChainLayerRepairTimes.
  These are the unassigned meets before KheEjectionChainLayerRepairTimes:

    [ unassigned meets of layer:
      [ Meet LS_186100(durn 2, children 0, tasks 2)
	domain: TG:NoId{TP_1..TP_10, TP_12..TP_21, ...}
      ]
      [ Meet LS_186600(durn 2, children 0, tasks 2)
	domain: TG:NoId{TP_1..TP_10, TP_12..TP_21, ...}
      ]
    ]

  And these are the unassigned meets after it:

    [ unassigned meets of layer:
      [ Meet LS_186000(durn 1, children 0, tasks 2)
	domain: TG:All{TP_1..TP_55}
      ]
      [ Meet LS_188700(durn 1, children 0, tasks 2)
	domain: TG:All{TP_1..TP_55}
      ]
      [ Meet LS_185000_2_0(durn 2, children 0, tasks 3)
	domain: TG:TP_7{TP_7}
      ]
    ]

  So the unassigned meets have changed, which probably does not
  matter, except that one preassigned meet has changed from
  assigned to unassigned, which does matter.  The final error
  message is

    after KheNodeLayeredAssignTimes: found unassigned
      preassigned meet LS_185000_2_0

  which is the meet that KheEjectionChainLayerRepairTimes unassigned.

  And now I'm getting

    KheMeetDoMove(LS_185000_2_0, NULL, -)
    KheMeetDoMove(LS_186700_2_0, NULL, -)
    KheMeetDoMove(LS_185000_2_0, NULL, -)
    KheMeetDoMove(LS_186700_2_0, NULL, -)
    KheMeetDoMove(LS_185000_2_0, NULL, -)
    KheMeetDoMove(LS_186700_2_0, NULL, -)
    KheMeetDoMove(LS_185000_2_0, NULL, -)
    KheMeetDoMove(LS_186700_2_0, NULL, -)
    KheMeetDoMove(LS_185000_2_0, NULL, -)

  during KheEjectionChainLayerRepairTimes.  The next step is to
  abort when that happens and see where it is coming from.

  The problem is that KheMeetSwap does not check that its
  two meets are not preassigned before embarking on the swap.
  I've added this check to the one place in src_solvers
  where KheMeetSwap is called; it's in khe_st_helper.c.
  And hurrah! ModularXHSST_test18_Ungerade_Halbjahr1_khe.xml
  is running through to the end now.  Time for a full run.

  Out of memory on ModularXHSST_test1_Ungerade_Halbjahr1_khe.xml,
  even with only three threads.  And then on the second instance,
  ModularXHSST_test2_Ungerade_Halbjahr1_khe.xml, I got this:

     HaArenaSetGetChunk: out of memory

  So back to debugging the memory allocator, I guess.  I've
  now got it going over smaller free lists when getting a
  new large one will not work.  That seems to be working.

  Now running right through untis.xml with no crashing, but
  it does run out of memory about half the time (26 times,
  when there are 18 * 3 = 54 solves).

  Also tried another run with ps_avail_mem=0, that is, no
  memory limits, as a test, but also to see how many out of
  memory solves there would be in that case.  It runs out
  of memory on none of the 54 solves, so I've made it the
  default now.

  But when I ran 12 solves in parallel, I got this;

      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 1
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 2
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 3
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 4
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 5
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 6
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 7
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 8
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 9
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 10
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 11
      parallel solve of ModularXHSST_test4_Ungerade_khe.xml: starting solve 12
    ./doit: line 10: 100781 Killed
      khe -R untis.xml ps_threads=12 ps_make=12 ps_keep=1 gs_time_limit=10:00
      -s ps_soln_group=KHE24x12 > res.xml

  So it seems there are limits.  Not sure what they are though.  Do
  I need to reinstate sysinfo as the default?  The solution values
  I got before the kill were comparable to the three-thread ones.

2 December 2025.  Thought over the issues and decided that making
  sysinfo the default (when available) was best on the whole.  All
  documented and implemented now.

  Using the sysinfo default value, trying 12 threads on untis.xml.
  There are 12 * 18 = 216 solves altogether;  182 of these ran
  out of memory.

3 December 2025.  Working on the bug I found yesterday, which is
  that HaArenaSetDebug is reporting a negative number of busy
  arenas.  Fixed it (it happened when the arena set ran out of
  memory during HaArenaMake), and now of the 12 * 18 = 216 solves,
  174 were aborted.

  I've changed as->free_chunk_lists from an extensible array to
  a fixed array with length 40.  This avoids memory allocation
  when filling that array, which was a potential problem, and
  acts as a sanity check too.

  Official tests of memory handling
  ---------------------------------

  (1) untis.xml with 12 threads:  12 * 18 = 216 solves, of which
      163 were aborted.  Of the 18 instances, 7 had 0 solves
      completed.

  (2) untis.xml with 3 threads:  3 * 18 = 54 solves, of which
      32 were aborted.  Of the 18 instances, 6 had 0 solves
      completed.

  (3) Choosing one of the instances that had 0 solves completed,
      namedly test15_Ungerade_Halbjahr1_khe.xml, I tried running
      that alone with 3 threads.  And guess what, it didn't run
      out of memory at all.  So then I tried 12 threads, and
      all 12 ran out of memory.

  (3) INRC2-4.xml with 12 threads:  all solves completed normally.

  The result for INRC2-4-100-0-1108 was

    [ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
      0.01490 0.01565 0.01570 0.01575 0.01580 0.01590 0.01595 0.01615
      0.01615 0.01630 0.01655 0.01670
    ] 0.01470 is cost of best soln (after recombining)

  which is pretty much what I have been getting (e.g. 11 Nov),
  rel = 1.18.  For the whole run the average rel is 1.12:

    Instances (20) 	LOR17 	KHE24x12
                 	Cost 	Rel. 	Cost 	Rel.
    ------------------------------------------------
    INRC2-4-030-1-6291 	0.01695 1.00 	0.01865	1.10
    INRC2-4-030-1-6753 	0.01890	1.00 	0.02055	1.09
    INRC2-4-035-0-1718 	0.01425	1.00 	0.01620	1.14
    INRC2-4-035-2-8875 	0.01155	1.00 	0.01325	1.15
    INRC2-4-040-0-2061 	0.01685	1.00 	0.01915	1.14
    INRC2-4-040-2-6106 	0.01890	1.00 	0.02115	1.12
    INRC2-4-050-0-0487 	0.01505	1.00 	0.01635	1.09
    INRC2-4-050-0-7272 	0.01500	1.00 	0.01635	1.09
    INRC2-4-060-1-6115 	0.02505	1.00 	0.02870	1.15
    INRC2-4-060-1-9638 	0.02750	1.00 	0.03180	1.16
    INRC2-4-070-0-3651 	0.02435	1.00 	0.02735	1.12
    INRC2-4-070-0-4967 	0.02175	1.00 	0.02465	1.13
    INRC2-4-080-2-4333 	0.03340	1.00 	0.03680	1.10
    INRC2-4-080-2-6048 	0.03260	1.00 	0.03795	1.16
    INRC2-4-100-0-1108 	0.01245	1.00 	0.01470	1.18
    INRC2-4-100-2-0646 	0.01950	1.00 	0.02260	1.16
    INRC2-4-110-0-1428 	0.02440	1.00 	0.02560	1.05
    INRC2-4-110-0-1935 	0.02560	1.00 	0.02810	1.10
    INRC2-4-120-1-4626 	0.02170	1.00 	0.02405	1.11
    INRC2-4-120-1-5698 	0.02220	1.00 	0.02590	1.17
    ------------------------------------------------
        Average		0.02089 1.00 	0.02349	1.12

  This is approaching the 1.10 that we want.  Interestingly,
  INRC2-4-100-0-1108 is still the worst.

4 December 2025.  Did a few quick tests of khe -w and khe -m.
  All seems good, ready for posting the new version today,


To Do
=====

  Post new version of KHE and email Krystallidis and Ortiz.
  Current version is 2.13, new version will be 2.14.

  PATAT 2026 paper deadline is Tuesday 17 March 2026

  Look into Honorary Associate renewal asap.

  Grinding down INRC2-4-100-0-1108.  Time for whatever's next.

  Combine KheSolnCopy and KheSolnReduce?

  Change the documentation to reflect what I've done recently,
  notably frac=0.5 and sorting by decreasing maximum workload.

  KheDynamicResourceSequentialSolve is now selecting resources
  with higher workload limits.  Implemented and tested and
  working.  I've also written the code to bias the solver
  towards longer tasks when other things are equal, based
  on the durns_squared fields of expanders and solutions.
  This code seems to be working, and I'm getting good results,
  but I'm not convinced that it is as biassed towards long
  tasks as it should be.  Some debug output would be good.

  Day Late extension all done.  Results seem worse but I'll need to
  look closely at what is really going on.

  I really need more powerful repairs now - repairs that rebalance
  unbalanced loads and get rid of unnecessary assignments.  But how?

  Look at weekend grouping on 3Sat-3Sun.  There is a grouping of an
  Early on 3Sat with a Day on 3Sun that LOR has (NU_25) but KHE24
  does not have for some reason.

  Combination reduction has a way of identifying complete
  weekends constraints even when they are not explicitly
  present.  Shouldn't this be imported into weekend grouping?

  Added KheResourceTimetableMonitorAddProperRootTasksInTimeRange
  and KheResourceTimetableMonitorAddProperRootTasksInInterval.
  But I note that there is already a function in the mtask finder:
  KheAddResourceProperRootTasksInInterval, which seems to be
  unused.  It has a slightly different `wholly within' aspect.

  Resource matching has a task equivalencing aspect that might
  be replaceable by multi-tasks.  I need to look into that.
  There is an issue in that there is potentially a lot of task
  grouping going on, which doesn't interact well with mtasks.

  If swapping to the end helped, what about swapping back to
  the front?  There is a history problem, but anyway.

  What about some more non-trivial repairs?  Let's look for them.

  Looking at cluster busy times defects.  Both solutions pay 90
  for defects with weekends, and otherwise the defects are all
  workload overloads.  The difference in the number of these
  defects is (1150 - 950) / 20 = 10.  This roughly agrees with
  U + Y which for KHE is 28 + 22 = 50 and for LOR is 24 + 19 = 43,
  giving a difference of 7.

  3Wed seems to be the epicentre of unnecessary assignments.
  But TR_84 and TR_85 have unnecessary assignments then because
  they require their busy days to come in groups of 3 or more.

  ------------- Fun facts about INRC2-4-110-0-1935 --------------------
  This instance differs from INRC2-4-100-0-1108 only in the number of
  nurses (and presumably tasks) and in the limits on number of busy days.

  Times:  4 weeks, 4 times per day
  Nurses:  80 nurses + 30 trainees = 110 nurses

  There are hard assign resource constraints, and soft ones with
  weight 30.  There are many hard preferences for head nurses etc.
  There is a moderate number of avoid unavailable times constraints;
  their weight is only 10.  There are hard unwanted patterns:
  no non-Night shift after a Night shift, no Early or Day after
  a Late shift.  Complete weekends, weight 30.

                  Busy    Weekends   C-Free C-Working
          Weight:  20         30        30     30
    -------------------------------------------------
    FullTime     15-22       0-2       2-3    3-5
    PartTime      7-15       0-2       3-4    4-7
    HalfTime      7-11       0-1       3-7    3-5
    20Percent     2-5        0-1       2-7    2-5
    -------------------------------------------------

  Limits on consecutive same shift days are present and are the same
  for all resources:  Early 2-5, Day 2-28, Late 2-5, Night 4-5, all
  weight 15.  All these limits are as for INRC2-4-100-0-1108 except
  that the total busy days limits are slightly different.

  As for INRC2-4-100-0-1108, there are four grades:  HeadNurse, Nurse,
  Caretaker, and Trainee.  Trainee tasks and nurses are quite separate
  and form a separate instance.  Every HeadNurse is also a Nurse (but not
  a Caretaker); every Nurse (who is not a HeadNurse) is also a Caretaker.
  ------------- Fun facts about INRC2-4-110-0-1935 --------------------

  ------------- Fun facts about INRC2-4-100-0-1108 --------------------
  Times:  4 weeks, 4 times per day
  Nurses:  75 nurses + 25 trainees = 100 nurses

  There are hard assign resource constraints, and soft ones with
  weight 30.  There are many hard preferences for head nurses etc.
  There is a moderate number of avoid unavailable times constraints;
  their weight is only 10.  There are hard unwanted patterns:
  no non-Night shift after a Night shift, no Early or Day after
  a Late shift.  Complete weekends, weight 30.

                  Busy    Weekends   C-Free C-Working
          Weight:  20         30        30     30
    -------------------------------------------------
    FullTime     14-20       0-2       2-3    3-5
    PartTime      7-15       0-2       3-4    4-7
    HalfTime      7-10       0-1       3-7    3-5
    20Percent     3-6        0-1       2-7    2-5
    -------------------------------------------------

  Limits on consecutive same shift days are present and are the same
  for all resources:  Early 2-5, Day 2-[ "INRC2-4-100-0-1108", 12 threads, 12 solves, 5.0 mins:
    0.01490 0.01555 0.01565 0.01575 0.01580 0.01590 0.01595 0.01615
    0.01615 0.01615 0.01630 0.01670
  ] 0.01470 is cost of best soln (after recombining)28, Late 2-5, Night 4-5, all
  weight 15.  Unavailable times have weight 10.

  There are four grades:  HeadNurse, Nurse, Caretaker, and Trainee.
  Trainee tasks and nurses are quite separate and form a separate
  instance.  Every HeadNurse is also a Nurse (but not a Caretaker);
  every Nurse (who is not a HeadNurse) is also a Caretaker.
  ------------- Fun facts about INRC2-4-100-0-1108 --------------------

  What about this for removing italic entries.  Swap with someone
  who is overloaded, so that the italic entry is now in the
  timetable of the overloaded resource.  Then unassign the
  italic entry and possibly a neighbouring entry.  We remove
  the overload, which will pay for the unassignment if we
  reduce the overload by 2.

  e.g. HN_2 why not unassign the italic Late and its neighbour?

  Can we right-justify the numeric columns in HSEval?

  KheIncreaseLoadDoubleSwapMultiRepair and KheDoubleSwapRepair
  seem to be working.  Can I use KheDoubleSwapRepair elsewhere?
  What about something clever for fixing unavailable times defects?
  Or the elephant: workload overloads?

  What about profile grouping of night shifts, given that they
  have to occur in blocks of 4 or 5?  I've just read over the
  doc for that and there is a lot of relevant stuff there.
  And it says that the interaction with history is not implemented,
  which could explain the troubles we are having at the start
  of the timetable.  So there is a whole programme of work here.
  The first step is to find out what grouping we are getting now.

  Three-resource repairs for too-short sequences.  Get one
  fragment from one resource, another from another, and
  put them together into a longer sequence that goes to a
  third resource.  Or conversely, take a short sequence,
  break it into two pieces, and send each piece to a different
  resource.  These are probably best kept for level 1.

     r1 delete too-short sequence S1
     r2 delete too-short sequence (or part of longer sequence) S2
     r3 accepts S1+S2.

  or (probably more promising)

     r1 delete too-short sequence S1+S2
     r2 add S1 where r2 is free
     r3 add S2 where r3 is free

  But if S1+S2 is too short, S1 and S2 will be extra short.
  They need to add on where they make existing sequences longer.
  How does all of this fit with expanding?  Not wanted, I guess.
  Level 1, no expansion, only when free but adjacent to a busy time.
  The trouble is, after looking at the defects actually present
  in the current solution, none of them would be fixed by this.

  How's this?  (1) Run time sweep (2) identify all cases of
  positive limit active intervals that are too short; (3)
  unassign them (4) identify ways to group the unassigned
  intervals with other intervals that gives the right lengths;
  (5) do the grouping then unassign; (6) carry on.  It's
  fairly radical but it might just work.  Another option
  would be some form of profile grouping at the start.

  What about a full-width swap, but stopping and testing as
  we go to see if we have a better (or at least continuable)
  state?

  What about a repair that breaks a run of length 2 into two 
  runs and reassigns them to different resources?  There are
  a lot of options, we'd have to be careful to limit the
  choices, e.g. to suitable resources that are free at those
  times.

  Ejection chains:  "free" or "freer", "busy" or "busier"

  Do some detailed analysis of INRC2-8-030-1-27093606.xml and the
  other INRC2-8 instances.  The results I'm getting are quite poor;
  they should be better.

  (High school timetabling)  Take a look at UK-SP-06 from XHSTT-2014.
  The results are quite a lot worse than they used to be.

  (High school timetabling)  Is there a repair that swaps the times
  of two meets that share a preassigned resource?  There ought to be,
  and it ought to work pretty darn well.

  (High school timetabling)  There are places when KheSwapRepair and
  KheMoveRepair can't be called because there is no mtask finder
  during time repair.  I need to be able to execute these kinds of
  repairs not just on mtasks but also on tasks - or something.

  In INRC2-8-030-1-27093606 there are mtasks that do not satisfy the
  usual conditions (no gaps etc.).  I need to look into these mtasks.
  At present DRS just returns, after doing nothing, in these cases.

  Would it make sense to use the diversifier to set es_swap_widening_max?
  I've rarely done that kind of thing before.

  "if the domain allows unassignment only, try a double move"
  I understand that the domain could be empty, which means that
  unassignment is the only possibility, but what use is a double
  move in that case?  I guess we unassign the task and then
  assign the resource being unassigned to some other task at
  that time.  Yes, it does make sense - can we implement it?

  I've been looking into where KheTaskFinderMake is called from.
  There is a call from KheSolnTryTaskUnAssignments, but there
  does not seem to be any pressing need to use a task finder
  there.  It has been done so that sequences of adjacent tasks
  can be unassigned together.  But the key operation,
  KheFindTasksInInterval, could be implemented in the resource
  timetable module.  This leaves

    khe_se_solvers.c (now deleted)
    khe_sr_reassign.c
    khe_sr_single_resource.c (I may delete this module)

  where there are calls to KheTaskFinderMake.

  Appendix dynamic_impl.sig.correlators still to write.

  The existing task finder needs gs_event_timetable_monitor, which we
  can only reasonably create when time assignment is all finished.  So
  we seem to have already ruled out time adjustments during resource
  adjustments.  MTasks are not necessarily as bad as that, but we
  will need to see if we can avoid gs_event_timetable_monitor when
  fixed_times is false.  This would mean that we could only move
  tasks that are currently assigned resources and thus can be
  accessed from those resources' timetables.

  --------- An interesting variant of ejection chains -------
  Here's an interesting proposal for a variant of ejection chains.
  Have just one repair operation, which is a minimum-cost bipartite
  matching of all resources to their tasks, over an arbitrary
  sequence of adjacent days.  Each resource can match with the
  sequence of tasks initially assigned to itself or to some
  other resource, or alternatively to a free day.

  When building the bipartite graph, we may choose to leave
  out certain edges.  For example, if we are trying to unassign
  a resource at a certain time, we leave out all the edges that
  assign it to some task.  Or if we are trying to assign it
  then, we leave out its current edge, the one that causes
  it to be unassigned.  Actually we could just nominate the
  resource or task and declare that its current edge should
  be omitted.

  Then we link together these rematchings using ejection
  chains.  We decide on a repair of a single task or set of
  tasks, which is to either force their assignment or force
  their unassignment, and we make this happen by building
  the bipartite graph with the appropriate omissions.

  How to prevent cycling within one chain is a question.
  We could use the usual "don't visit the same monitor
  twice" or rather "don't alter the same monitor twice".
  Alternatively, we could insist that no chain visit any
  given day twice.

  The existing rematch module allows you to build the
  demand nodes for a given set of supply nodes and re-use
  those demand nodes.  I could use that; I could cache
  sets of demand nodes as they are built for the first time,
  and re-use the cached values.  The actual solve call
  would need to be passed a fixed non-edge separately.

  (1) Decide to move some task, or sequence of adjacent tasks.  We
      need good analysis to produce a smallest set of adjacent
      tasks that is likely to work well for the current defect.

  (2) Weighted bipartite match over that sequence of adjacent days.

  Before building the whole matching, see the effect of the
  proposed change on the resource or task affected, and only
  proceed if it improves the initial defect and does not
  introduce any more.  Then build the full graph and proceed.
  Could bury this whole aspect into the repairer.

  One repair equals one time interval plus a (supply, demand)
  edge such that that edge must be omitted from the graph.
  This will drive the solution away from the current solution.
  --------- An interesting variant of ejection chains -------

  (The stuff below here refers to the dynamic programming algorithm.)
  What about an A* search, not for pruning but for choosing
  the next solution to expand?  The trouble is it would not
  prove anything, unless the A* estimate was known to be a
  lower bound on the actual cost.  To get that we would
  basically have to do an optimal assignment of one resource
  from the current point to the end.  Too slow, surely.

  Can we run the algorithm on multiple cores?  If we do this
  successfully we could reduce the running time by a factor
  of 12, or say 10.  But how?  Perhaps we need to lock each
  index within the indexed list data structure.  Nasty.

  At "Here is the code (omitted above) to build", the shift
  solution trie section moves from trie construction to a form
  of expansion by resources.  This latter part probably
  belongs elsewhere.

  If solutions did have a common parent type including a
  signature, we could unify code that adds a solution to
  a solution set, doing dominance testing along the way;
  although the code to free a solution object would need
  a type switch.

  Do a review of signature caching.  It never seemed to speed
  anything up, although it should have done, and the code for
  it may have decayed.

  A merge of KHE_DRS_SIGNATURE and KHE_DRS_SIGNATURE_SET might be
  good.  It would be a tagged union of the two types, basically.
  The point is that then users don't have to worry about how
  signatures are put together.  A signer could have the same
  merged structure, and it might be able to build structured
  signatures just as it builds unstructured ones now.  Perhaps
  not even tagged, perhaps one part holds states and another
  part holds sub-signatures (but not sub-sub-signatures?).
  Another way to merge them would be to make KHE_DRS_SIGNATURE
  private to KHE_DRS_SIGNATURE_SET, so that everything is a
  signature set, and users of KHE_DRS_SIGNATURE now would
  have to use a signature set containing one signature.

  What about hashing the key first?  If it strikes an exact match
  we get a definite answer immediately:  the one with smaller cost
  gets deleted.  But what are the chances?  Not good, I think, but
  I am not sure.  Actually a trie would be faster and more definite.
  For assign by resources we could drop down the tree as we build.
  But if the new solution replaces the old, we still have to do
  the full thing.

  A must-assign task must get assigned to someone.  Can we use
  that to predict poor performance on the next day?  Or is there
  any other way to uncover correlation between, as opposed to
  within, resources?

  I need to focus on the first two days of solving for five trainees
  (and indeed four, although for four the problem does not really hit
  until making 1Fri).  Here's what I wrote on 19 February 2023:

    "Like before, I had to abandon 5 trainees:

      [ KheDrsSolveSearch(5 resources, 14 days)
	KheDrsSolveSearch ending day 1Mon (made 3041, undominated 3041)
	KheDrsSolveSearch ending day 1Tue (made 433770, undominated 34418)
	... (killed before getting this far)
      ]

    The number 3041 is reasonable, as the following argument shows.
    Each trainee has a choice of 4 shifts plus a free day, making 5
    choices altogether.  (Because there are excess slots we can say
    that in practice all 4 shifts are available to all trainees.)
    So there are about 5 * 5 * 5 * 5 * 5 = 3125 choices.  And on
    subsequent days, for each undominated solution on the previous
    day there are about 3125 choices, although some of them will be
    killed off very early by hard constraints, which explains why
    we do not generate anything near 3041 * 3125 day 2 solutions."

  This is the basic remaining problem.  Compared with the number
  of solutions that could be made on Day 2, the number actually
  made is small:  433770 / (3041 * 3125) = 0.05.  And compared
  with the number made, the number of undominated solutions kept is
  also small:  34418 / 433770 = 0.08.  But despite these positives
  the algorithm is being overwhelmed by large numbers of solutions.

  Sorting by weight shaved about 20% off the run time, and
  visiting the hard constraint entries before the soft ones
  when dominance testing shaved off another 30% (amazing),
  down to 6.4 seconds.  So anything we can do to speed up
  one dom test will be well worth doing.  Any other ideas?

  Solver seems to be working now, but still it is not fast enough
  to reassign five resources, or indeed four trainees.  I need
  another good idea.

  I recently moved "included_free_resources_index + 1" to what I
  thought was a better location, but now that I see it documented
  I am much less sure.  Look at it again.

  Did breaking up resource expand begin into two stages actually
  achieve anything?  KheDrsExpanderOpenToExtraCost is called but
  does not seem to be affected by the breakup.

  EvalSignature:  where is it presented, do the calls to it
  make sense to the reader, e.g. in KheDrsAsstToShiftMake?

  Make the solver return early (with failure) if time runs out.

  Signature value adjustment may need a rethink for sequence
  monitors.

  May need to revisit the current plan of always returning when
  the available cost goes negative.  This is because sequence
  monitors can contribute a positive amount to available cost.
  At any rate we should do some testing to see which is faster.

  Good idea:  compare old dom test with new dom test, and
  if there are cases where the old test succeeds and the
  new one does not, look into it.  Also vice versa.

  ==== dynamic programming ideas above this point, general ideas below ====

  If we want to combine ejection chains with dynamic programming,
  it might actually be easier to add ejection chain code to the
  dynamic programming module.  Limit task sets to the ones that
  the resources were freed from.  Could do that now, actually.

  What about a solver that swaps around the assigned shifts,
  without assigning or unassigning any resource, with the
  aim of getting the number of consecutive same shifts right.
  Is that a tractable problem?  Surely ejection chains do that?

  Look into how the resource assignment invariant interacts
  with the new "rs" option.

  It seems to be time to do some serious testing of the VLSN solver
  and compare what we get with what Legrain got.  My paper says he
  got 1695, his paper (tt/patat21/dynamic_papers/legrain.pdf) says
  1685.  My own best result from my own paper was 1835.

  Legrain's running time is m x 6n + 60, where m is the number of
  weeks and n is the number of nurses.  For the test instance,
  m = 4 and n = 30, so this is 4 x 6 x 30 + 60 = 13 minutes.

  Testing ./doit in tt/nurse/solve.

  Reference in paper I refereed to dynamic programming solver?

  Speaking generally, we now have two new solvers to play with:
  the single resource solver, and the cluster minimum solver.
  Our mission is to make the best use we can of both.  We can
  run the cluster minimum solver once at the start and have its
  results permanently used throughout the rest of the solve.  And
  we can use KheSingleResourceSolverBest in conjunction with a
  balance solver to select a best solution from the single
  resource solver, and adopt that solution.  But when should we
  run single resource solving, and which resource(s) should we
  select for single resource solving?  For example:

  * Run single-resource solving on a fixed percentage of the
    resources (with highest workload limits) before time sweep,
    and then omit those resources from the time sweep.

  * Find optimal timetables for several resources over a subset
    of the interval, and use that as the basis for a VLSN search.

  Explore possible uses for the now-working cluster minimum
  solver.  Could it be run just before time sweep?  Could
  the changed minimum limits remain in place for the entire
  solve?  Also look at the solutions we are getting now from
  single resource assignment.  If one resource is already
  assigned, does that change the solve for the others?

  Make the cluster minimum solver take account of history.

  OK, what about this?  Use "extended profile grouping" to group all
  tasks into runs of tasks of the same shift type and domain.  Then
  use resource packing (largest workload resources first) to pack
  the runs into the resources.  Finish off with ejection chains.
  This to replace the current first stage.  Precede profile grouping
  by combinatorial grouping, to get weekend tasks grouped together.  
  Keep a matching at each time, so that unavailable times of other
  resources are taken into account, we want the unassigned tasks at
  every time to be assignable to the unpacked resources at that time.
  At least it's different!

  After INRC2-4-030-1-6291 is done, INRC2-4-035-0-1718 would be good to
  work on.  The current results are 21% worse, giving plenty to get into.

  Event timetables still to do.  Just another kind of dimension?
  But it shows meets, not tasks.

  Ideas:

  * Some kind of lookahead during time sweep that ensures resources
    get the weekends they need?  Perhaps deduce that the max limit
    implies a min limit, and go from there?

  * Swapping runs between three or more resources.  I tried this
    but it seems to take more time than it is worth; it's better
    to give the extra time to ejection chains

  * Ejection beams - K ejection chains being lengthened in
    parallel, if the number of unrepaired defects exceeds K
    we abandon the repair, but while it is less we keep going
    Tried this, it has some interest but does not improve things.

  * Hybridization with simulated annealing:  accept some chains
    that produce worse solutions; gradually reduce the temperature.

  Decided to just pick up where I left off, more or less, and go to
  work on INRC2-4-030-1-6291.  I'm currently solving in just 5.6
  seconds, so it makes a good test.

  Fun facts about INRC2-4-030-1-6291
  ----------------------------------

  * 4 weeks

  * 4 shifts per day:  Early (1), Day (2), Late (3), and Night (4) 
    The number of required ones varies more or less randomly; not
    assigning one has soft cost 30.

  * 30 Nurses:

       4 HeadNurse:  HN_0,  ... , HN_3
      13 Nurse:      NU_4,  ... , NU_16
       8 Caretaker:  CT_17, ... , CT_24
       5 Trainee:    TR_25, ... , TR_29

    A HeadNurse can also work as a Nurse, and a Nurse can also work
    as a Caretaker; but a Caretaker can only work as a Caretaker, and
    a Trainee can only work as a Trainee.  Given that there are no
    limit resources constraints and every task has a hard constraint
    preferring either a HeadNurse, a Nurse, a Caretaker, or a Trainee,
    this makes Trainee assignment an independent problem.

  * 3 contracts: Contract-FullTime (12 nurses), Contract-HalfTime
    (10 nurses), Contract-PartTime (8 nurses).  These determine
    workload limits of various kinds (see below).  There seems
    to be no relationship between them and nurse type.

  * There are unavailable times (soft 10) but they are not onerous

  * Unwanted patterns: [L][ED], [N][EDL], [D][E] (hard), so these
    prohibit all backward rotations.

  * Complete weekends (soft 30)

  * Contract constraints:                   Half   Part   Full    Wt
    ----------------------------------------------------------------
    Number of assignments                   5-11   7-15  15-20*   20
    Max busy weekends                          1      2      2    30
    Consecutive same shift days (Early)      2-5    2-5    2-5    15
    Consecutive same shift days (Day)       2-28   2-28   2-28    15
    Consecutive same shift days (Late)       2-5    2-5    2-5    15
    Consecutive same shift days (Night)      3-5    3-5    3-5    15
    Consecutive free days                    2-5    2-4    2-3    30
    Consecutive busy days                    2-4    3-5    3-5    30
    ----------------------------------------------------------------
    *15-20 is notated 15-22 but more than 20 is impossible.

  Currently giving XUTT a rest for a while.  Here is its to do
  list, prefixed by + characters:

  +Can distinct() be used for distinct times?  Why not?  And also
  +using it for "same location" might work.

  +I've finished university course timetabling, except for MaxBreaks
  +and MaxBlock, which I intend to leave for a while and ponder over
  +(see below).  I've also finished sports scheduling except for SE1
  +"games", which I am waiting on Bulck for but which will not be a
  +problem.

  +MaxBreaks and MaxBlock
  +----------------------

    +These are challenging because they do the sorts of things that
    +pattern matching does (e.g. idle times), but the criterion
    +which determines whether two things are adjacent is different:

      +High school timetabling - adjacent time periods
      +Nurse rostering - adjacent days
      +MaxBreaks and MaxBlock - intervals have gap of at most S.

    +It would be good to have a sequence of blocks to iterate over,
    +just like we have some subsequences to iterate over in high
    +school timetabling and nurse rostering.  Then MaxBreaks would
    +utilize the number of elements in the sequence, and MaxBlock
    +would utilize the duration of each block.

    +We also need to allow for future incorporation of travel time 
    +into MaxBreaks and MaxBlock.  Two events would be adjacent if
    +the amount of time left over after travelling from the first
    +to the second was at most S.

    +Assuming a 15-week semester and penalty 2:

    +MaxBreaks(R, S):

	+<Tree val="sum|15d">
	    +<ForEach v="$day" from="Days">
		+<Tree val="sum:0-(R+1)|2">
		    +<ForEachBlock v="$ms" gap="S" travel="travel()">
			+<AtomicMeetSet e="E" t="$day">
			+<Tree val="1">
		    +</ForEachBlock>
		+</Tree>
	    +</ForEach>
	+</Tree>

    +MaxBlock(M, S):

	+<Tree val="sum|15d">
	    +<ForEach v="$day" from="Days">
		+<Tree val="sum:0-M|2">
		    +<ForEachBlock v="$ms" gap="S" singles="no" travel="travel">
			+<AtomicMeetSet e="E" t="$day">
			+<Tree val="$ms.span:0-M|1s">
		    +</ForEachBlock>
		+</Tree>
	    +</ForEach>
        +</Tree>

    +Actually it might be better if each iteration produced a meet set.
    +We could then ask for span and so forth as usual.  There is also
    +a connection with spacing(a, b).  In fact it would be good to
    +give a general expression which determines whether two
    +chronologically adjacent meets are in the same block.
    +Then we could use "false" to get every meet into a separate
    +block, and then spacing(a, b) would apply to each pair of
    +adjacent blocks in the ordering.  If "block" has the same
    +type as "meet set", we're laughing.

    +I'll let this lie fallow for a while and come back to it.

  +Rather than sorting meets and defining cost functions which
  +are sums, can we iterate over the sorted meets?

  +The ref and expr attributes of time sequences and event sequences
  +do the same thing.

  +There is an example of times with attributes in the section on
  +weighted domain constraints.  Do we want them?  How do they fit
  +with time pattern trees?  Are there weights for compound times?

  +Moved history from Tree to ForEachTimeGroup.  This will be
  +consistent with pattern matching, and more principled, since
  +history in effect extends the range of the iterator.  But
  +what to do about general patterns?  We need to know how each
  +element of the pattern matches through history.

  +Could use tags to identify specific task sets within patterns.

  Install the new version of HSEval on web site, but not until after
  the final PATAT 2020 deadline.

  In the CQ14-13 table, I need to see available workload in minutes.

  Fun facts about instance CQ14-13
  --------------------------------

  * A four-week instance (1Mon to 4Sun) with 18 times per day:

      a1 (1),  a2 (2),  a3 (3),  a4 (4),  a5 (5),
      d1 (6),  d2 (7),  d3 (8),  d4 (9),  d5 (10),
      p1 (11), p2 (12), p3 (13), p4 (14), p5 (15),
      n1 (16), n2 (17), n3 (18)

    There are workloads, presumably in minutes, that vary quite a bit:

      a1 (480),  a2 (480),  a3 (480),  a4 (600),  a5 (720),
      d1 (480),  d2 (480),  d3 (480),  d4 (600),  d5 (720),
      p1 (480),  p2 (480),  p3 (480),  p4 (600),  p5 (720),
      n1 (480),                        n2 (600),  n3 (720)

    480 minutes is an 8-hour shift, 720 minutes is 12 hours.

  * 120 resources, with many hard preferences for certain shifts:

      Preferred-a1 Preferred-a2 Preferred-a3 Preferred-a4 Preferred-a5
      Preferred-d1 Preferred-d2 Preferred-d3 Preferred-d4 Preferred-d5
      Preferred-p1 Preferred-p2 Preferred-p3 Preferred-p4 Preferred-p5
      Preferred-n1 Preferred-n2 Preferred-n3

    although most resources have plenty of choices from this list.
    Anyway this leads to a huge number of prefer resources constraints.

  * There are also many avoid unavailable times constraints, some for
    whole days, many others for individual times; hard and soft.

  * Unwanted patterns (hard).  In these patterns, a stands for
    [a1a2a3a4a5] and so on.

      [d4][a]
      [p5][adp4-5]
      [n1][adp]
      [n2-3][adpn3]
      [d1-3][a1-4]
      [a5d5p1-4][ad]

    This is basically "day off after a sequence of night shifts",
    with some other stuff that probably matters less; a lot of it
    is about the 480 and 720 minute shifts.

  * MaxWeekends (hard) for most resources is 2, for some it is 1 or 3.

  * MaxSameShiftDays (hard) varies a lot, with fewer of the long
    workload shifts allowed.  NB this is not consecutive, this is
    total.  About at most 10 of the shorter, 3 of the longer.
    Doesn't seem very constraining, given that typical workloads
    are 15 or 16 shifts.

  * Many day or shift on requests, soft with varying weights (1-3).

  * Minimum and maximum workload limits in minutes (hard), e.g.

      Minutes           480-minute shifts
      -----------------------------------------------------------
      3120 - 3840
      4440 - 5160
      7440 - 8160        15.5 - 17.0
      7920 - 8640        16.5 - 18.0

    The last two ranges cover the great majority of resources.
    These ranges are quite tight, especially for hard constraints.

  * MinConsecutiveFreeDays 2 (hard) for most resources, 3 (hard)
    for a few.

  * MaxConsecutiveBusyDays 5 (hard) for most resources, 6 (hard)
    for a few.

  * MinConsecutiveBusyDays 2 (hard), for all or most resources.

  Decided to work on CQ14-13 for a while, then tidy up, rerun,
  and submit.

  What does profile grouping do when the minimum limits are
  somewhat different for different resources, and thus spread
  over several constraints?

  INRC1-ML02 would be a good test.  It runs fast and the gap is
  pretty wide at the moment.  Actually I worked on it before (from
  8 November 2019).  It inspired KhePropagateUnavailableTimes.

  Fun facts about INRC1-ML02
  --------------------------

    * 4 weeks 1Fri to 4Thu

    * 4 shifts per day: E (1), L (2), D (3), and N (4).  But there are
      only two D shifts each day, so this is basically a three-shift
      system of Early, Late, and Night shifts.

    * 30 Nurses:
  
        Contract-0  Nurse0  - Nurse7
        Contract-1  Nurse8  - Nurse26
        Contract-2  Nurse27 - Nurse29

    * Many day and shift off requests, all soft 1 but challenging.
      I bet this is where the cost is incurred.

    * Complete weekends (soft 2), no night shift before free
      weekend (soft 1), identical shift types during weekend (soft 1),
      unwanted patterns [L][E], [L][D], [D][N], [N][E], [N][D],
      [D][E][D], all soft 1

    * Contract constraints         Contract-0    Contract-1   Contract-2
      ----------------------------------------------------------------
      Assignments                    10-18        6-14          4-8
      Consecutive busy weekends       2-3     unconstrained     2-3
      Consecutive free days           2-4         3-5           4-6
      Consecutive busy days           3-5         2-4           3-4
      ----------------------------------------------------------------

      Workloads are tight, there are only 6 shifts to spare, or 8 if
      you ignore the overloads in Nurse28 and Nurse29, which both
      GOAL and KHE18x8 have, so presumably they are inevitable.


  Do something about constraints with step cost functions, if only
  so that I can say in the paper that it's done.

  In INRC2-4-030-1-6291, the difference between my 1880 result and
  the LOR17 1695 result is about 200.  About 100 of that is in
  minimum consecutive same shift days defects.  Max working weekends
  defects are another problem, my solution has 3 more of those
  than the LOR17 solution has; at 30 points each that's 90 points.
  If we can improve our results on these defects we will go a long
  way towards closing the gap.

  Grinding down INRC2-4-030-1-6291 from where it is now.  It would
  be good to get a better initial solution from time sweep than I am
  getting now.  Also, there are no same shift days defects in the
  LOR17 solution, whereas there are 

  Perhaps profile grouping could do something unconventional if it
  finds a narrow peak in the profile that really needs to be grouped.

  What about an ejection chain repair, taking the current runs
  as indivisible?

  My chances of being able to do better on INRC2-4-030-1-6291
  seem to be pretty slim.  But I really should pause and make
  a serious attack on it.  After that there is only CQ to go,
  and I have until 30 January.  There's time now and if I don't
  do it now I never will.

  Better to not generate contract (and skill?) resource groups if
  not used.

  Change KHE's general policy so that operations that change
  nothing succeed.  Having them fail composes badly.  The user
  will need to avoid cases that change nothing.

  Are there other modules that could use the task finder?
  Combinatorial grouping for example?  There are no functions
  in khe_task.c that look like task finding, but there are some
  in khe_resource_timetable_monitor.c:

    KheResourceTimetableMonitorTimeAvailable
    KheResourceTimetableMonitorTimeGroupAvailable
    KheResourceTimetableMonitorTaskAvailableInFrame
    KheResourceTimetableMonitorAddProperRootTasks

  KheTaskSetMoveMultiRepair phase variable may be slow, try
  removing it and just doing everything all together.

  Fun facts about COI-Musa
  ------------------------

  * 2 weeks, one shift per day, 11 nurses (skills RN, LPN, NA)

  * RN nurses:  Nurse1, Nurse2, Nurse3,
    LPN nurses: Nurse4, Nurse5, 
    NA nurses:  Nurse6, Nurse7, Nurse8, Nurse9, Nurse10, Nurse11

  Grinding down COI-HED01.  See above, 10 October, for what I've
  done so far.

  It should actually be possible to group four M's together in
  Week 1, and so on, although combinatorial grouping only tries
  up to 3 days so it probably does not realize this.

  Fun facts about COI-HED01
  -------------------------

    * 31 days, 5 shifts per day: 1=M, 2=D, 3=H, 4=A, 5=N

    * Weekend days are different, they use the H shift.  There
      is also something peculiar about 3Tue, it also uses the
      H shift.  It seems to be being treated like a weekend day.
      This point is reflected in other constraints, which treat
      Week 3 as though it had only four days.

    * All demand expressed by limit resources constraints,
      except for the D shift, which has two tasks subject
      to assign resource and prefer resources constraints.
      The other shifts vary between about 7 and 9 tasks.  But
      my new converter avoids all limit resources constraints.

    * There are 16 "OP" nurses and 4 "Temp" nurses.
      Three nurses have extensive sequences of days off.
      There is one skill, "Skill-0", but it contains the
      same nurses as the OP nurses.

    * The constraints are somewhat peculiar, and need attention
      (e.g. how do they affect combinatorial grouping?)
    
        [D][0][not N]  (Constraint:1)
          After a D, we want a day off and then a night shift (OP only).
	  Only one nurse has a D at any one time, so making this should
	  not be very troublesome.

	[not M][D]  (Constraint:2)
	  Prefer M before D (OP only), always seems to get ignored,
	  even in the best solutions.  This is because during the
	  week that D occurs, we can't have a week full of M's.
	  So really this constraint contradicts the others.

	[DHN][MDHAN]  (Constraint:3)
	  Prefer day off after D, H, or N.  Always seems to be
	  satisfied.  Since H occurs only on weekends, plus 3Tue,
	  each resource can work at most one day of the weekend,
	  and if that day is Sunday, the resource cannot work
	  M or A shifts the following week (since that would
	  require working every day).  Sure enough, in the
	  best solution, when an OP nurse works an H shift on
	  a Sunday, the following week contains N shifts and
	  usually a D shift.  And all of the H shifts assigned
	  to Temp nurses are Sunday or 3Tue ones.

	Constraint:4 says that Temp nurses should take H and
	D shifts only.  It would be better expressed by a
	prefer resources constraint but KHE seems happy
	enough with it.

	Constraint:5 says that assigning any shift at all to
	a Temp nurse is to be penalized.  Again, a prefer
	resources constraint would have been better, but at
	present both KHE and the best solution assign 15 shifts
	to Temp nurses, so that's fine.

	The wanted pattern is {M}{A}{ND}{M}{A}{ND}..., where
	{X} means that X only should occur during a week.
	This is for OP nurses only.  It is expressed rather
	crudely:  if 1 M in Week X, then 4 M in Week X.
	This part of it does not apply to N, however; it says
	"if any A in Week X, then at least one N in Week X+1".
	So during N weeks the resource usually has less than
	4 N shifts, and this is its big chance to take a D.

	OP nurses should take at least one M, exactly one D,
	at least one H, at most 2 H, at least one A, at least
	one N.  These constraints are not onerous.

    * Assign resource and prefer resources constraints specify:

        - There is one D shift per day

    * Limit resources constraints specify 

        Weekdays excluding 3Tue

        - Each N shift must have exactly 2 Skill-0 nurses.

	- Each M shift and each A shift must have exactly 4
	  Skill-0 nurses

	- There are no H shifts

	Weekend days, including 3Tue

	- Each H shift must have at least 2 Skill-0 nurses

	- Each H shift must have exactly 4 nurses altogether

	- There are no M, A, or N shifts on 3Tue

	- There are no M, A, or N shifts on weekend days

    * The new converter is expressing all demands with assign
      resource and prefer resources constraints, as follows:

      D shifts:

        <R>NA=s1000:1</R>
	<R>A=s1000:1</R>

	So one resource, any skill.

      H shifts (weekends and 3Tue):

        <R>NA=s1000+NW0=s1000:1</R>
	<R>NA=s1000+NW0=s1000:2</R>
	<R>NA=s1000:1</R>
	<R>NA=s1000:2</R>
	<R>A=s1000:1</R>

	So 2 Skill-0 and 2 arbitrary, as above

      M and A shifts (weekdays not 3Tue):

        <R>NA=s1000+NW0=s1000:1</R>
	<R>NA=s1000+NW0=s1000:2</R>
	<R>NA=s1000+NW0=s1000:3</R>
	<R>NA=s1000+NW0=s1000:4</R>
	<R>W0=s1000:1</R>
	<R>W0=s1000:2</R>
	<R>W0=s1000:3</R>
	<R>W0=s1000:4</R>
	<R>W0=s1000:5</R>

	So exactly 4 Skill-0, no limits on Temp nurses

      N shifts (weekday example)

        <R>NA=s1000+NW0=s1000:1</R>
	<R>NA=s1000+NW0=s1000:2</R>
	<R>W0=s1000:1</R>
	<R>W0=s1000:2</R>
	<R>W0=s1000:3</R>
	<R>W0=s1000:4</R>
	<R>W0=s1000:5</R>

      Exactly 2 Skill-0, no limits on Temp nurses.

  It would be good to have a look at COI-HED01.  It has
  deteriorated and it is fast enough to be a good test.
  Curtois' best is 136 and KHE18x8 is currently at 183.
  A quick look suggests that the main problems are the
  rotations from week to week.

  Back to grinding down CQ14-05.  I've fixed the construction
  problem but with no noticeable effect on solution cost.

  KheClusterBusyTimesConstraintResourceOfTypeCount returns the
  number of resources, not the number of distinct resources.
  This may be a problem in some applications of this function.

  Fun facts about CQ14-05
  -----------------------

    * 28 days, 2 shifts per day (E and L), whose demand is:

           1Mon 1Tue 1Wed 1Thu 1Fri 1Sat 1Sun 2Mon 2Tue 2Wed 2Thu
        ---------------------------------------------------------
        E   5    7    5    6    7    6    6    6    6    6    5
        L   4    4    5    4    3    3    4    4    4    6    4
        ---------------------------------------------------------
        Tot 9   11   10   10   10    9   10   10   10   12    9

      Uncovered demands (assign resources defects) make up the
      bulk of the cost (1500 out of 1543).  Most of this (14 out
      of 15) occurs on the weekends.

    * 16 resources named A, B, ... P.  There is a Preferred-L
      resource group containing {C, D, F, G, H, I, J, M, O, P}.
      The resources in its complement, {A, B, E, K, L, N}, are
      not allowed to take late shifts.

    * Max 2 busy weekends (max 3 for for resources K to P)

    * Unwanted pattern [L][E]

    * Max 14 same-shift days (not consecutive).  Not hard to
      ensure given that resource workload limits are 16 - 18.

    * Many day or shift on requests.  These basically don't
      matter because they have low weight and my current best
      solution has about the same number of them as Curtois'

    * Workload limits (all resources) min 7560, max 8640
      All events (both E and L) have workload 480;
      7560 / 480 = 15.7, 8640 / 480 = 18.0, so every resurce
      needs between 16 and 18 shifts.  The Avail column agrees.

    * Min 2 consecutive free days (min 3 for resources K to P)

    * Max 5 consecutive busy days (max 6 for resources K to P)

    * Curtois' best is 1143.  This represents 2 fewer unassigned
      shifts (costing 100 each) and virtually the same other stuff.

  Try to get CQ14-24 to use less memory and produce better results.
  But start with a smaller, faster CQ14 instance:  CQ14-05, say.

  In Ozk*, there are two skill types (RN and Aid), and each
  nurse has exactly one of those skills.  Can this be used to
  convert the limit resources constraints into assign resource
  and prefer resources constraints?

  Grinding down COI-BCDT-Sep in general.  I more or less lost
  interest when I got cost 184 on the artificial instance, but
  this does include half-cycle repairs.  So more thought needed.
  Could we add half-cycle repairs to the second repair phase
  if the first ended quickly?

  KheCombSolverAddProfileGroupRequirement could be merged with
  KheCombSolverAddTimeGroupRequirement if we add an optional
  domain parameter to KheCombSolverAddTimeGroupRequirement.

  Fun facts about COI-BCDT-Sep
  ----------------------------

    * 4 weeks and 2 days, starting on a Wednesday

    * Shifts: 1 V (vacation), 2 M (morning), 3 A (afternoon), 4 N (night).

    * All cover constraints are limit resources constraints.  But they
      are quite strict and hard.  Could they be replaced by assign
      resource constraints?  (Yes, they have been.)

	  Constraint            Shifts               Limit    Cost
	  --------------------------------------------------------
          DemandConstraint:1A   N                    max 4      10
	  DemandConstraint:2A   all A; weekend M     max 4     100
	  DemandConstraint:3A   weekdays M           max 5     100
	  DemandConstraint:4A   all A, N; weekend M  max 5    hard
	  DemandConstraint:5A   weekdays M           max 6    hard
	  DemandConstraint:6A   all A, N; weekend M  min 3    hard
	  DemandConstraint:7A   all N                min 4      10
	  DemandConstraint:8A   all A; weekend M     min 4     100
	  DemandConstraint:9A   weekday M            min 4    hard
	  DemandConstraint:10A  weekday M            min 5     100
	  --------------------------------------------------------

      Weekday M:   min 4 (hard), min 5 (100), max 5 (100), max 6 (hard),
      Weekend M:   min 3 (hard), min 4 (100), max 4 (100), max 5 (hard) 
      All A:       min 3 (hard), min 4 (100), max 4 (100), max 5 (hard)
      All N:       min 3 (hard), min 4 (10),  max 4 (10),  max 5 (hard)

    * There are day and shift off constraints, not onerous

    * Avoid A followed by M

    * Night shifts are to be assigned in blocks of 3, although
      a four block is allowed to avoid fri N and sat free.  There
      are hard constraints requiring at least 2 and at most 4
      night shifts in a row.

    * At least six days between sequences of N shifts; the
      implementation here could be better, possibly.

    * At least two days off after five consecutive shifts

    * At least two days off after night shift

    * Prefer at least two morning shifts before a vacation period and
      at least one night shift afterwards

    * Between 4 and 8 weekend days

    * At least 10 days off

    * 5-7 A (afternoon) shifts, 5-7 N (night) shifts

    * Days shifts (M and A, taken together) in blocks of exactly 3

    * At most 5 working days in a row.

  Work on COI-BCDT-Sep, try to reduce the running time.  There are
  a lot of constraints, which probably explains the poor result.

  Should we limit domain reduction at the start to hard constraints?
  A long test would be good.

  In khe_se_solvers.c, KheAddInitialTasks and KheAddFinalTasks could
  be extended to return an unassign_r1_ts task set which could then be
  passed on to the double repair.  No great urgency, but it does make
  sense to do this.  But first, let's see whether any instances need it.

  Also thought of a possibility of avoiding repairs during time sweep,
  when the cost blows out too much.  Have to think about it and see if
  it is feasible.

  Take a close look at resource matching.  How good are the
  assignments it is currently producing?  Could it do better?

  Now it is basically the big instances, ERRVH, ERMGH, and MER
  that need attention.  Previously I was working on ERRVH, I
  should go back to that.

  Is lookahead actually working in the way I expect it to?
  Or is there something unexpected going on that is preventing
  it from doing what it has the potential to do?

  UniTime requirements not covered yet:

    Need an efficient way to list available rooms and their
    penalties.  Nominally this is done by task constraints but
    something more concise, which indicates that the domain
    is partitioned, would be better.

    Ditto for the time domain of a meet.

    SameStart distribution constraint.  Place all times
    with the same start time in one time group, have one
    time group for each distinct starting time, and use
    a meet constraint with type count and eval="0-1|...".

    SameTime is a problem because there is not a simple
    partition into disjoint sets of times.  Need some
    kind of builtin function between pairs of times, but
    then it's not clear how this fits in a meet set tree.

    DifferentTime is basically no overlap, again we seem
    to need a binary attribute.

    SameDays and SameWeeks are cluster constraints, the limit
    would have to be extracted from the event with the largest
    number of meets, which is a bit dodgy.

    DifferentDays and DifferentWeeks just a max 1 on each day
    or week.

    Overlap and NotOverlap: need a binary for the amount of
    overlap between two times, and then we can constrain it
    to be at least 1 or at most 0.  NB the distributive law

       overlap(a+b, c+d) = overlap(a, c) + overlap(a, d)
         + overlap(b, c) + overlap(b, d)

    but this nice property is not going to hold for all
    binary attributes.

    Precedence: this is the order events constraint, with
    "For classes that have multiple meetings in a week or
    that are on different weeks, the constraint only cares
    about the first meeting of the class."  No design for
    this yet.

    WorkDay(S): "There should not be more than S time slots
    between the start of the first class and the end of the
    last class on any given day."  This is a kind of avoid
    idle times constraint, applied to events rather than to
    resources (which for us is a detail).
      One task or meet set per day, and then a special function
    (span or something) to give the appropriate measure.  But
    how do you define one day?  By a time group.

    MinGap(G): Any two classes that are taught on the same day
    (they are placed on overlapping days and weeks) must be at
    least G slots apart.  Not sure what to make of this.
    I guess it's overlap(a, b, extension) where extension
    applies to both a and b.

    MaxDays(D): "Given classes cannot spread over more than D days
    of the week".  Just a straight cluster constraint.

    MaxDayLoad(S): "Given classes must be spread over the days
      of the week (and weeks) in a way that there is no more
      than a given number of S time slots on every day."  Just
      a straight limit busy times constraint, measuring durations.
      But not the full duration, rather the duration on one day.

      This is one of several indications that we cannot treat
      a non-atomic time as a unit in all cases.

    MaxBreaks(R,S): "MaxBreaks(R,S) This constraint limits the
      number of breaks during a day between a given set of classes
      (not more than R breaks during a day). For each day of week
      and week, there is a break between classes if there is more
      than S empty time slots in between."  A very interesting
      definition of what it means for two times to be consecutive.

    MaxBlock(M,S): "This constraint limits the length of a block
      of consecutive classes during a day (not more than M slots
      in a block). For each day of week and week, two consecutive
      classes are considered to be in the same block if the gap
      between them is not more than S time slots."  Limit active
      intervals, interpreted using durations rather than times.

  A resource r is busy at some time t if that time overlaps with
  any interval in any meet that r is attending.

  Need a way to define time *groups* to take advantage of symmetries.
  e.g. 1-15{MWF}3 = {1-15M3, 1-15W3, 1-15F3}.  All doubles:
  [Mon-Fr][12 & 23 & 45 & 67 & 78] or something.
  {MWF:<time>} or something.  But what is the whole day anyway?
  All intervals, presumably. {1-15:{MTWRF:1-8}

  See 16 April 2019 for things to do with the XUTT paper.

  It's not clear at the moment how time sweep should handle
  rematching.  If left as is, without lookahead, it might
  well undo all the good work done by lookahead.  But to
  add lookahead might be slow.  Start by turning it off:
  rs_time_sweep_rematch_off=true.  The same problem afflicts
  ejection chain repair during time sweep.  Needs thought.
  Can the lookahead stuff be made part of the solution cost?
  "If r is assigned t, add C to solution cost".  Not easily.
  It is like a temporary prefer resources monitor.

  Here's an idea for a repair:  if a sequence is too short, try
  moving it all to another resource where there is room to make
  it longer.  KheResourceUnderloadAugment will in fact do nothing
  at all in these cases, so we really do need to do something,
  even an ejecting move on that day.

  Working over INRC2-4-030-1-6753 generally, trying to improve
  the ejection chain repairs.  No luck so far.

  Resource swapping is really just resource rematching, only not
  as good.  That is, unless there are limit resources constraints.

  The last few ideas have been too small beer.  Must do better!
  Currently trying to improve KHE18's solutions to INRC2-4-035-2-8875.xml:

    1 = Early, 2 = Day, 3 = Late, 4 = Night
    FullTime: max 2 weekends, 15-22 shifts, consec 2-3 free 3-5 busy
    PartTime: max 2 weekends,  7-15 shifts, consec 2-5 free 3-5 busy
    HalfTime: max 1 weekends,  5-11 shifts, consec 3-5 free 3-5 busy
    All: unwanted [4][123], [3][12], complete weekends, single asst per day
    All: consec same shift days: Early 2-5, Day 2-28, Late 2-5, Night 4-5

    FullTime resources and the number of weekends they work in LOR are:
    
      NU_8 2, NU_9 1, CT_17 1, CT_18 0, CT_20 1, CT_25 1, TR_30 2, TR_32 3

    NB full-time can only work 20 shifts because of max 5 busy then
    min 2 free, e.g. 5-2-5-2-5-2-5-2 with 4*5 = 20 busy shifts.  But
    this as it stands is not viable because you work no weekends.  The
    opposite, 2-5-2-5-2-5-2-5 works 4 weekends which is no good either.
    Ideally you would want 5-2-5-4-5-2-5, which works 2 weekends, but
    the 4 free days are a defect.  More breaks is the only way to
    work 2 weekends, but that means a lower workload again.  This is
    why several of LOR's full-timers are working only 18 hours.  The
    conclusion is that trying to redistribute workload overloads is
    not going to help much.

    Resource types

    HeadNurse (HN_*) can also work as Nurse or Caretaker
    Nurse     (NU_*) can also work as Caretaker
    Caretaker (CT_*) works only as Caretaker
    Trainee   (TR_*) works only as Trainee

  "At least two days off after night shift" - if we recode this,
  we might do better on COI-BCDT-Sep.  But it's surprisingly hard.

  Option es_fresh_visits seems to be inconsistent, it causes
  things to become unvisited when there is an assumption that
  they are visited.  Needs looking into.  Currently commented
  out in khe_sr_combined.c.

  For the future:  time limit storing.  khe_sm_timer.c already
  has code for writing time limits, but not yet for reading.

  Work on time modelling paper for PATAT 2020.  The time model
  is an enabler for any projects I might do around ITC 2019,
  for example modelling student sectioning and implementing
  single student timetabling, so it is important for the future
  and needs to be got right.

  Time sets, time groups, resource sets, and resource groups
  ----------------------------------------------------------

    Thinking about whether I can remove construction of time
    neighbourhoods, by instead offering offset parameters on
    the time set operations (subset, etc.) which do the same.

    Need to use resource sets and time sets a lot more in the
    instance, for the constructed resource and time sets which
    in general have no name.  Maybe replace solution time groups
    and solution resource groups altogether.  But it's not
    trivial, because solution time groups are used by meets,
    and solution resource groups are used by tasks, both for
    handling domains (meet and task bounds).  What about

      typedef struct khe_time_set_rec {
          SSET elems;
      } KHE_TIME_SET;

    with SSET optimized by setting length to -1 to finalize.
    Most of the operations would have to be macros which
    add address-of operators in the style of SSET itself.

       KHE_TIME_SET KheTimeSetNeighbour(KHE_TIME_SET ts, int offset);

    would be doable with no memory allocation and one binary
    search (which could be optional for an internal version).

    I'm leaving this lie for now, something has to be done
    here but I'm not sure what, and there is no great hurry.

  There is a problem with preparing once and solving many times:
  adjustments for limit resources monitors depend on assignments
  in the vicinity, which may vary from one call to another.  The
  solution may well be simply to document the issue.

  At present resource matching is grouping then ungrouping during
  preparation, then grouping again when we start solving.  Can this
  be simplified?  There is a mark in the way.

  Document sset (which should really be khe_sset) and khe_set.

  I'm slightly worried that the comparison function for NRC
  worker constraints might have lost its transitivity now that
  history_after is being compared in some cases but not others.

  Look at the remaining special cases in all.map and see if some
  form of condensing can be applied to them.

  Might be a good idea to review the preserve_existing option in
  resource matching.  I don't exactly understand it at the moment.

  There seem to be several silly things in the current code that are
  about statistics.  I should think about collecting statistics in
  general, and implement something.  But not this time around.

  KheTaskFirstUnFixed is quite widely used, but I am beginning to
  are the same as mine (which GOAL's are not)?  If so I need
  to compare my results with theirs.  The paper is in the 2012
  PATAT proceedings, page 254.  Also it gives this site:

    https://www.kuleuven-kulak.be/nrpcompetition/competitor-ranking

  Can I find the results from the competition winner?  According to
  Santos et al. this was Valouxis et al, but their paper is in EJOR.

  Add code for limit resources monitors to khe_se_secondary.c.

  In KheClusterBusyTimesAugment, no use is being made of the
  allow_zero option at the moment.  Need to do this some time.

  Generalize the handling of the require_zero parameter of
  KheOverloadAugment, by allowing an ejection tree repair
  when the ejector depth is 1.  There is something like
  this already in KheClusterOverloadAugment, so look at
  that before doing anything else.

  There is an "Augment functions" section of the ejection chains
  chapter of the KHE guide that will need an update - do it last.

  (KHE) What about a general audit of how monitors report what
  is defective, with a view to finding a general rule for how
  to do this, and unifying all the monitors under that rule?
  The rule could be to store reported_deviation, renaming it
  to deviation, and to calculate a delta on that and have a
  function which applies the delta.  Have to look through all
  the monitors to see how that is likely to pan out.  But the
  general idea of a delta on the deviation does seem to be
  right, given that we want evaluation to be incremental.

  (KHE) For all monitors, should I include attached and unattached
  in the deviation function, so that attachment and unattachment
  are just like any other update functions?

  Ejection chains idea:  include main loop defect ejection trees
  in the major schedule, so that, at the end when main loop defects
  have resisted all previous attempts to repair them, we can try
  ejection trees on each in turn.  Make one change, produce several
  defects, and try to repair them all.  A good last resort?

  Ejection chains idea:  instead of requiring an ejection chain
  to improve the solution by at least (0, 1), require it to
  improve it by a larger amount, at first.  This will run much
  faster and will avoid trying to fix tiny problems until there
  is nothing better to do.  But have I already tried it?  It
  sounds a lot like es_limit_defects.
