survival.bend

unified-nsga-iii@0.2.1.0 · 1,094 lines · 0 definitions

1# Survival / niching — C# ReferencePointManager + NondominatedSortingSurvival.
2# Independent maps (associate / histograms / gather) use mid-split
3# `a b = f(lo) f(hi)`. Nearest-ref stays sequential (associate_raw already
4# splits per individual). Last-front fill builds `NRow{i, ref, dist}` once
5# so the sequential niche loop never `List.get`s an Assignment (or a fat
6# Individual) per remaining candidate. Same pick rules / same RNG order.
7# Associate normalized individuals to Das–Dennis rays; select `target_size`
8# from a combined pool (fill complete fronts, niche-preserve the last).
9# Deterministic when C# rng is null (`select`): lowest ref among min-count
10# niches, then min perpendicular distance if that niche is empty, else
11# lowest index. `select_st` (Run) threads rng: random among equal
12# min-count niches (C# Select(..., rng)). Empty niches take the closest
13# unused member; extras draw among members within 2×best+0.01 of the
14# ray — not uniform inNiche[rng.Next], which collapsed oracle ZDT2.
15
16import Base
17import unified-nsga-iii@0.2.1.0/individual.bend as Ind
18import unified-nsga-iii@0.2.1.0/non_dominated_sort.bend as NDS
19import unified-nsga-iii@0.2.1.0/normalization.bend as Norm
20import unified-nsga-iii@0.2.1.0/rng.bend as Rng
21import unified-nsga-iii@0.2.1.0/util.bend as Util
22
23type Assignment is Data:
24  MkAssignment{
25    reference_index: Nat
26    perpendicular_distance: F32
27    niche_count: Nat
28  }
29
30def asg_ref(a: Assignment) -> Nat:
31  match a:
32    case MkAssignment{reference_index, perpendicular_distance, niche_count}:
33      reference_index
34
35def asg_dist(a: Assignment) -> F32:
36  match a:
37    case MkAssignment{reference_index, perpendicular_distance, niche_count}:
38      perpendicular_distance
39
40def asg_count(a: Assignment) -> Nat:
41  match a:
42    case MkAssignment{reference_index, perpendicular_distance, niche_count}:
43      niche_count
44
45def get_asg(asgs: List<&2, Assignment>, i: Nat) -> Assignment:
46  Maybe.default(&2, Assignment, List.get(&2, Assignment, asgs, i), MkAssignment{0n, 0.0, 0n})
47
48def get_ind(+pop: List<&2, Ind.Individual>, i: Nat) -> Ind.Individual:
49  Maybe.default(&2, Ind.Individual, List.get(&2, Ind.Individual, pop, i), Ind.from_objectives(Nil{}))
50
51# Last-front row: population index plus the two fields the fill loop
52# reads. Built once from `asgs` so each pick walks this thin list
53# instead of `List.get` into the combined-pool assignment spine.
54type NRow is Data:
55  NRow{
56    i: Nat
57    r: Nat
58    d: F32
59  }
60
61def nrow_i(row: NRow) -> Nat:
62  match row:
63    case NRow{i, r, d}:
64      i
65
66def nrow_r(row: NRow) -> Nat:
67  match row:
68    case NRow{i, r, d}:
69      r
70
71def nrow_d(row: NRow) -> F32:
72  match row:
73    case NRow{i, r, d}:
74      d
75
76def nrows_of.go(+asgs: List<&2, Assignment>, cand: List<&2, Nat>, acc: List<&2, NRow>) -> List<&2, NRow>:
77  match cand:
78    case Nil{}:
79      List.reverse(&2, NRow, acc)
80    case +i <> rest:
81      +a = get_asg(asgs, i)
82      nrows_of.go(asgs, rest, NRow{i, asg_ref(a), asg_dist(a)} <> acc)
83
84def nrows_of(+asgs: List<&2, Assignment>, +cand: List<&2, Nat>) -> List<&2, NRow>:
85  nrows_of.go(asgs, cand, Nil{})
86
87def first_row_i(rows: List<&2, NRow>) -> Nat:
88  match rows:
89    case Nil{}:
90      0n
91    case +row <> _:
92      nrow_i(row)
93
94def first_row_r(rows: List<&2, NRow>) -> Nat:
95  match rows:
96    case Nil{}:
97      0n
98    case +row <> _:
99      nrow_r(row)
100
101def get_nrow(rows: List<&2, NRow>, i: Nat) -> NRow:
102  Maybe.default(&2, NRow, List.get(&2, NRow, rows, i), NRow{0n, 0n, 0.0})
103
104def drop_nrow.put(+row: NRow, rest: List<&2, NRow>, drop: Bool) -> List<&2, NRow>:
105  match drop:
106    case True{}:
107      rest
108    case False{}:
109      row <> rest
110
111def drop_nrow(rows: List<&2, NRow>, +pick: Nat) -> List<&2, NRow>:
112  match rows:
113    case Nil{}:
114      Nil{}
115    case +row <> rest:
116      drop_nrow.put(row, drop_nrow(rest, pick), Nat.is_eq(nrow_i(row), pick))
117
118def keep_nat(h: Nat, rest: List<&2, Nat>, keep: Bool) -> List<&2, Nat>:
119  match keep:
120    case True{}:
121      h <> rest
122    case False{}:
123      rest
124
125def uniq_refs.go(rows: List<&2, NRow>, +acc: List<&2, Nat>) -> List<&2, Nat>:
126  match rows:
127    case Nil{}:
128      List.reverse(&2, Nat, acc)
129    case +row <> rest:
130      +r = nrow_r(row)
131      uniq_refs.go(rest, keep_nat(r, acc, Bool.not(Util.in_nats(r, acc))))
132
133def uniq_refs(rows: List<&2, NRow>) -> List<&2, Nat>:
134  uniq_refs.go(rows, Nil{})
135
136def min_count_rows.go(+counts: List<&2, Nat>, rows: List<&2, NRow>, +acc: Nat) -> Nat:
137  match rows:
138    case Nil{}:
139      acc
140    case +row <> rest:
141      min_count_rows.go(counts, rest, Util.nat_min(acc, Util.get_nat(counts, nrow_r(row))))
142
143def min_count_rows(+counts: List<&2, Nat>, rows: List<&2, NRow>) -> Nat:
144  min_count_rows.go(counts, rows, 4294967295n)
145
146def refs_at_seq(+counts: List<&2, Nat>, +need: Nat, refs: List<&2, Nat>) -> List<&2, Nat>:
147  match refs:
148    case Nil{}:
149      Nil{}
150    case +r <> rest:
151      keep_nat(r, refs_at_seq(counts, need, rest), Nat.is_eq(Util.get_nat(counts, r), need))
152
153def in_ref_rows(+r: Nat, rows: List<&2, NRow>) -> List<&2, NRow>:
154  match rows:
155    case Nil{}:
156      Nil{}
157    case +row <> rest:
158      Util.keep_if(NRow, row, in_ref_rows(r, rest), Nat.is_eq(nrow_r(row), r))
159
160def argmin_dist_rows.go(rows: List<&2, NRow>, +best_i: Nat, +best_d: F32) -> Nat:
161  match rows:
162    case Nil{}:
163      best_i
164    case +row <> rest:
165      +d = nrow_d(row)
166      +lt = F32.is_lt(d, best_d)
167      argmin_dist_rows.go(rest, Bool.pick(Nat, lt, nrow_i(row), best_i), Bool.pick(F32, lt, d, best_d))
168
169def argmin_dist_rows(rows: List<&2, NRow>) -> Nat:
170  match rows:
171    case Nil{}:
172      0n
173    case +row <> rest:
174      argmin_dist_rows.go(rest, nrow_i(row), nrow_d(row))
175
176def min_dist_rows.go(rows: List<&2, NRow>, +acc: F32) -> F32:
177  match rows:
178    case Nil{}:
179      acc
180    case +row <> rest:
181      +d = nrow_d(row)
182      min_dist_rows.go(rest, Bool.pick(F32, F32.is_lt(d, acc), d, acc))
183
184def min_dist_rows(rows: List<&2, NRow>) -> F32:
185  match rows:
186    case Nil{}:
187      0.0
188    case +row <> rest:
189      min_dist_rows.go(rest, nrow_d(row))
190
191def min_idx_rows.go(rows: List<&2, NRow>, +acc: Nat) -> Nat:
192  match rows:
193    case Nil{}:
194      acc
195    case +row <> rest:
196      min_idx_rows.go(rest, Util.nat_min(acc, nrow_i(row)))
197
198def min_idx_rows(rows: List<&2, NRow>) -> Nat:
199  match rows:
200    case Nil{}:
201      0n
202    case +row <> rest:
203      min_idx_rows.go(rest, nrow_i(row))
204
205def near_rows(+lim: F32, rows: List<&2, NRow>) -> List<&2, NRow>:
206  match rows:
207    case Nil{}:
208      Nil{}
209    case +row <> rest:
210      Util.keep_if(NRow, row, near_rows(lim, rest), F32.is_le(nrow_d(row), lim))
211
212def perp.big(+f: List<&2, F32>, +w: List<&2, F32>, +ww: F32) -> F32:
213  +t = F32.div(Util.vec_dot(f, w), ww)
214  F32.sqrt(Util.vec_sqnorm(Util.vec_sub(f, Util.vec_scale(t, w))))
215
216def perp.zero(+f: List<&2, F32>, tiny: Bool) -> F32:
217  match tiny:
218    case True{}:
219      F32.sqrt(Util.vec_sqnorm(f))
220    case False{}:
221      0.0
222
223def perp.pick(+f: List<&2, F32>, +w: List<&2, F32>, +ww: F32, tiny: Bool) -> F32:
224  match tiny:
225    case True{}:
226      perp.zero(f, True{})
227    case False{}:
228      perp.big(f, w, ww)
229
230def perp(+f: List<&2, F32>, +w: List<&2, F32>) -> F32:
231  +ww = Util.vec_sqnorm(w)
232  perp.pick(f, w, ww, F32.is_lt(ww, 0.0000000000000001))
233
234# First-min on strict `<` (ties keep the lower index). Sequential over
235# dirs; associate_raw already mid-splits per individual.
236def nearest_go(+f: List<&2, F32>, dirs: List<&2, List<&2, F32>>, +i: Nat, +best_i: Nat, +best_d: F32) -> Assignment:
237  match dirs:
238    case Nil{}:
239      MkAssignment{best_i, best_d, 0n}
240    case +w <> ws:
241      +d = perp(f, w)
242      nearest_go(
243        f,
244        ws,
245        1n+i,
246        Bool.pick(Nat, F32.is_lt(d, best_d), i, best_i),
247        Bool.pick(F32, F32.is_lt(d, best_d), d, best_d)
248      )
249
250def nearest_one(f: List<&2, F32>, dirs: List<&2, List<&2, F32>>) -> Assignment:
251  nearest_go(f, dirs, 0n, 0n, 1000000000.0)
252
253def associate_head(+dirs: List<&2, List<&2, F32>>, +ind: Ind.Individual, F: List<&2, List<&2, F32>>) -> Assignment:
254  match F:
255    case Nil{}:
256      nearest_one(Ind.objectives(ind), dirs)
257    case f <> fs:
258      nearest_one(f, dirs)
259
260# Nearest-ref per individual is independent given dirs. Mid-split pop and F.
261def associate_raw.go(
262  +dirs: List<&2, List<&2, F32>>,
263  +fuel: Nat,
264  pop: List<&2, Ind.Individual>,
265  +F: List<&2, List<&2, F32>>
266) -> List<&2, Assignment>:
267  match fuel pop:
268    case 0n Nil{}:
269      Nil{}
270    case 0n ind <> rest:
271      match F:
272        case Nil{}:
273          nearest_one(Ind.objectives(ind), dirs) <> associate_raw.go(dirs, 0n, rest, Nil{})
274        case f <> fs:
275          nearest_one(f, dirs) <> associate_raw.go(dirs, 0n, rest, fs)
276    case 1n+p Nil{}:
277      Nil{}
278    case 1n+p ind <> Nil{}:
279      associate_head(dirs, ind, F) <> Nil{}
280    case 1n+p h <> t:
281      +all = {h <> t : List<&2, Ind.Individual>}
282      +mid = Nat.div(Util.list_len(Ind.Individual, all), 2n)
283      lo hi = associate_raw.go(dirs, p, List.take(&2, Ind.Individual, all, mid), List.take(&2, List<&2, F32>, F, mid)) associate_raw.go(dirs, p, List.drop(&2, Ind.Individual, all, mid), List.drop(&2, List<&2, F32>, F, mid))
284      List.append(&2, Assignment, lo, hi)
285
286def associate_raw(+pop: List<&2, Ind.Individual>, +F: List<&2, List<&2, F32>>, +dirs: List<&2, List<&2, F32>>) -> List<&2, Assignment>:
287  associate_raw.go(dirs, Util.list_len(Ind.Individual, pop), pop, F)
288
289# Histogram increments commute. Mid-split asgs, then add count vectors.
290def count_raw.go(+n: Nat, +fuel: Nat, asgs: List<&2, Assignment>) -> List<&2, Nat>:
291  match fuel asgs:
292    case 0n Nil{}:
293      Util.zeros_nat(n)
294    case 0n a <> rest:
295      Util.inc_at(count_raw.go(n, 0n, rest), asg_ref(a))
296    case 1n+p Nil{}:
297      Util.zeros_nat(n)
298    case 1n+p a <> Nil{}:
299      Util.inc_at(Util.zeros_nat(n), asg_ref(a))
300    case 1n+p h <> t:
301      +all = {h <> t : List<&2, Assignment>}
302      +mid = Nat.div(Util.list_len(Assignment, all), 2n)
303      lo hi = count_raw.go(n, p, List.take(&2, Assignment, all, mid)) count_raw.go(n, p, List.drop(&2, Assignment, all, mid))
304      Util.nats_add(lo, hi)
305
306def count_asg(+asgs: List<&2, Assignment>, +acc: List<&2, Nat>) -> List<&2, Nat>:
307  Util.nats_add(acc, count_raw.go(Util.list_len(Nat, acc), Util.list_len(Assignment, asgs), asgs))
308
309# Assignments whose reference_index lands in a length-n histogram.
310# Out-of-range refs are silent no-ops in inc_at / List.set.
311def in_bin_count(asgs: List<&2, Assignment>, +n: Nat) -> Nat:
312  match asgs:
313    case Nil{}:
314      0n
315    case +a <> rest:
316      Nat.add(Util.bin_hit(asg_ref(a), n), in_bin_count(rest, n))
317
318def with_counts.one(+counts: List<&2, Nat>, +a: Assignment) -> Assignment:
319  match a:
320    case MkAssignment{+r, d, n}:
321      MkAssignment{r, d, Util.get_nat(counts, r)}
322
323def with_counts(+counts: List<&2, Nat>, +asgs: List<&2, Assignment>) -> List<&2, Assignment>:
324  Util.map_halves(~Assignment, ~Assignment, ~List<&2, Nat>, ~with_counts.one, counts, asgs)
325
326def associate(+pop: List<&2, Ind.Individual>, +F: List<&2, List<&2, F32>>, +dirs: List<&2, List<&2, F32>>) -> List<&2, Assignment>:
327  +raw = associate_raw(pop, F, dirs)
328  with_counts(count_asg(raw, Util.zeros_nat(Util.list_len(List<&2, F32>, dirs))), raw)
329
330def front0(fronts: List<&2, List<&2, Nat>>) -> List<&2, Nat>:
331  match fronts:
332    case Nil{}:
333      Nil{}
334    case h <> t:
335      h
336
337def n_fronts(fronts: List<&2, List<&2, Nat>>) -> Nat:
338  Util.list_len(List<&2, Nat>, fronts)
339
340def head_len(fronts: List<&2, List<&2, Nat>>) -> Nat:
341  match fronts:
342    case Nil{}:
343      0n
344    case h <> t:
345      Util.list_len(Nat, h)
346
347def gather.one(+pop: List<&2, Ind.Individual>, +i: Nat) -> Ind.Individual:
348  get_ind(pop, i)
349
350def gather(+pop: List<&2, Ind.Individual>, +idxs: List<&2, Nat>) -> List<&2, Ind.Individual>:
351  Util.map_halves(~Nat, ~Ind.Individual, ~List<&2, Ind.Individual>, ~gather.one, pop, idxs)
352
353def cand_ref.one(+asgs: List<&2, Assignment>, +i: Nat) -> Nat:
354  asg_ref(get_asg(asgs, i))
355
356def cand_refs(+asgs: List<&2, Assignment>, cand: List<&2, Nat>) -> List<&2, Nat>:
357  Util.map_halves(~Nat, ~Nat, ~List<&2, Assignment>, ~cand_ref.one, asgs, cand)
358
359def min_count.go(+counts: List<&2, Nat>, +fuel: Nat, refs: List<&2, Nat>) -> Nat:
360  match fuel refs:
361    case 0n Nil{}:
362      4294967295n
363    case 0n +r <> rs:
364      Util.nat_min(Util.get_nat(counts, r), min_count.go(counts, 0n, rs))
365    case 1n+p Nil{}:
366      4294967295n
367    case 1n+p +r <> Nil{}:
368      Util.get_nat(counts, r)
369    case 1n+p h <> t:
370      +all = {h <> t : List<&2, Nat>}
371      +mid = Nat.div(Util.list_len(Nat, all), 2n)
372      lo hi = min_count.go(counts, p, List.take(&2, Nat, all, mid)) min_count.go(counts, p, List.drop(&2, Nat, all, mid))
373      Util.nat_min(lo, hi)
374
375def min_count(+counts: List<&2, Nat>, refs: List<&2, Nat>, acc: Nat) -> Nat:
376  match refs:
377    case Nil{}:
378      acc
379    case +r <> +rs:
380      +xs = {r <> rs : List<&2, Nat>}
381      Util.nat_min(acc, min_count.go(counts, Util.list_len(Nat, xs), xs))
382
383def cons_if(h: Nat, rest: List<&2, Nat>, keep: Bool) -> List<&2, Nat>:
384  match keep:
385    case True{}:
386      h <> rest
387    case False{}:
388      rest
389
390# First-occurrence unique (C# HashSet of refs-with-candidates).
391def uniq_nats.go(xs: List<&2, Nat>, +acc: List<&2, Nat>) -> List<&2, Nat>:
392  match xs:
393    case Nil{}:
394      List.reverse(&2, Nat, acc)
395    case +h <> t:
396      uniq_nats.go(t, cons_if(h, acc, Bool.not(Util.in_nats(h, acc))))
397
398def uniq_nats(xs: List<&2, Nat>) -> List<&2, Nat>:
399  uniq_nats.go(xs, Nil{})
400
401type CntNeed is Data:
402  CN{
403    counts: List<&2, Nat>
404    need: Nat
405  }
406
407def refs_at.ok(+ctx: CntNeed, +r: Nat) -> Bool:
408  match ctx:
409    case CN{counts, need}:
410      Nat.is_eq(Util.get_nat(counts, r), need)
411
412def refs_at(+counts: List<&2, Nat>, +need: Nat, refs: List<&2, Nat>) -> List<&2, Nat>:
413  Util.filter_halves(~Nat, ~CntNeed, ~refs_at.ok, CN{counts, need}, refs)
414
415def min_idx(xs: List<&2, Nat>, acc: Nat) -> Nat:
416  match xs:
417    case Nil{}:
418      acc
419    case +x <> rest:
420      min_idx(rest, Util.nat_min(acc, x))
421
422def min_idx_ne(xs: List<&2, Nat>) -> Nat:
423  match xs:
424    case Nil{}:
425      0n
426    case x <> rest:
427      min_idx(rest, x)
428
429def pick_ref(+asgs: List<&2, Assignment>, +counts: List<&2, Nat>, +cand: List<&2, Nat>) -> Nat:
430  +refs = cand_refs(asgs, cand)
431  min_idx_ne(refs_at(counts, min_count(counts, refs, 4294967295n), refs))
432
433type RefPick is Data:
434  RP{
435    asgs: List<&2, Assignment>
436    r: Nat
437  }
438
439def in_ref.ok(+ctx: RefPick, +i: Nat) -> Bool:
440  match ctx:
441    case RP{asgs, r}:
442      Nat.is_eq(asg_ref(get_asg(asgs, i)), r)
443
444def in_ref(+asgs: List<&2, Assignment>, +r: Nat, cand: List<&2, Nat>) -> List<&2, Nat>:
445  Util.filter_halves(~Nat, ~RefPick, ~in_ref.ok, RP{asgs, r}, cand)
446
447def argmin_join(+asgs: List<&2, Assignment>, +ia: Nat, +ib: Nat) -> Nat:
448  Bool.pick(Nat, F32.is_lt(asg_dist(get_asg(asgs, ib)), asg_dist(get_asg(asgs, ia))), ib, ia)
449
450def argmin_dist.go(+asgs: List<&2, Assignment>, +fuel: Nat, cand: List<&2, Nat>) -> Nat:
451  match fuel cand:
452    case 0n Nil{}:
453      0n
454    case 0n +i <> Nil{}:
455      i
456    case 0n +i <> +j <> js:
457      argmin_join(asgs, i, argmin_dist.go(asgs, 0n, j <> js))
458    case 1n+p Nil{}:
459      0n
460    case 1n+p +i <> Nil{}:
461      i
462    case 1n+p h <> t:
463      +all = {h <> t : List<&2, Nat>}
464      +mid = Nat.div(Util.list_len(Nat, all), 2n)
465      lo hi = argmin_dist.go(asgs, p, List.take(&2, Nat, all, mid)) argmin_dist.go(asgs, p, List.drop(&2, Nat, all, mid))
466      argmin_join(asgs, lo, hi)
467
468def argmin_dist(+asgs: List<&2, Assignment>, cand: List<&2, Nat>) -> Nat:
469  match cand:
470    case Nil{}:
471      0n
472    case +i <> +rest:
473      +xs = {i <> rest : List<&2, Nat>}
474      argmin_dist.go(asgs, Util.list_len(Nat, xs), xs)
475
476def pick_member.by(+asgs: List<&2, Assignment>, group: List<&2, Nat>, empty_niche: Bool) -> Nat:
477  match empty_niche:
478    case True{}:
479      argmin_dist(asgs, group)
480    case False{}:
481      min_idx_ne(group)
482
483# First remaining last-front index. Used when inNiche is empty
484# (C# would throw; never pick a phantom combined-pool index 0).
485def first_cand(cand: List<&2, Nat>) -> Nat:
486  match cand:
487    case Nil{}:
488      0n
489    case x <> _:
490      x
491
492def pick_member.by.safe(+asgs: List<&2, Assignment>, group: List<&2, Nat>, empty_niche: Bool, +fallback: Nat) -> Nat:
493  match group:
494    case Nil{}:
495      fallback
496    case g0 <> gs:
497      pick_member.by(asgs, {g0 <> gs : List<&2, Nat>}, empty_niche)
498
499def pick_member(+asgs: List<&2, Assignment>, +counts: List<&2, Nat>, +cand: List<&2, Nat>, +r: Nat) -> Nat:
500  pick_member.by.safe(asgs, in_ref(asgs, r, cand), Nat.is_eq(Util.get_nat(counts, r), 0n), first_cand(cand))
501
502# Affine API: advanced State; chosen index is `Rng.last_nat` (no pair match).
503# C# Select(..., rng): draw only when more than one min-count ref.
504def pick_ref.rng.go(+mins: List<&2, Nat>, n: Nat, +fallback: Nat, rng: Rng.State) -> Rng.State:
505  match n:
506    case 0n:
507      Rng.with_k(rng, fallback)
508    case 1n:
509      Rng.with_k(rng, min_idx_ne(mins))
510    case 1n+p:
511      +rng2 = Rng.next_nat(rng, 1n+p)
512      Rng.with_k(rng2, Util.get_nat(mins, Rng.last_nat(rng2)))
513
514def pick_ref.rng.after(+mins: List<&2, Nat>, +fallback: Nat, rng: Rng.State) -> Rng.State:
515  pick_ref.rng.go(mins, Util.list_len(Nat, mins), fallback, rng)
516
517def pick_ref.rng(+asgs: List<&2, Assignment>, +counts: List<&2, Nat>, +cand: List<&2, Nat>, rng: Rng.State) -> Rng.State:
518  +refs = uniq_nats(cand_refs(asgs, cand))
519  pick_ref.rng.after(
520    refs_at(counts, min_count(counts, refs, 4294967295n), refs),
521    asg_ref(get_asg(asgs, first_cand(cand))),
522    rng
523  )
524
525# Uniform inNiche[rng.Next] (C# non-empty + rng) lets far-from-ray last-front
526# junk into a niche. With this LCG that pile-up dominates ZDT2's concave
527# front (x0 → 0). Draw extras only among members within 2×best+0.01 of
528# the ray (first assignment to a niche stays closest).
529def near_lim(+best: F32) -> F32:
530  F32.add(F32.mul(best, 2.0), 0.01)
531
532def near_ok(+asgs: List<&2, Assignment>, +i: Nat, +lim: F32) -> Bool:
533  F32.is_le(asg_dist(get_asg(asgs, i)), lim)
534
535type NearPick is Data:
536  NP{
537    asgs: List<&2, Assignment>
538    lim: F32
539  }
540
541def near_members.ok(+ctx: NearPick, +i: Nat) -> Bool:
542  match ctx:
543    case NP{asgs, lim}:
544      near_ok(asgs, i, lim)
545
546def near_members(+asgs: List<&2, Assignment>, group: List<&2, Nat>, +lim: F32) -> List<&2, Nat>:
547  Util.filter_halves(~Nat, ~NearPick, ~near_members.ok, NP{asgs, lim}, group)
548
549def pick_among.rng(+group: List<&2, Nat>, n: Nat, +fallback: Nat, rng: Rng.State) -> Rng.State:
550  match n:
551    case 0n:
552      Rng.with_k(rng, fallback)
553    case 1n:
554      Rng.with_k(rng, min_idx_ne(group))
555    case 1n+p:
556      +rng2 = Rng.next_nat(rng, 1n+p)
557      Rng.with_k(rng2, Util.get_nat(group, Rng.last_nat(rng2)))
558
559def pick_near.rng(+asgs: List<&2, Assignment>, +group: List<&2, Nat>, rng: Rng.State) -> Rng.State:
560  +near = near_members(asgs, group, near_lim(asg_dist(get_asg(asgs, argmin_dist(asgs, group)))))
561  pick_among.rng(near, Util.list_len(Nat, near), first_cand(group), rng)
562
563def pick_member.rng.empty(+asgs: List<&2, Assignment>, +group: List<&2, Nat>, empty_niche: Bool, rng: Rng.State) -> Rng.State:
564  match empty_niche:
565    case True{}:
566      Rng.with_k(rng, argmin_dist(asgs, group))
567    case False{}:
568      pick_near.rng(asgs, group, rng)
569
570def pick_member.rng.by(+asgs: List<&2, Assignment>, group: List<&2, Nat>, empty_niche: Bool, +fallback: Nat, rng: Rng.State) -> Rng.State:
571  match group:
572    case Nil{}:
573      Rng.with_k(rng, fallback)
574    case g0 <> gs:
575      pick_member.rng.empty(asgs, {g0 <> gs : List<&2, Nat>}, empty_niche, rng)
576
577def pick_member.rng(+asgs: List<&2, Assignment>, +counts: List<&2, Nat>, +cand: List<&2, Nat>, +r: Nat, rng: Rng.State) -> Rng.State:
578  pick_member.rng.by(
579    asgs,
580    in_ref(asgs, r, cand),
581    Nat.is_eq(Util.get_nat(counts, r), 0n),
582    first_cand(cand),
583    rng
584  )
585
586def seed_raw.go(+asgs: List<&2, Assignment>, +n: Nat, +fuel: Nat, selected: List<&2, Nat>) -> List<&2, Nat>:
587  match fuel selected:
588    case 0n Nil{}:
589      Util.zeros_nat(n)
590    case 0n +i <> rest:
591      Util.inc_at(seed_raw.go(asgs, n, 0n, rest), asg_ref(get_asg(asgs, i)))
592    case 1n+p Nil{}:
593      Util.zeros_nat(n)
594    case 1n+p +i <> Nil{}:
595      Util.inc_at(Util.zeros_nat(n), asg_ref(get_asg(asgs, i)))
596    case 1n+p h <> t:
597      +all = {h <> t : List<&2, Nat>}
598      +mid = Nat.div(Util.list_len(Nat, all), 2n)
599      lo hi = seed_raw.go(asgs, n, p, List.take(&2, Nat, all, mid)) seed_raw.go(asgs, n, p, List.drop(&2, Nat, all, mid))
600      Util.nats_add(lo, hi)
601
602def seed_counts(+asgs: List<&2, Assignment>, +selected: List<&2, Nat>, +acc: List<&2, Nat>) -> List<&2, Nat>:
603  Util.nats_add(acc, seed_raw.go(asgs, Util.list_len(Nat, acc), Util.list_len(Nat, selected), selected))
604
605# fuel, k, cand first so the match follows binder order and fuel shrinks.
606def pick_ref.rows(+counts: List<&2, Nat>, +rows: List<&2, NRow>) -> Nat:
607  +refs = uniq_refs(rows)
608  min_idx_ne(refs_at_seq(counts, min_count_rows(counts, rows), refs))
609
610def pick_member.by.ne.rows(+group: List<&2, NRow>, empty_niche: Bool) -> Nat:
611  match empty_niche:
612    case True{}:
613      argmin_dist_rows(group)
614    case False{}:
615      min_idx_rows(group)
616
617def pick_member.by.rows(group: List<&2, NRow>, empty_niche: Bool, +fallback: Nat) -> Nat:
618  match group:
619    case Nil{}:
620      fallback
621    case g0 <> gs:
622      pick_member.by.ne.rows({g0 <> gs : List<&2, NRow>}, empty_niche)
623
624def pick_member.rows(+counts: List<&2, Nat>, +rows: List<&2, NRow>, +r: Nat) -> Nat:
625  pick_member.by.rows(in_ref_rows(r, rows), Nat.is_eq(Util.get_nat(counts, r), 0n), first_row_i(rows))
626
627def pick_ref.rng.rows(+counts: List<&2, Nat>, +rows: List<&2, NRow>, rng: Rng.State) -> Rng.State:
628  +refs = uniq_refs(rows)
629  pick_ref.rng.after(
630    refs_at_seq(counts, min_count_rows(counts, rows), refs),
631    first_row_r(rows),
632    rng
633  )
634
635def pick_among.nrows(+rows: List<&2, NRow>, n: Nat, +fallback: Nat, rng: Rng.State) -> Rng.State:
636  match n:
637    case 0n:
638      Rng.with_k(rng, fallback)
639    case 1n:
640      Rng.with_k(rng, first_row_i(rows))
641    case 1n+p:
642      +rng2 = Rng.next_nat(rng, 1n+p)
643      Rng.with_k(rng2, nrow_i(get_nrow(rows, Rng.last_nat(rng2))))
644
645def pick_near.rng.rows(+group: List<&2, NRow>, rng: Rng.State) -> Rng.State:
646  +near = near_rows(near_lim(min_dist_rows(group)), group)
647  pick_among.nrows(near, Util.list_len(NRow, near), first_row_i(group), rng)
648
649def pick_member.rng.empty.rows(+group: List<&2, NRow>, empty_niche: Bool, rng: Rng.State) -> Rng.State:
650  match empty_niche:
651    case True{}:
652      Rng.with_k(rng, argmin_dist_rows(group))
653    case False{}:
654      pick_near.rng.rows(group, rng)
655
656def pick_member.rng.by.rows(group: List<&2, NRow>, empty_niche: Bool, +fallback: Nat, rng: Rng.State) -> Rng.State:
657  match group:
658    case Nil{}:
659      Rng.with_k(rng, fallback)
660    case g0 <> gs:
661      pick_member.rng.empty.rows({g0 <> gs : List<&2, NRow>}, empty_niche, rng)
662
663def pick_member.rng.rows(+counts: List<&2, Nat>, +rows: List<&2, NRow>, +r: Nat, rng: Rng.State) -> Rng.State:
664  pick_member.rng.by.rows(
665    in_ref_rows(r, rows),
666    Nat.is_eq(Util.get_nat(counts, r), 0n),
667    first_row_i(rows),
668    rng
669  )
670
671def niche_loop(fuel: Nat, k: Nat, rows: List<&2, NRow>, +counts: List<&2, Nat>, selected: List<&2, Nat>) -> List<&2, Nat>:
672  match fuel k rows:
673    case 0n kk r:
674      selected
675    case f 0n r:
676      selected
677    case f kk Nil{}:
678      selected
679    case 1n+f 1n+krest +r0 <> +rs:
680      +rows2 = {r0 <> rs : List<&2, NRow>}
681      +r = pick_ref.rows(counts, rows2)
682      +pick = pick_member.rows(counts, rows2, r)
683      niche_loop(
684        f,
685        krest,
686        drop_nrow(rows2, pick),
687        Util.inc_at(counts, r),
688        List.append(&2, Nat, selected, [pick])
689      )
690
691def fill(fuel: Nat, fronts: List<&2, List<&2, Nat>>, fits: Bool, +acc: List<&2, Nat>, +target: Nat, +asgs: List<&2, Assignment>, +n_dirs: Nat) -> List<&2, Nat>:
692  match fuel fronts fits:
693    case 0n fs p:
694      acc
695    case f Nil{} p:
696      acc
697    case 1n+f +fr <> +rest False{}:
698      +need = Nat.sub(target, Util.list_len(Nat, acc))
699      +rows = nrows_of(asgs, fr)
700      niche_loop(
701        1n+Util.list_len(NRow, rows),
702        need,
703        rows,
704        seed_counts(asgs, acc, Util.zeros_nat(n_dirs)),
705        acc
706      )
707    case 1n+f +fr <> +rest True{}:
708      +new_acc = List.append(&2, Nat, acc, fr)
709      fill(
710        f,
711        rest,
712        Nat.is_le(Nat.add(Util.list_len(Nat, new_acc), head_len(rest)), target),
713        new_acc,
714        target,
715        asgs,
716        n_dirs
717      )
718
719def fill.wrap(+fronts: List<&2, List<&2, Nat>>, +target: Nat, +asgs: List<&2, Assignment>, +n_dirs: Nat) -> List<&2, Nat>:
720  fill(
721    1n+n_fronts(fronts),
722    fronts,
723    Nat.is_le(head_len(fronts), target),
724    Nil{},
725    target,
726    asgs,
727    n_dirs
728  )
729
730def niche_loop.rng(fuel: Nat, k: Nat, rows: List<&2, NRow>, +counts: List<&2, Nat>, selected: List<&2, Nat>, rng: Rng.State) -> List<&2, Nat> & Rng.State:
731  match fuel k rows:
732    case 0n kk r:
733      (selected, rng)
734    case f 0n r:
735      (selected, rng)
736    case f kk Nil{}:
737      (selected, rng)
738    case 1n+f 1n+krest +r0 <> +rs:
739      +rows2 = {r0 <> rs : List<&2, NRow>}
740      +rng1 = pick_ref.rng.rows(counts, rows2, rng)
741      +r = Rng.last_nat(rng1)
742      +rng2 = pick_member.rng.rows(counts, rows2, r, rng1)
743      +pick = Rng.last_nat(rng2)
744      niche_loop.rng(
745        f,
746        krest,
747        drop_nrow(rows2, pick),
748        Util.inc_at(counts, r),
749        List.append(&2, Nat, selected, [pick]),
750        rng2
751      )
752
753def fill.rng(fuel: Nat, fronts: List<&2, List<&2, Nat>>, fits: Bool, +acc: List<&2, Nat>, +target: Nat, +asgs: List<&2, Assignment>, +n_dirs: Nat, rng: Rng.State) -> List<&2, Nat> & Rng.State:
754  match fuel fronts fits:
755    case 0n fs p:
756      (acc, rng)
757    case f Nil{} p:
758      (acc, rng)
759    case 1n+f +fr <> +rest False{}:
760      +need = Nat.sub(target, Util.list_len(Nat, acc))
761      +rows = nrows_of(asgs, fr)
762      niche_loop.rng(
763        1n+Util.list_len(NRow, rows),
764        need,
765        rows,
766        seed_counts(asgs, acc, Util.zeros_nat(n_dirs)),
767        acc,
768        rng
769      )
770    case 1n+f +fr <> +rest True{}:
771      +new_acc = List.append(&2, Nat, acc, fr)
772      fill.rng(
773        f,
774        rest,
775        Nat.is_le(Nat.add(Util.list_len(Nat, new_acc), head_len(rest)), target),
776        new_acc,
777        target,
778        asgs,
779        n_dirs,
780        rng
781      )
782
783def fill.wrap.rng(+fronts: List<&2, List<&2, Nat>>, +target: Nat, +asgs: List<&2, Assignment>, +n_dirs: Nat, rng: Rng.State) -> List<&2, Nat> & Rng.State:
784  fill.rng(
785    1n+n_fronts(fronts),
786    fronts,
787    Nat.is_le(head_len(fronts), target),
788    Nil{},
789    target,
790    asgs,
791    n_dirs,
792    rng
793  )
794
795def select_idx.with(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, +fronts: List<&2, List<&2, Nat>>, pair: Norm.State & List<&2, List<&2, F32>>) -> List<&2, Nat>:
796  (st, F) = pair
797  fill.wrap(fronts, target, associate(pop, F, dirs), Util.list_len(List<&2, F32>, dirs))
798
799def select_idx.big(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Nat>:
800  +fronts = NDS.sort(pop)
801  select_idx.with(pop, target, dirs, fronts, Norm.normalize_nd(Norm.new_state(0n), pop, front0(fronts)))
802
803def select_idx.dirs(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, empty_dirs: Bool) -> List<&2, Nat>:
804  match empty_dirs:
805    case True{}:
806      List.take(&2, Nat, List.range(Util.list_len(Ind.Individual, pop)), target)
807    case False{}:
808      select_idx.big(pop, target, dirs)
809
810def select_idx.by(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, small: Bool) -> List<&2, Nat>:
811  match small:
812    case True{}:
813      List.range(Util.list_len(Ind.Individual, pop))
814    case False{}:
815      select_idx.dirs(pop, target, dirs, List.is_empty(&2, List<&2, F32>, dirs))
816
817# --- constrained survival (C# NondominatedSortingSurvival) ---
818# An all-feasible pool stays on the paths above. A mixed or infeasible
819# pool niches only the feasible subset and fills a shortfall by ascending
820# CV (original index breaks ties). Fillers are not niche-linked.
821# All-infeasible does not move the hyperplane.
822
823type CvRow is Data:
824  CvRow{
825    i: Nat
826    ind: Ind.Individual
827  }
828
829def any_infeas.on(later: Bool, feas: Bool) -> Bool:
830  match feas:
831    case False{}:
832      True{}
833    case True{}:
834      later
835
836def any_infeas(pop: List<&2, Ind.Individual>) -> Bool:
837  match pop:
838    case Nil{}:
839      False{}
840    case h <> t:
841      any_infeas.on(any_infeas(t), Ind.feasible(h))
842
843def row_before(a: CvRow, b: CvRow) -> Bool:
844  match a b:
845    case CvRow{i, ind} CvRow{j, other}:
846      +ca = Ind.constraint_violation(ind)
847      +cb = Ind.constraint_violation(other)
848      Bool.or(F32.is_lt(ca, cb), Bool.and(F32.is_eq(ca, cb), Bool.not(Nat.is_le(j, i))))
849
850# Insertion sort step. One def: the recursive call is only the
851# `before = False` arm, and that arm's tail is the original rest.
852# `before = True` conses onto that same rest (no second insert).
853# phase 0 reads the next cons; phase 1 places the row. Fuel is 1+2|xs|.
854# Recursing through a tail that already contains `x`, then consing `x`
855# again, duplicates the row. Each smaller CV doubles the list. At the
856# constrained budgets that exponential spine overflows the native stack
857# (`memory fault (machine stack overflow?)` on OSY / C1-DTLZ1; TNK
858# fail-stops). `src/constrained_budget.bend` runs those budgets.
859def insert_row.go(fuel: Nat, phase: Nat, before: Bool, +x: CvRow, xs: List<&2, CvRow>, +h: CvRow) -> List<&2, CvRow>:
860  match fuel phase before:
861    case 0n p b:
862      x <> xs
863    case 1n+f 0n b:
864      match xs:
865        case Nil{}:
866          x <> Nil{}
867        case +head <> tail:
868          insert_row.go(f, 1n, row_before(x, head), x, tail, head)
869    case 1n+f 1n+ph True{}:
870      x <> h <> xs
871    case 1n+f 1n+ph False{}:
872      h <> insert_row.go(f, 0n, False{}, x, xs, h)
873
874def insert_row(+x: CvRow, +xs: List<&2, CvRow>) -> List<&2, CvRow>:
875  insert_row.go(Nat.add(1n, Nat.mul(2n, Util.list_len(CvRow, xs))), 0n, False{}, x, xs, x)
876
877def sort_rows(xs: List<&2, CvRow>) -> List<&2, CvRow>:
878  match xs:
879    case Nil{}:
880      Nil{}
881    case h <> t:
882      insert_row(h, sort_rows(t))
883
884def row_ind(row: CvRow) -> Ind.Individual:
885  match row:
886    case CvRow{i, ind}:
887      ind
888
889def rows_inds(xs: List<&2, CvRow>) -> List<&2, Ind.Individual>:
890  match xs:
891    case Nil{}:
892      Nil{}
893    case h <> t:
894      row_ind(h) <> rows_inds(t)
895
896def map_clear(xs: List<&2, Ind.Individual>) -> List<&2, Ind.Individual>:
897  match xs:
898    case Nil{}:
899      Nil{}
900    case h <> t:
901      Ind.clear_niche(h) <> map_clear(t)
902
903def take_clear(rows: List<&2, CvRow>, +n: Nat) -> List<&2, Ind.Individual>:
904  map_clear(List.take(&2, Ind.Individual, rows_inds(sort_rows(rows)), n))
905
906def append_fill(+kept: List<&2, Ind.Individual>, rows: List<&2, CvRow>, +target: Nat) -> List<&2, Ind.Individual>:
907  List.append(
908    &2,
909    Ind.Individual,
910    kept,
911    take_clear(rows, Nat.sub(target, Util.list_len(Ind.Individual, kept)))
912  )
913
914def unused_idxs(+used: List<&2, Nat>, +n: Nat) -> List<&2, Nat>:
915  Util.minus(List.range(n), used)
916
917# Length lock: |take(pad(core, want, n), want)| = want when want ≤ n.
918# Identity when core is already a unique subset of 0..n-1 of length want
919# (the fill / range / take paths used in A/B).
920def pad_idxs(+xs: List<&2, Nat>, +want: Nat, +n: Nat) -> List<&2, Nat>:
921  List.append(&2, Nat, xs, List.take(&2, Nat, unused_idxs(xs, n), Nat.sub(want, Util.list_len(Nat, xs))))
922
923def clip_idxs(+xs: List<&2, Nat>, +want: Nat) -> List<&2, Nat>:
924  List.take(&2, Nat, xs, want)
925
926def size_lock(+xs: List<&2, Nat>, +want: Nat, +n: Nat) -> List<&2, Nat>:
927  clip_idxs(List.append(&2, Nat, pad_idxs(xs, want, n), List.range(want)), want)
928
929def select_idx(+combined: List<&2, Ind.Individual>, +target_size: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Nat>:
930  +n = Util.list_len(Ind.Individual, combined)
931  +want = Util.nat_min(target_size, n)
932  size_lock(select_idx.by(combined, target_size, dirs, Nat.is_le(n, target_size)), want, n)
933
934def select_plain(+combined: List<&2, Ind.Individual>, +target_size: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Ind.Individual>:
935  gather(combined, select_idx(combined, target_size, dirs))
936
937# Feasible subset already fits: keep original order. Niching runs only
938# when that subset is longer than the slot count.
939def feas_keep.det.on(+feas: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, fits: Bool) -> List<&2, Ind.Individual>:
940  match fits:
941    case True{}:
942      feas
943    case False{}:
944      select_plain(feas, target, dirs)
945
946def feas_keep.det(+feas: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Ind.Individual>:
947  feas_keep.det.on(feas, target, dirs, Nat.is_le(Util.list_len(Ind.Individual, feas), target))
948
949type Split is Data:
950  Split{
951    feas: List<&2, Ind.Individual>
952    rows: List<&2, CvRow>
953  }
954
955def split_add.feas(+h: Ind.Individual, sp: Split) -> Split:
956  match sp:
957    case Split{feas, rows}:
958      Split{h <> feas, rows}
959
960def split_add.infeas(+h: Ind.Individual, +i: Nat, sp: Split) -> Split:
961  match sp:
962    case Split{feas, rows}:
963      Split{feas, CvRow{i, h} <> rows}
964
965def split_on(+h: Ind.Individual, rest: Split, +i: Nat, feas: Bool) -> Split:
966  match feas:
967    case True{}:
968      split_add.feas(h, rest)
969    case False{}:
970      split_add.infeas(h, i, rest)
971
972def split.go(pop: List<&2, Ind.Individual>, +i: Nat) -> Split:
973  match pop:
974    case Nil{}:
975      Split{Nil{}, Nil{}}
976    case +h <> t:
977      split_on(h, split.go(t, 1n+i), i, Ind.feasible(h))
978
979def select_constrained.feas(feas: List<&2, Ind.Individual>, +rows: List<&2, CvRow>, +target: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Ind.Individual>:
980  match feas:
981    case Nil{}:
982      take_clear(rows, target)
983    case h <> t:
984      append_fill(feas_keep.det(h <> t, target, dirs), rows, target)
985
986def select_constrained.from(sp: Split, +target: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Ind.Individual>:
987  match sp:
988    case Split{feas, rows}:
989      select_constrained.feas(feas, rows, target, dirs)
990
991def select_constrained(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Ind.Individual>:
992  select_constrained.from(split.go(pop, 0n), target, dirs)
993
994# All-feasible Run path. Defined before constrained fill so a feasible
995# subset that still needs niching calls this directly (no cycle through
996# select_st).
997def size_lock.rng(+want: Nat, +n: Nat, pair: List<&2, Nat> & Rng.State) -> List<&2, Nat> & Rng.State:
998  (xs, rng) = pair
999  (size_lock(xs, want, n), rng)
1000
1001def select_st.after(+st: Norm.State, +pop: List<&2, Ind.Individual>, pair: List<&2, Nat> & Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1002  (idxs, rng2) = pair
1003  (st, gather(pop, idxs), rng2)
1004
1005def select_st.with(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, +fronts: List<&2, List<&2, Nat>>, pair: Norm.State & List<&2, List<&2, F32>>, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1006  (st, F) = pair
1007  +n = Util.list_len(Ind.Individual, pop)
1008  +want = Util.nat_min(target, n)
1009  select_st.after(st, pop, size_lock.rng(want, n, fill.wrap.rng(fronts, target, associate(pop, F, dirs), Util.list_len(List<&2, F32>, dirs), rng)))
1010
1011def select_st.big(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1012  +fronts = NDS.sort(pop)
1013  select_st.with(pop, target, dirs, fronts, Norm.normalize_nd(state, pop, front0(fronts)), rng)
1014
1015def select_st.take(+pop: List<&2, Ind.Individual>, +target: Nat, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1016  +n = Util.list_len(Ind.Individual, pop)
1017  (state, gather(pop, size_lock(List.take(&2, Nat, List.range(n), target), Util.nat_min(target, n), n)), rng)
1018
1019def select_st.dirs(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State, empty_dirs: Bool) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1020  match empty_dirs:
1021    case True{}:
1022      select_st.take(pop, target, state, rng)
1023    case False{}:
1024      select_st.big(pop, target, dirs, state, rng)
1025
1026def feas_keep.st.fit.unpack(+feas: List<&2, Ind.Individual>, rng: Rng.State, pair: Norm.State & List<&2, List<&2, F32>>) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1027  (st, fs) = pair
1028  (st, feas, rng)
1029
1030def feas_keep.st.fit(+feas: List<&2, Ind.Individual>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1031  feas_keep.st.fit.unpack(feas, rng, Norm.normalize_nd(state, feas, front0(NDS.sort(feas))))
1032
1033def feas_keep.st.on(+feas: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State, fits: Bool) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1034  match fits:
1035    case True{}:
1036      feas_keep.st.fit(feas, state, rng)
1037    case False{}:
1038      select_st.dirs(feas, target, dirs, state, rng, List.is_empty(&2, List<&2, F32>, dirs))
1039
1040def feas_keep.st(+feas: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1041  feas_keep.st.on(feas, target, dirs, state, rng, Nat.is_le(Util.list_len(Ind.Individual, feas), target))
1042
1043def select_st.constrained.kept(+rows: List<&2, CvRow>, +target: Nat, trip: Norm.State & List<&2, Ind.Individual> & Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1044  (st, kept, rng2) = trip
1045  (st, append_fill(kept, rows, target), rng2)
1046
1047def select_st.constrained.feas(feas: List<&2, Ind.Individual>, +rows: List<&2, CvRow>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1048  match feas:
1049    case Nil{}:
1050      (state, take_clear(rows, target), rng)
1051    case h <> t:
1052      select_st.constrained.kept(rows, target, feas_keep.st(h <> t, target, dirs, state, rng))
1053
1054def select_st.constrained.from(sp: Split, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1055  match sp:
1056    case Split{feas, rows}:
1057      select_st.constrained.feas(feas, rows, target, dirs, state, rng)
1058
1059def select_st.constrained(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1060  select_st.constrained.from(split.go(pop, 0n), target, dirs, state, rng)
1061
1062def select.route(+combined: List<&2, Ind.Individual>, +target_size: Nat, +dirs: List<&2, List<&2, F32>>, constrained: Bool) -> List<&2, Ind.Individual>:
1063  match constrained:
1064    case True{}:
1065      select_constrained(combined, target_size, dirs)
1066    case False{}:
1067      select_plain(combined, target_size, dirs)
1068
1069# `select` stays the rng-null length-locked path (`Laws.select_size`).
1070# Constrained feasible-niching + CV fill is `select_st` (Run).
1071def select(+combined: List<&2, Ind.Individual>, +target_size: Nat, +dirs: List<&2, List<&2, F32>>) -> List<&2, Ind.Individual>:
1072  select_plain(combined, target_size, dirs)
1073
1074# Persistent-normalization Select for Unsga3Algorithm.Run. Threads rng
1075# so last-front niching matches C# Select(..., rng) on min-count niche
1076# ties. Empty niches take closest; extras are random among near-best
1077# on that ray. v0 `select` stays the `rng == null` path (empty laws).
1078def select_st.large(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State, bad: Bool) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1079  match bad:
1080    case False{}:
1081      select_st.dirs(pop, target, dirs, state, rng, List.is_empty(&2, List<&2, F32>, dirs))
1082    case True{}:
1083      select_st.constrained(pop, target, dirs, state, rng)
1084
1085def select_st.by(+pop: List<&2, Ind.Individual>, +target: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State, small: Bool) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1086  match small:
1087    case True{}:
1088      (state, pop, rng)
1089    case False{}:
1090      select_st.large(pop, target, dirs, state, rng, any_infeas(pop))
1091
1092def select_st(+combined: List<&2, Ind.Individual>, +target_size: Nat, +dirs: List<&2, List<&2, F32>>, state: Norm.State, rng: Rng.State) -> Norm.State & List<&2, Ind.Individual> & Rng.State:
1093  +n = Util.list_len(Ind.Individual, combined)
1094  select_st.by(combined, target_size, dirs, state, rng, Nat.is_le(n, target_size))