1use gam_math::probability::normal_cdf;
2use gam_runtime::resource::{ByteLruCache, ResidentBytes};
3use smallvec::{SmallVec, smallvec};
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8#[derive(Clone, Debug)]
17pub enum CubicCellKernelError {
18 InvalidInterval { reason: String },
21 InvalidCellShape { reason: String },
26 InsufficientMoments { reason: String },
29 BivariateNormalDomain { reason: String },
32}
33
34impl_reason_error_boilerplate! {
35 CubicCellKernelError {
36 InvalidInterval,
37 InvalidCellShape,
38 InsufficientMoments,
39 BivariateNormalDomain,
40 }
41}
42
43impl CubicCellKernelError {
44 #[inline]
45 fn invalid_interval(reason: impl Into<String>) -> Self {
46 CubicCellKernelError::InvalidInterval {
47 reason: reason.into(),
48 }
49 }
50 #[inline]
51 fn invalid_cell_shape(reason: impl Into<String>) -> Self {
52 CubicCellKernelError::InvalidCellShape {
53 reason: reason.into(),
54 }
55 }
56 #[inline]
57 fn insufficient_moments(reason: impl Into<String>) -> Self {
58 CubicCellKernelError::InsufficientMoments {
59 reason: reason.into(),
60 }
61 }
62 #[inline]
63 fn bivariate_normal_domain(reason: impl Into<String>) -> Self {
64 CubicCellKernelError::BivariateNormalDomain {
65 reason: reason.into(),
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
96pub struct LocalSpanCubic {
97 pub left: f64,
98 pub right: f64,
99 pub c0: f64,
100 pub c1: f64,
101 pub c2: f64,
102 pub c3: f64,
103}
104
105impl LocalSpanCubic {
106 #[inline]
107 pub fn evaluate(self, x: f64) -> f64 {
108 let t = x - self.left;
109 self.c0 + self.c1 * t + self.c2 * t * t + self.c3 * t * t * t
110 }
111
112 #[inline]
113 pub fn first_derivative(self, x: f64) -> f64 {
114 let t = x - self.left;
115 self.c1 + 2.0 * self.c2 * t + 3.0 * self.c3 * t * t
116 }
117
118 #[inline]
119 pub fn second_derivative(self, x: f64) -> f64 {
120 let t = x - self.left;
121 2.0 * self.c2 + 6.0 * self.c3 * t
122 }
123}
124
125pub const ANCHORED_DEVIATION_KERNEL: &str = "DenestedCubicTransport";
126pub const NORMALIZED_CELL_BRANCH_TOL: f64 = 1e-10;
134
135const INV_TWO_PI: f64 = 1.0 / std::f64::consts::TAU;
136
137#[cfg(target_os = "linux")]
141pub const GL_NODES_FOR_GPU_KERNEL: &[f64; 384] = &GL_NODES;
142#[cfg(target_os = "linux")]
144pub const GL_WEIGHTS_FOR_GPU_KERNEL: &[f64; 384] = &GL_WEIGHTS;
145
146const GL_NODES: [f64; 384] = [
147 -9.999_804_411_726_474e-1,
148 -9.998_969_471_378_596e-1,
149 -9.997_467_408_113_523e-1,
150 -9.995_297_988_558_859e-1,
151 -9.992_461_316_671_845e-1,
152 -9.988_957_572_063_257e-1,
153 -9.984_786_985_384_589e-1,
154 -9.979_949_833_727_938e-1,
155 -9.974_446_439_389_107e-1,
156 -9.968_277_169_440_913e-1,
157 -9.961_442_435_551_087e-1,
158 -9.953_942_693_885_953e-1,
159 -9.945_778_445_047_068e-1,
160 -9.936_950_234_020_883e-1,
161 -9.927_458_650_133_153e-1,
162 -9.917_304_327_004_32e-1,
163 -9.906_487_942_504_061e-1,
164 -9.895_010_218_704_087e-1,
165 -9.882_871_921_828_699e-1,
166 -9.870_073_862_202_815e-1,
167 -9.856_616_894_197_333e-1,
168 -9.842_501_916_171_713e-1,
169 -9.827_729_870_413_743e-1,
170 -9.812_301_743_076_443e-1,
171 -9.796_218_564_112_101e-1,
172 -9.779_481_407_203_411e-1,
173 -9.762_091_389_691_724e-1,
174 -9.744_049_672_502_397e-1,
175 -9.725_357_460_067_257e-1,
176 -9.706_016_000_244_151e-1,
177 -9.686_026_584_233_628e-1,
178 -9.665_390_546_492_71e-1,
179 -9.644_109_264_645_802e-1,
180 -9.622_184_159_392_698e-1,
181 -9.599_616_694_413_742e-1,
182 -9.576_408_376_272_095e-1,
183 -9.552_560_754_313_16e-1,
184 -9.528_075_420_561_144e-1,
185 -9.502_954_009_612_771e-1,
186 -9.477_198_198_528_157e-1,
187 -9.450_809_706_718_851e-1,
188 -9.423_790_295_833_044e-1,
189 -9.396_141_769_637_963e-1,
190 -9.367_865_973_899_459e-1,
191 -9.338_964_796_258_775e-1,
192 -9.309_440_166_106_54e-1,
193 -9.279_294_054_453_956e-1,
194 -9.248_528_473_801_222e-1,
195 -9.217_145_478_003_181e-1,
196 -9.185_147_162_132_208e-1,
197 -9.152_535_662_338_34e-1,
198 -9.119_313_155_706_682e-1,
199 -9.085_481_860_112_055e-1,
200 -9.051_044_034_070_944e-1,
201 -9.016_001_976_590_722e-1,
202 -8.980_358_027_016_164e-1,
203 -8.944_114_564_873_288e-1,
204 -8.907_274_009_710_492e-1,
205 -8.869_838_820_937_034e-1,
206 -8.831_811_497_658_847e-1,
207 -8.793_194_578_511_7e-1,
208 -8.753_990_641_491_725e-1,
209 -8.714_202_303_783_312e-1,
210 -8.673_832_221_584_393e-1,
211 -8.632_883_089_929_12e-1,
212 -8.591_357_642_507_945e-1,
213 -8.549_258_651_485_127e-1,
214 -8.506_588_927_313_666e-1,
215 -8.463_351_318_547_683e-1,
216 -8.419_548_711_652_254e-1,
217 -8.375_184_030_810_715e-1,
218 -8.330_260_237_729_452e-1,
219 -8.284_780_331_440_178e-1,
220 -8.238_747_348_099_726e-1,
221 -8.192_164_360_787_36e-1,
222 -8.145_034_479_299_62e-1,
223 -8.097_360_849_942_72e-1,
224 -8.049_146_655_322_506e-1,
225 -8.000_395_114_131_988e-1,
226 -7.951_109_480_936_471e-1,
227 -7.901_293_045_956_28e-1,
228 -7.850_949_134_847_117e-1,
229 -7.800_081_108_478_04e-1,
230 -7.748_692_362_707_1e-1,
231 -7.696_786_328_154_644e-1,
232 -7.644_366_469_974_285e-1,
233 -7.591_436_287_621_58e-1,
234 -7.537_999_314_620_412e-1,
235 -7.484_059_118_327_094e-1,
236 -7.429_619_299_692_227e-1,
237 -7.374_683_493_020_299e-1,
238 -7.319_255_365_727_068e-1,
239 -7.263_338_618_094_733e-1,
240 -7.206_936_983_024_912e-1,
241 -7.150_054_225_789_432e-1,
242 -7.092_694_143_778_975e-1,
243 -7.034_860_566_249_567e-1,
244 -6.976_557_354_066_943e-1,
245 -6.917_788_399_448_808e-1,
246 -6.858_557_625_704_99e-1,
247 -6.798_868_986_975_534e-1,
248 -6.738_726_467_966_731e-1,
249 -6.678_134_083_685_102e-1,
250 -6.617_095_879_169_366e-1,
251 -6.555_615_929_220_4e-1,
252 -6.493_698_338_129_212e-1,
253 -6.431_347_239_402_948e-1,
254 -6.368_566_795_488_945e-1,
255 -6.305_361_197_496_849e-1,
256 -6.241_734_664_918_837e-1,
257 -6.177_691_445_347_913e-1,
258 -6.113_235_814_194_364e-1,
259 -6.048_372_074_400_329e-1,
260 -5.983_104_556_152_549e-1,
261 -5.917_437_616_593_286e-1,
262 -5.851_375_639_529_456e-1,
263 -5.784_923_035_139_965e-1,
264 -5.718_084_239_681_3e-1,
265 -5.650_863_715_191_369e-1,
266 -5.583_265_949_191_623e-1,
267 -5.515_295_454_387_482e-1,
268 -5.446_956_768_367_068e-1,
269 -5.378_254_453_298_289e-1,
270 -5.309_193_095_624_275e-1,
271 -5.239_777_305_757_194e-1,
272 -5.170_011_717_770_473e-1,
273 -5.099_900_989_089_429e-1,
274 -5.029_449_800_180_356e-1,
275 -4.958_662_854_238_058_4e-1,
276 -4.887_544_876_871_878e-1,
277 -4.816_100_615_790_221e-1,
278 -4.744_334_840_483_605_5e-1,
279 -4.672_252_341_906_264e-1,
280 -4.599_857_932_156_304e-1,
281 -4.527_156_444_154_463_7e-1,
282 -4.454_152_731_321_473_5e-1,
283 -4.380_851_667_254_05e-1,
284 -4.307_258_145_399_544_5e-1,
285 -4.233_377_078_729_265e-1,
286 -4.159_213_399_410_494e-1,
287 -4.084_772_058_477_228e-1,
288 -4.010_058_025_499_653e-1,
289 -3.935_076_288_252_386e-1,
290 -3.859_831_852_381_500_6e-1,
291 -3.784_329_741_070_358_6e-1,
292 -3.708_574_994_704_271e-1,
293 -3.632_572_670_534_011e-1,
294 -3.556_327_842_338_202e-1,
295 -3.479_845_600_084_600_6e-1,
296 -3.403_131_049_590_297e-1,
297 -3.326_189_312_180_866e-1,
298 -3.249_025_524_348_469_5e-1,
299 -3.171_644_837_408_958_4e-1,
300 -3.094_052_417_157_978e-1,
301 -3.016_253_443_526_109e-1,
302 -2.938_253_110_233_064_5e-1,
303 -2.860_056_624_440_967_5e-1,
304 -2.781_669_206_406_729e-1,
305 -2.703_096_089_133_553e-1,
306 -2.624_342_518_021_592_4e-1,
307 -2.545_413_750_517_773e-1,
308 -2.466_315_055_764_817_5e-1,
309 -2.387_051_714_249_486_3e-1,
310 -2.307_629_017_450_062e-1,
311 -2.228_052_267_483_099_4e-1,
312 -2.148_326_776_749_466_5e-1,
313 -2.068_457_867_579_697_5e-1,
314 -1.988_450_871_878_683_4e-1,
315 -1.908_311_130_769_724_5e-1,
316 -1.828_043_994_237_965_6e-1,
317 -1.747_654_820_773_241_2e-1,
318 -1.667_148_977_012_352_4e-1,
319 -1.586_531_837_380_799_3e-1,
320 -1.505_808_783_733_995e-1,
321 -1.424_985_204_997_981_4e-1,
322 -1.344_066_496_809_674_7e-1,
323 -1.263_058_061_156_663e-1,
324 -1.181_965_306_016_578_4e-1,
325 -1.100_793_644_996_070_4e-1,
326 -1.019_548_496_969_403_7e-1,
327 -9.382_352_857_167_028e-2,
328 -8.568_594_395_618_719e-2,
329 -7.754_263_910_102_077e-2,
330 -6.939_415_763_857_37e-2,
331 -6.124_104_354_682_962e-2,
332 -5.308_384_111_303_817_6e-2,
333 -4.492_309_489_737_94e-2,
334 -3.675_934_969_660_982e-2,
335 -2.859_315_050_769_284_7e-2,
336 -2.042_504_249_141_571e-2,
337 -1.225_557_093_599_553_8e-2,
338 -4.085_281_220_676_868e-3,
339 4.085_281_220_676_868e-3,
340 1.225_557_093_599_553_8e-2,
341 2.042_504_249_141_571e-2,
342 2.859_315_050_769_284_7e-2,
343 3.675_934_969_660_982e-2,
344 4.492_309_489_737_94e-2,
345 5.308_384_111_303_817_6e-2,
346 6.124_104_354_682_962e-2,
347 6.939_415_763_857_37e-2,
348 7.754_263_910_102_077e-2,
349 8.568_594_395_618_719e-2,
350 9.382_352_857_167_028e-2,
351 1.019_548_496_969_403_7e-1,
352 1.100_793_644_996_070_4e-1,
353 1.181_965_306_016_578_4e-1,
354 1.263_058_061_156_663e-1,
355 1.344_066_496_809_674_7e-1,
356 1.424_985_204_997_981_4e-1,
357 1.505_808_783_733_995e-1,
358 1.586_531_837_380_799_3e-1,
359 1.667_148_977_012_352_4e-1,
360 1.747_654_820_773_241_2e-1,
361 1.828_043_994_237_965_6e-1,
362 1.908_311_130_769_724_5e-1,
363 1.988_450_871_878_683_4e-1,
364 2.068_457_867_579_697_5e-1,
365 2.148_326_776_749_466_5e-1,
366 2.228_052_267_483_099_4e-1,
367 2.307_629_017_450_062e-1,
368 2.387_051_714_249_486_3e-1,
369 2.466_315_055_764_817_5e-1,
370 2.545_413_750_517_773e-1,
371 2.624_342_518_021_592_4e-1,
372 2.703_096_089_133_553e-1,
373 2.781_669_206_406_729e-1,
374 2.860_056_624_440_967_5e-1,
375 2.938_253_110_233_064_5e-1,
376 3.016_253_443_526_109e-1,
377 3.094_052_417_157_978e-1,
378 3.171_644_837_408_958_4e-1,
379 3.249_025_524_348_469_5e-1,
380 3.326_189_312_180_866e-1,
381 3.403_131_049_590_297e-1,
382 3.479_845_600_084_600_6e-1,
383 3.556_327_842_338_202e-1,
384 3.632_572_670_534_011e-1,
385 3.708_574_994_704_271e-1,
386 3.784_329_741_070_358_6e-1,
387 3.859_831_852_381_500_6e-1,
388 3.935_076_288_252_386e-1,
389 4.010_058_025_499_653e-1,
390 4.084_772_058_477_228e-1,
391 4.159_213_399_410_494e-1,
392 4.233_377_078_729_265e-1,
393 4.307_258_145_399_544_5e-1,
394 4.380_851_667_254_05e-1,
395 4.454_152_731_321_473_5e-1,
396 4.527_156_444_154_463_7e-1,
397 4.599_857_932_156_304e-1,
398 4.672_252_341_906_264e-1,
399 4.744_334_840_483_605_5e-1,
400 4.816_100_615_790_221e-1,
401 4.887_544_876_871_878e-1,
402 4.958_662_854_238_058_4e-1,
403 5.029_449_800_180_356e-1,
404 5.099_900_989_089_429e-1,
405 5.170_011_717_770_473e-1,
406 5.239_777_305_757_194e-1,
407 5.309_193_095_624_275e-1,
408 5.378_254_453_298_289e-1,
409 5.446_956_768_367_068e-1,
410 5.515_295_454_387_482e-1,
411 5.583_265_949_191_623e-1,
412 5.650_863_715_191_369e-1,
413 5.718_084_239_681_3e-1,
414 5.784_923_035_139_965e-1,
415 5.851_375_639_529_456e-1,
416 5.917_437_616_593_286e-1,
417 5.983_104_556_152_549e-1,
418 6.048_372_074_400_329e-1,
419 6.113_235_814_194_364e-1,
420 6.177_691_445_347_913e-1,
421 6.241_734_664_918_837e-1,
422 6.305_361_197_496_849e-1,
423 6.368_566_795_488_945e-1,
424 6.431_347_239_402_948e-1,
425 6.493_698_338_129_212e-1,
426 6.555_615_929_220_4e-1,
427 6.617_095_879_169_366e-1,
428 6.678_134_083_685_102e-1,
429 6.738_726_467_966_731e-1,
430 6.798_868_986_975_534e-1,
431 6.858_557_625_704_99e-1,
432 6.917_788_399_448_808e-1,
433 6.976_557_354_066_943e-1,
434 7.034_860_566_249_567e-1,
435 7.092_694_143_778_975e-1,
436 7.150_054_225_789_432e-1,
437 7.206_936_983_024_912e-1,
438 7.263_338_618_094_733e-1,
439 7.319_255_365_727_068e-1,
440 7.374_683_493_020_299e-1,
441 7.429_619_299_692_227e-1,
442 7.484_059_118_327_094e-1,
443 7.537_999_314_620_412e-1,
444 7.591_436_287_621_58e-1,
445 7.644_366_469_974_285e-1,
446 7.696_786_328_154_644e-1,
447 7.748_692_362_707_1e-1,
448 7.800_081_108_478_04e-1,
449 7.850_949_134_847_117e-1,
450 7.901_293_045_956_28e-1,
451 7.951_109_480_936_471e-1,
452 8.000_395_114_131_988e-1,
453 8.049_146_655_322_506e-1,
454 8.097_360_849_942_72e-1,
455 8.145_034_479_299_62e-1,
456 8.192_164_360_787_36e-1,
457 8.238_747_348_099_726e-1,
458 8.284_780_331_440_178e-1,
459 8.330_260_237_729_452e-1,
460 8.375_184_030_810_715e-1,
461 8.419_548_711_652_254e-1,
462 8.463_351_318_547_683e-1,
463 8.506_588_927_313_666e-1,
464 8.549_258_651_485_127e-1,
465 8.591_357_642_507_945e-1,
466 8.632_883_089_929_12e-1,
467 8.673_832_221_584_393e-1,
468 8.714_202_303_783_312e-1,
469 8.753_990_641_491_725e-1,
470 8.793_194_578_511_7e-1,
471 8.831_811_497_658_847e-1,
472 8.869_838_820_937_034e-1,
473 8.907_274_009_710_492e-1,
474 8.944_114_564_873_288e-1,
475 8.980_358_027_016_164e-1,
476 9.016_001_976_590_722e-1,
477 9.051_044_034_070_944e-1,
478 9.085_481_860_112_055e-1,
479 9.119_313_155_706_682e-1,
480 9.152_535_662_338_34e-1,
481 9.185_147_162_132_208e-1,
482 9.217_145_478_003_181e-1,
483 9.248_528_473_801_222e-1,
484 9.279_294_054_453_956e-1,
485 9.309_440_166_106_54e-1,
486 9.338_964_796_258_775e-1,
487 9.367_865_973_899_459e-1,
488 9.396_141_769_637_963e-1,
489 9.423_790_295_833_044e-1,
490 9.450_809_706_718_851e-1,
491 9.477_198_198_528_157e-1,
492 9.502_954_009_612_771e-1,
493 9.528_075_420_561_144e-1,
494 9.552_560_754_313_16e-1,
495 9.576_408_376_272_095e-1,
496 9.599_616_694_413_742e-1,
497 9.622_184_159_392_698e-1,
498 9.644_109_264_645_802e-1,
499 9.665_390_546_492_71e-1,
500 9.686_026_584_233_628e-1,
501 9.706_016_000_244_151e-1,
502 9.725_357_460_067_257e-1,
503 9.744_049_672_502_397e-1,
504 9.762_091_389_691_724e-1,
505 9.779_481_407_203_411e-1,
506 9.796_218_564_112_101e-1,
507 9.812_301_743_076_443e-1,
508 9.827_729_870_413_743e-1,
509 9.842_501_916_171_713e-1,
510 9.856_616_894_197_333e-1,
511 9.870_073_862_202_815e-1,
512 9.882_871_921_828_699e-1,
513 9.895_010_218_704_087e-1,
514 9.906_487_942_504_061e-1,
515 9.917_304_327_004_32e-1,
516 9.927_458_650_133_153e-1,
517 9.936_950_234_020_883e-1,
518 9.945_778_445_047_068e-1,
519 9.953_942_693_885_953e-1,
520 9.961_442_435_551_087e-1,
521 9.968_277_169_440_913e-1,
522 9.974_446_439_389_107e-1,
523 9.979_949_833_727_938e-1,
524 9.984_786_985_384_589e-1,
525 9.988_957_572_063_257e-1,
526 9.992_461_316_671_845e-1,
527 9.995_297_988_558_859e-1,
528 9.997_467_408_113_523e-1,
529 9.998_969_471_378_596e-1,
530 9.999_804_411_726_474e-1,
531];
532const GL_WEIGHTS: [f64; 384] = [
533 5.019_410_348_676_869_6e-5,
534 1.168_390_665_730_266_3e-4,
535 1.835_749_193_551_655_8e-4,
536 2.503_070_890_844_105e-4,
537 3.170_242_698_112_815e-4,
538 3.837_208_020_912_921_4e-4,
539 4.503_919_137_716_827e-4,
540 5.170_330_453_491_649e-4,
541 5.836_397_042_630_135e-4,
542 6.502_074_240_969_948e-4,
543 7.167_317_509_947_801e-4,
544 7.832_082_385_905_168e-4,
545 8.496_324_460_039_209e-4,
546 9.159_999_370_632_641e-4,
547 9.823_062_800_663_463e-4,
548 1.048_547_047_793_689_5e-3,
549 1.114_717_817_647_310_6e-3,
550 1.180_814_171_855_922e-3,
551 1.246_831_697_715_441_5e-3,
552 1.312_765_987_850_66e-3,
553 1.378_612_640_487_646_8e-3,
554 1.444_367_259_734_736e-3,
555 1.510_025_455_865_810_3e-3,
556 1.575_582_845_607_936_8e-3,
557 1.641_035_052_429_271_5e-3,
558 1.706_377_706_828_447_1e-3,
559 1.771_606_446_623_834_7e-3,
560 1.836_716_917_243_567_5e-3,
561 1.901_704_772_014_899_2e-3,
562 1.966_565_672_453_437e-3,
563 2.031_295_288_552_398_4e-3,
564 2.095_889_299_071_020_6e-3,
565 2.160_343_391_822_734_3e-3,
566 2.224_653_263_962_713e-3,
567 2.288_814_622_274_955e-3,
568 2.352_823_183_458_769e-3,
569 2.416_674_674_414_340_5e-3,
570 2.480_364_832_528_265_6e-3,
571 2.543_889_405_957_74e-3,
572 2.607_244_153_914_452e-3,
573 2.670_424_846_947_554e-3,
574 2.733_427_267_226_093_3e-3,
575 2.796_247_208_820_428e-3,
576 2.858_880_477_983_06e-3,
577 2.921_322_893_428_515_3e-3,
578 2.983_570_286_612_554_5e-3,
579 3.045_618_502_010_327_8e-3,
580 3.107_463_397_393_755_5e-3,
581 3.169_100_844_108_32e-3,
582 3.230_526_727_348_174e-3,
583 3.291_736_946_431_361e-3,
584 3.352_727_415_073_250_3e-3,
585 3.413_494_061_659_418_4e-3,
586 3.474_032_829_517_317e-3,
587 3.534_339_677_187_348_4e-3,
588 3.594_410_578_692_452e-3,
589 3.654_241_523_806_987e-3,
590 3.713_828_518_324_312_5e-3,
591 3.773_167_584_323_583_5e-3,
592 3.832_254_760_435_171e-3,
593 3.891_086_102_105_193_4e-3,
594 3.949_657_681_858_895e-3,
595 4.007_965_589_562_678e-3,
596 4.066_005_932_685_269e-3,
597 4.123_774_836_557_6e-3,
598 4.181_268_444_631_281e-3,
599 4.238_482_918_736_289e-3,
600 4.295_414_439_336_925e-3,
601 4.352_059_205_787_275e-3,
602 4.408_413_436_584_285e-3,
603 4.464_473_369_620_78e-3,
604 4.520_235_262_436_235e-3,
605 4.575_695_392_466_791e-3,
606 4.630_850_057_293_894e-3,
607 4.685_695_574_891_041e-3,
608 4.740_228_283_870_022e-3,
609 4.794_444_543_725_102e-3,
610 4.848_340_735_076_109e-3,
611 4.901_913_259_910_197e-3,
612 4.955_158_541_821_682_4e-3,
613 5.008_073_026_251_332e-3,
614 5.060_653_180_723_101_4e-3,
615 5.112_895_495_080_397e-3,
616 5.164_796_481_720_011e-3,
617 5.216_352_675_825_451e-3,
618 5.267_560_635_597_735e-3,
619 5.318_416_942_485_385e-3,
620 5.368_918_201_412_827e-3,
621 5.419_061_041_006_627e-3,
622 5.468_842_113_820_941e-3,
623 5.518_258_096_560_71e-3,
624 5.567_305_690_303_767e-3,
625 5.615_981_620_720_803e-3,
626 5.664_282_638_294_182e-3,
627 5.712_205_518_534_655e-3,
628 5.759_747_062_196_925_5e-3,
629 5.806_904_095_492_818e-3,
630 5.853_673_470_303_617_4e-3,
631 5.900_052_064_389_824e-3,
632 5.946_036_781_599_814e-3,
633 5.991_624_552_076_468e-3,
634 6.036_812_332_462_087e-3,
635 6.081_597_106_101_673e-3,
636 6.125_975_883_244_196e-3,
637 6.169_945_701_242_237e-3,
638 6.213_503_624_749_591e-3,
639 6.256_646_745_917_723e-3,
640 6.299_372_184_589_237e-3,
641 6.341_677_088_490_664e-3,
642 6.383_558_633_422_572e-3,
643 6.425_014_023_448_273e-3,
644 6.466_040_491_080_434e-3,
645 6.506_635_297_465_724e-3,
646 6.546_795_732_567_842_5e-3,
647 6.586_519_115_348_261e-3,
648 6.625_802_793_945_317e-3,
649 6.664_644_145_851_14e-3,
650 6.703_040_578_086_941e-3,
651 6.740_989_527_375_895e-3,
652 6.778_488_460_314_126e-3,
653 6.815_534_873_540_5e-3,
654 6.852_126_293_902_878e-3,
655 6.888_260_278_623_754e-3,
656 6.923_934_415_463_31e-3,
657 6.959_146_322_880_146_5e-3,
658 6.993_893_650_190_702e-3,
659 7.028_174_077_725_734e-3,
660 7.061_985_316_985_506e-3,
661 7.095_325_110_792_439e-3,
662 7.128_191_233_441_844e-3,
663 7.160_581_490_850_321e-3,
664 7.192_493_720_702_486e-3,
665 7.223_925_792_595_309e-3,
666 7.254_875_608_179_984e-3,
667 7.285_341_101_302_512e-3,
668 7.315_320_238_141_324_5e-3,
669 7.344_811_017_343_063e-3,
670 7.373_811_470_156_258e-3,
671 7.402_319_660_562_818e-3,
672 7.430_333_685_407_178e-3,
673 7.457_851_674_523_319e-3,
674 7.484_871_790_859_79e-3,
675 7.511_392_230_602_079e-3,
676 7.537_411_223_293_362e-3,
677 7.562_927_031_952_382e-3,
678 7.587_937_953_189_561_5e-3,
679 7.612_442_317_320_796e-3,
680 7.636_438_488_478_739e-3,
681 7.659_924_864_722_064e-3,
682 7.682_899_878_142_539e-3,
683 7.705_361_994_969_524e-3,
684 7.727_309_715_672_44e-3,
685 7.748_741_575_060_914e-3,
686 7.769_656_142_382_462e-3,
687 7.790_052_021_418_226e-3,
688 7.809_927_850_575_903e-3,
689 7.829_282_302_980_82e-3,
690 7.848_114_086_564_56e-3,
691 7.866_421_944_151_094e-3,
692 7.884_204_653_540_665e-3,
693 7.901_461_027_591_6e-3,
694 7.918_189_914_299_318e-3,
695 7.934_390_196_873_448e-3,
696 7.950_060_793_812_204e-3,
697 7.965_200_658_974_709e-3,
698 7.979_808_781_650_77e-3,
699 7.993_884_186_628_266e-3,
700 8.007_425_934_258_548e-3,
701 8.020_433_120_518_866e-3,
702 8.032_904_877_072_8e-3,
703 8.044_840_371_328_26e-3,
704 8.056_238_806_493_175e-3,
705 8.067_099_421_628_42e-3,
706 8.077_421_491_698_82e-3,
707 8.087_204_327_621_594e-3,
708 8.096_447_276_312_202e-3,
709 8.105_149_720_727_933e-3,
710 8.113_311_079_909_208e-3,
711 8.120_930_809_018_415e-3,
712 8.128_008_399_376_085e-3,
713 8.134_543_378_495_033e-3,
714 8.140_535_310_111_77e-3,
715 8.145_983_794_215_77e-3,
716 8.150_888_467_075_875e-3,
717 8.155_249_001_265_092e-3,
718 8.159_065_105_681_899e-3,
719 8.162_336_525_570_1e-3,
720 8.165_063_042_535_465e-3,
721 8.167_244_474_560_707e-3,
722 8.168_880_676_017_344e-3,
723 8.169_971_537_675_47e-3,
724 8.170_516_986_711_104e-3,
725 8.170_516_986_711_104e-3,
726 8.169_971_537_675_47e-3,
727 8.168_880_676_017_344e-3,
728 8.167_244_474_560_707e-3,
729 8.165_063_042_535_465e-3,
730 8.162_336_525_570_1e-3,
731 8.159_065_105_681_899e-3,
732 8.155_249_001_265_092e-3,
733 8.150_888_467_075_875e-3,
734 8.145_983_794_215_77e-3,
735 8.140_535_310_111_77e-3,
736 8.134_543_378_495_033e-3,
737 8.128_008_399_376_085e-3,
738 8.120_930_809_018_415e-3,
739 8.113_311_079_909_208e-3,
740 8.105_149_720_727_933e-3,
741 8.096_447_276_312_202e-3,
742 8.087_204_327_621_594e-3,
743 8.077_421_491_698_82e-3,
744 8.067_099_421_628_42e-3,
745 8.056_238_806_493_175e-3,
746 8.044_840_371_328_26e-3,
747 8.032_904_877_072_8e-3,
748 8.020_433_120_518_866e-3,
749 8.007_425_934_258_548e-3,
750 7.993_884_186_628_266e-3,
751 7.979_808_781_650_77e-3,
752 7.965_200_658_974_709e-3,
753 7.950_060_793_812_204e-3,
754 7.934_390_196_873_448e-3,
755 7.918_189_914_299_318e-3,
756 7.901_461_027_591_6e-3,
757 7.884_204_653_540_665e-3,
758 7.866_421_944_151_094e-3,
759 7.848_114_086_564_56e-3,
760 7.829_282_302_980_82e-3,
761 7.809_927_850_575_903e-3,
762 7.790_052_021_418_226e-3,
763 7.769_656_142_382_462e-3,
764 7.748_741_575_060_914e-3,
765 7.727_309_715_672_44e-3,
766 7.705_361_994_969_524e-3,
767 7.682_899_878_142_539e-3,
768 7.659_924_864_722_064e-3,
769 7.636_438_488_478_739e-3,
770 7.612_442_317_320_796e-3,
771 7.587_937_953_189_561_5e-3,
772 7.562_927_031_952_382e-3,
773 7.537_411_223_293_362e-3,
774 7.511_392_230_602_079e-3,
775 7.484_871_790_859_79e-3,
776 7.457_851_674_523_319e-3,
777 7.430_333_685_407_178e-3,
778 7.402_319_660_562_818e-3,
779 7.373_811_470_156_258e-3,
780 7.344_811_017_343_063e-3,
781 7.315_320_238_141_324_5e-3,
782 7.285_341_101_302_512e-3,
783 7.254_875_608_179_984e-3,
784 7.223_925_792_595_309e-3,
785 7.192_493_720_702_486e-3,
786 7.160_581_490_850_321e-3,
787 7.128_191_233_441_844e-3,
788 7.095_325_110_792_439e-3,
789 7.061_985_316_985_506e-3,
790 7.028_174_077_725_734e-3,
791 6.993_893_650_190_702e-3,
792 6.959_146_322_880_146_5e-3,
793 6.923_934_415_463_31e-3,
794 6.888_260_278_623_754e-3,
795 6.852_126_293_902_878e-3,
796 6.815_534_873_540_5e-3,
797 6.778_488_460_314_126e-3,
798 6.740_989_527_375_895e-3,
799 6.703_040_578_086_941e-3,
800 6.664_644_145_851_14e-3,
801 6.625_802_793_945_317e-3,
802 6.586_519_115_348_261e-3,
803 6.546_795_732_567_842_5e-3,
804 6.506_635_297_465_724e-3,
805 6.466_040_491_080_434e-3,
806 6.425_014_023_448_273e-3,
807 6.383_558_633_422_572e-3,
808 6.341_677_088_490_664e-3,
809 6.299_372_184_589_237e-3,
810 6.256_646_745_917_723e-3,
811 6.213_503_624_749_591e-3,
812 6.169_945_701_242_237e-3,
813 6.125_975_883_244_196e-3,
814 6.081_597_106_101_673e-3,
815 6.036_812_332_462_087e-3,
816 5.991_624_552_076_468e-3,
817 5.946_036_781_599_814e-3,
818 5.900_052_064_389_824e-3,
819 5.853_673_470_303_617_4e-3,
820 5.806_904_095_492_818e-3,
821 5.759_747_062_196_925_5e-3,
822 5.712_205_518_534_655e-3,
823 5.664_282_638_294_182e-3,
824 5.615_981_620_720_803e-3,
825 5.567_305_690_303_767e-3,
826 5.518_258_096_560_71e-3,
827 5.468_842_113_820_941e-3,
828 5.419_061_041_006_627e-3,
829 5.368_918_201_412_827e-3,
830 5.318_416_942_485_385e-3,
831 5.267_560_635_597_735e-3,
832 5.216_352_675_825_451e-3,
833 5.164_796_481_720_011e-3,
834 5.112_895_495_080_397e-3,
835 5.060_653_180_723_101_4e-3,
836 5.008_073_026_251_332e-3,
837 4.955_158_541_821_682_4e-3,
838 4.901_913_259_910_197e-3,
839 4.848_340_735_076_109e-3,
840 4.794_444_543_725_102e-3,
841 4.740_228_283_870_022e-3,
842 4.685_695_574_891_041e-3,
843 4.630_850_057_293_894e-3,
844 4.575_695_392_466_791e-3,
845 4.520_235_262_436_235e-3,
846 4.464_473_369_620_78e-3,
847 4.408_413_436_584_285e-3,
848 4.352_059_205_787_275e-3,
849 4.295_414_439_336_925e-3,
850 4.238_482_918_736_289e-3,
851 4.181_268_444_631_281e-3,
852 4.123_774_836_557_6e-3,
853 4.066_005_932_685_269e-3,
854 4.007_965_589_562_678e-3,
855 3.949_657_681_858_895e-3,
856 3.891_086_102_105_193_4e-3,
857 3.832_254_760_435_171e-3,
858 3.773_167_584_323_583_5e-3,
859 3.713_828_518_324_312_5e-3,
860 3.654_241_523_806_987e-3,
861 3.594_410_578_692_452e-3,
862 3.534_339_677_187_348_4e-3,
863 3.474_032_829_517_317e-3,
864 3.413_494_061_659_418_4e-3,
865 3.352_727_415_073_250_3e-3,
866 3.291_736_946_431_361e-3,
867 3.230_526_727_348_174e-3,
868 3.169_100_844_108_32e-3,
869 3.107_463_397_393_755_5e-3,
870 3.045_618_502_010_327_8e-3,
871 2.983_570_286_612_554_5e-3,
872 2.921_322_893_428_515_3e-3,
873 2.858_880_477_983_06e-3,
874 2.796_247_208_820_428e-3,
875 2.733_427_267_226_093_3e-3,
876 2.670_424_846_947_554e-3,
877 2.607_244_153_914_452e-3,
878 2.543_889_405_957_74e-3,
879 2.480_364_832_528_265_6e-3,
880 2.416_674_674_414_340_5e-3,
881 2.352_823_183_458_769e-3,
882 2.288_814_622_274_955e-3,
883 2.224_653_263_962_713e-3,
884 2.160_343_391_822_734_3e-3,
885 2.095_889_299_071_020_6e-3,
886 2.031_295_288_552_398_4e-3,
887 1.966_565_672_453_437e-3,
888 1.901_704_772_014_899_2e-3,
889 1.836_716_917_243_567_5e-3,
890 1.771_606_446_623_834_7e-3,
891 1.706_377_706_828_447_1e-3,
892 1.641_035_052_429_271_5e-3,
893 1.575_582_845_607_936_8e-3,
894 1.510_025_455_865_810_3e-3,
895 1.444_367_259_734_736e-3,
896 1.378_612_640_487_646_8e-3,
897 1.312_765_987_850_66e-3,
898 1.246_831_697_715_441_5e-3,
899 1.180_814_171_855_922e-3,
900 1.114_717_817_647_310_6e-3,
901 1.048_547_047_793_689_5e-3,
902 9.823_062_800_663_463e-4,
903 9.159_999_370_632_641e-4,
904 8.496_324_460_039_209e-4,
905 7.832_082_385_905_168e-4,
906 7.167_317_509_947_801e-4,
907 6.502_074_240_969_948e-4,
908 5.836_397_042_630_135e-4,
909 5.170_330_453_491_649e-4,
910 4.503_919_137_716_827e-4,
911 3.837_208_020_912_921_4e-4,
912 3.170_242_698_112_815e-4,
913 2.503_070_890_844_105e-4,
914 1.835_749_193_551_655_8e-4,
915 1.168_390_665_730_266_3e-4,
916 5.019_410_348_676_869_6e-5,
917];
918
919#[derive(Clone, Copy, Debug, Eq, PartialEq)]
920pub enum ExactCellBranch {
921 Affine,
922 Quartic,
923 Sextic,
924}
925
926#[inline]
943fn effective_branch_tol(cell: DenestedCubicCell) -> f64 {
944 let anchor_scale = cell.c0.abs().max(cell.c1.abs()).max(1.0);
945 NORMALIZED_CELL_BRANCH_TOL * anchor_scale
946}
947
948#[derive(Clone, Copy, Debug, PartialEq)]
949pub struct DenestedCubicCell {
950 pub left: f64,
951 pub right: f64,
952 pub c0: f64,
953 pub c1: f64,
954 pub c2: f64,
955 pub c3: f64,
956}
957
958impl DenestedCubicCell {
959 #[inline]
960 pub fn eta(self, z: f64) -> f64 {
961 self.c0 + self.c1 * z + self.c2 * z * z + self.c3 * z * z * z
962 }
963
964 #[inline]
965 pub fn q(self, z: f64) -> f64 {
966 let eta = self.eta(z);
967 0.5 * (z * z + eta * eta)
968 }
969}
970
971#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
972pub struct CellMomentFingerprint {
973 pub hash: u64,
974 bins: [u64; 6],
975}
976
977#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
978pub struct CellMomentCacheKey {
979 pub fingerprint: CellMomentFingerprint,
980 pub max_degree: usize,
981}
982
983#[derive(Clone, Copy, Debug, Default, PartialEq)]
984pub struct CellMomentDedupStats {
985 pub lookups: u64,
986 pub hits: u64,
987 pub misses: u64,
988}
989
990impl CellMomentDedupStats {
991 #[inline]
992 pub fn hit_rate(self) -> f64 {
993 if self.lookups == 0 {
994 0.0
995 } else {
996 self.hits as f64 / self.lookups as f64
997 }
998 }
999}
1000
1001#[inline]
1002fn splitmix64(x: u64) -> u64 {
1003 gam_linalg::utils::splitmix64_hash(x)
1004}
1005
1006#[inline]
1007fn mix_fingerprint_words(words: &[u64]) -> u64 {
1008 let mut h = 0xcbf2_9ce4_8422_2325u64;
1009 for &word in words {
1010 h ^= splitmix64(word);
1011 h = h.wrapping_mul(0x100_0000_01b3);
1012 }
1013 h
1014}
1015
1016#[inline]
1017fn quantized_cell_word(x: f64, epsilon: f64) -> u64 {
1018 if epsilon == 0.0 || !epsilon.is_finite() || epsilon < 0.0 || !x.is_finite() {
1019 return x.to_bits();
1020 }
1021 (x / epsilon).round().to_bits()
1022}
1023
1024pub fn cell_moment_fingerprint(cell: DenestedCubicCell, epsilon: f64) -> CellMomentFingerprint {
1032 let bins = [
1033 quantized_cell_word(cell.left, epsilon),
1034 quantized_cell_word(cell.right, epsilon),
1035 quantized_cell_word(cell.c0, epsilon),
1036 quantized_cell_word(cell.c1, epsilon),
1037 quantized_cell_word(cell.c2, epsilon),
1038 quantized_cell_word(cell.c3, epsilon),
1039 ];
1040 CellMomentFingerprint {
1041 hash: mix_fingerprint_words(&bins),
1042 bins,
1043 }
1044}
1045
1046#[inline]
1047pub fn cell_moment_cache_key(
1048 cell: DenestedCubicCell,
1049 max_degree: usize,
1050 epsilon: f64,
1051) -> CellMomentCacheKey {
1052 CellMomentCacheKey {
1053 fingerprint: cell_moment_fingerprint(cell, epsilon),
1054 max_degree,
1055 }
1056}
1057
1058#[derive(Clone, Copy, Debug, PartialEq)]
1059pub struct DenestedPartitionCell {
1060 pub cell: DenestedCubicCell,
1061 pub score_span: LocalSpanCubic,
1062 pub link_span: LocalSpanCubic,
1063 pub left_edge: PartitionEdge,
1069 pub right_edge: PartitionEdge,
1070}
1071
1072impl DenestedPartitionCell {}
1073
1074#[derive(Clone, Copy, Debug, PartialEq)]
1076pub enum PartitionEdge {
1077 Fixed(f64),
1080 Crossing { tau: f64 },
1083}
1084
1085impl PartitionEdge {
1086 #[inline]
1088 pub fn z_at(self, a: f64, b: f64) -> f64 {
1089 match self {
1090 Self::Fixed(z) => z,
1091 Self::Crossing { tau } => (tau - a) / b,
1092 }
1093 }
1094}
1095
1096#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1097struct TailCellMomentCacheKey {
1098 c0_bits: u64,
1099 c1_bits: u64,
1100 endpoint_bits: u64,
1101 side: i8,
1102 max_degree: usize,
1103}
1104
1105const TAIL_CELL_MOMENT_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
1106const TAIL_CELL_MOMENT_CACHE_MAX_ENTRIES: usize = 262_144;
1107
1108#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1109pub struct TailCellMomentCacheStats {
1110 pub hits: usize,
1111 pub misses: usize,
1112 pub entries: usize,
1113}
1114
1115impl TailCellMomentCacheStats {
1116 #[inline]
1117 pub fn requests(self) -> usize {
1118 self.hits + self.misses
1119 }
1120
1121 #[inline]
1122 pub fn hit_rate(self) -> f64 {
1123 let requests = self.requests();
1124 if requests == 0 {
1125 0.0
1126 } else {
1127 self.hits as f64 / requests as f64
1128 }
1129 }
1130}
1131
1132#[derive(Debug)]
1146pub struct TailCellMomentCache {
1147 moments: ByteLruCache<TailCellMomentCacheKey, CellMomentState>,
1148 in_flight: std::sync::Mutex<
1149 std::collections::HashMap<
1150 TailCellMomentCacheKey,
1151 Arc<std::sync::OnceLock<Result<CellMomentState, String>>>,
1152 >,
1153 >,
1154 hits: std::sync::atomic::AtomicUsize,
1155 misses: std::sync::atomic::AtomicUsize,
1156}
1157
1158impl Default for TailCellMomentCache {
1159 fn default() -> Self {
1160 let shard_count = std::thread::available_parallelism()
1164 .map(|workers| workers.get().saturating_mul(8))
1165 .unwrap_or(32)
1166 .clamp(8, 256);
1167 Self {
1168 moments: ByteLruCache::with_max_entries_sharded(
1169 TAIL_CELL_MOMENT_CACHE_MAX_BYTES,
1170 TAIL_CELL_MOMENT_CACHE_MAX_ENTRIES,
1171 shard_count,
1172 ),
1173 in_flight: std::sync::Mutex::new(std::collections::HashMap::new()),
1174 hits: std::sync::atomic::AtomicUsize::new(0),
1175 misses: std::sync::atomic::AtomicUsize::new(0),
1176 }
1177 }
1178}
1179
1180impl TailCellMomentCache {
1181 #[inline]
1183 pub fn new() -> Self {
1184 Self::default()
1185 }
1186
1187 #[inline]
1190 pub fn clear(&self) {
1191 self.moments.clear();
1192 self.in_flight
1193 .lock()
1194 .unwrap_or_else(|p| p.into_inner())
1195 .clear();
1196 self.hits.store(0, std::sync::atomic::Ordering::Relaxed);
1197 self.misses.store(0, std::sync::atomic::Ordering::Relaxed);
1198 }
1199
1200 #[inline]
1202 pub fn stats(&self) -> TailCellMomentCacheStats {
1203 TailCellMomentCacheStats {
1204 hits: self.hits.load(std::sync::atomic::Ordering::Relaxed),
1205 misses: self.misses.load(std::sync::atomic::Ordering::Relaxed),
1206 entries: self.moments.len(),
1207 }
1208 }
1209
1210 pub fn evaluate(
1221 &self,
1222 cell: DenestedCubicCell,
1223 max_degree: usize,
1224 ) -> Result<CellMomentState, String> {
1225 let Some(key) = tail_cell_cache_key(cell, max_degree) else {
1226 return evaluate_cell_moments_uncached(cell, max_degree);
1227 };
1228 if let Some(state) = self.moments.get(&key) {
1229 self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1230 return Ok(state);
1231 }
1232
1233 let (slot, leader) = {
1234 let mut in_flight = self.in_flight.lock().unwrap_or_else(|p| p.into_inner());
1235 if let Some(slot) = in_flight.get(&key) {
1236 (Arc::clone(slot), false)
1237 } else {
1238 let slot = Arc::new(std::sync::OnceLock::new());
1239 in_flight.insert(key, Arc::clone(&slot));
1240 (slot, true)
1241 }
1242 };
1243
1244 if !leader {
1245 let state = slot.wait().clone()?;
1246 self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1247 return Ok(state);
1248 }
1249
1250 let state = evaluate_cell_moments_uncached(cell, max_degree);
1251 if let Ok(state) = &state {
1252 self.moments.insert(key, state.clone());
1253 self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1254 }
1255 self.misses
1256 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1257 if let Err(existing_state) = slot.set(state.clone()) {
1258 std::mem::drop(existing_state);
1259 }
1260 self.in_flight
1261 .lock()
1262 .unwrap_or_else(|p| p.into_inner())
1263 .remove(&key);
1264 state
1265 }
1266}
1267
1268static TAIL_CELL_MOMENT_CACHE: std::sync::OnceLock<TailCellMomentCache> =
1269 std::sync::OnceLock::new();
1270static TAIL_CELL_MOMENT_CACHE_ENABLED: std::sync::atomic::AtomicBool =
1271 std::sync::atomic::AtomicBool::new(true);
1272
1273fn tail_cell_moment_cache() -> &'static TailCellMomentCache {
1274 TAIL_CELL_MOMENT_CACHE.get_or_init(TailCellMomentCache::default)
1275}
1276
1277#[inline]
1278fn tail_cell_cache_key(
1279 cell: DenestedCubicCell,
1280 max_degree: usize,
1281) -> Option<TailCellMomentCacheKey> {
1282 if cell.c2.abs() > NORMALIZED_CELL_BRANCH_TOL || cell.c3.abs() > NORMALIZED_CELL_BRANCH_TOL {
1283 return None;
1284 }
1285 match (!cell.left.is_finite(), !cell.right.is_finite()) {
1286 (true, false) if cell.right.is_finite() => Some(TailCellMomentCacheKey {
1287 c0_bits: cell.c0.to_bits(),
1288 c1_bits: cell.c1.to_bits(),
1289 endpoint_bits: cell.right.to_bits(),
1290 side: -1,
1291 max_degree,
1292 }),
1293 (false, true) if cell.left.is_finite() => Some(TailCellMomentCacheKey {
1294 c0_bits: cell.c0.to_bits(),
1295 c1_bits: cell.c1.to_bits(),
1296 endpoint_bits: cell.left.to_bits(),
1297 side: 1,
1298 max_degree,
1299 }),
1300 _ => None,
1301 }
1302}
1303
1304pub fn set_tail_cell_moment_cache_enabled(enabled: bool) {
1305 TAIL_CELL_MOMENT_CACHE_ENABLED.store(enabled, std::sync::atomic::Ordering::Relaxed);
1306}
1307
1308pub fn reset_tail_cell_moment_cache() {
1309 tail_cell_moment_cache().clear();
1310}
1311
1312pub fn tail_cell_moment_cache_stats() -> TailCellMomentCacheStats {
1313 tail_cell_moment_cache().stats()
1314}
1315
1316#[derive(Clone, Copy, Debug, Eq)]
1317pub struct CellFingerprint {
1318 c0: u64,
1319 c1: u64,
1320 c2: u64,
1321 c3: u64,
1322 left: u64,
1323 right: u64,
1324}
1325
1326impl CellFingerprint {
1327 #[inline]
1328 pub fn new(cell: DenestedCubicCell) -> Self {
1329 Self {
1330 c0: cell.c0.to_bits(),
1331 c1: cell.c1.to_bits(),
1332 c2: cell.c2.to_bits(),
1333 c3: cell.c3.to_bits(),
1334 left: cell.left.to_bits(),
1335 right: cell.right.to_bits(),
1336 }
1337 }
1338}
1339
1340impl PartialEq for CellFingerprint {
1341 #[inline]
1342 fn eq(&self, other: &Self) -> bool {
1343 self.c0 == other.c0
1344 && self.c1 == other.c1
1345 && self.c2 == other.c2
1346 && self.c3 == other.c3
1347 && self.left == other.left
1348 && self.right == other.right
1349 }
1350}
1351
1352impl Hash for CellFingerprint {
1353 #[inline]
1354 fn hash<H: Hasher>(&self, state: &mut H) {
1355 self.c0.hash(state);
1356 self.c1.hash(state);
1357 self.c2.hash(state);
1358 self.c3.hash(state);
1359 self.left.hash(state);
1360 self.right.hash(state);
1361 }
1362}
1363
1364#[derive(Clone, Debug, Default, PartialEq)]
1365pub struct CachedCellMoments {
1366 state: Option<Arc<CellMomentState>>,
1373 derivative_state: Option<Arc<CellDerivativeMomentState>>,
1380}
1381
1382impl CachedCellMoments {
1383 #[inline]
1384 pub fn new(state: Arc<CellMomentState>) -> Self {
1385 Self {
1386 state: Some(state),
1387 derivative_state: None,
1388 }
1389 }
1390
1391 #[inline]
1392 pub fn new_derivative(state: Arc<CellDerivativeMomentState>) -> Self {
1393 Self {
1394 state: None,
1395 derivative_state: Some(state),
1396 }
1397 }
1398
1399 #[inline]
1400 pub fn state_for_degree(&self, max_degree: usize) -> Option<CellMomentState> {
1401 let state = self.state.as_ref()?;
1402 if state.moments.len().saturating_sub(1) < max_degree {
1403 return None;
1404 }
1405 let mut state = (**state).clone();
1410 state.moments.truncate(max_degree + 1);
1411 Some(state)
1412 }
1413
1414 #[inline]
1415 pub fn derivative_state_for_degree(
1416 &self,
1417 max_degree: usize,
1418 ) -> Option<CellDerivativeMomentState> {
1419 let state = self.derivative_state.as_ref()?;
1420 if state.moments.len().saturating_sub(1) < max_degree {
1421 return None;
1422 }
1423 let mut state = (**state).clone();
1425 state.moments.truncate(max_degree + 1);
1426 Some(state)
1427 }
1428
1429 #[inline]
1430 pub fn with_value(mut self, state: Arc<CellMomentState>) -> Self {
1431 self.state = Some(state);
1432 self
1433 }
1434
1435 #[inline]
1436 pub fn with_derivative(mut self, state: Arc<CellDerivativeMomentState>) -> Self {
1437 self.derivative_state = Some(state);
1438 self
1439 }
1440}
1441
1442impl ResidentBytes for CachedCellMoments {
1443 fn resident_bytes(&self) -> usize {
1444 let value_bytes = self
1445 .state
1446 .as_ref()
1447 .map_or(0, |state| state.resident_bytes());
1448 let derivative_bytes = self
1449 .derivative_state
1450 .as_ref()
1451 .map_or(0, |state| state.resident_bytes());
1452 std::mem::size_of::<Self>()
1453 .saturating_add(value_bytes)
1454 .saturating_add(derivative_bytes)
1455 }
1456}
1457
1458#[derive(Debug, Default)]
1459pub struct CellMomentCacheStats {
1460 hits: AtomicU64,
1461 misses: AtomicU64,
1462}
1463
1464impl CellMomentCacheStats {
1465 #[inline]
1466 pub fn snapshot(&self) -> (u64, u64) {
1467 (
1468 self.hits.load(Ordering::Relaxed),
1469 self.misses.load(Ordering::Relaxed),
1470 )
1471 }
1472
1473 #[inline]
1474 pub fn hit_rate_delta(&self, before: (u64, u64)) -> (u64, u64, f64) {
1475 let (hits, misses) = self.snapshot();
1476 let dh = hits.saturating_sub(before.0);
1477 let dm = misses.saturating_sub(before.1);
1478 let total = dh + dm;
1479 let rate = if total == 0 {
1480 0.0
1481 } else {
1482 dh as f64 / total as f64
1483 };
1484 (dh, dm, rate)
1485 }
1486}
1487
1488pub type CellMomentLruCache = ByteLruCache<CellFingerprint, CachedCellMoments>;
1489
1490pub const CELL_MOMENT_INLINE_CAPACITY: usize = 10;
1491
1492pub type CellMomentVec = SmallVec<[f64; CELL_MOMENT_INLINE_CAPACITY]>;
1493
1494#[derive(Clone, Debug, PartialEq)]
1495pub struct CellMomentState {
1496 pub branch: ExactCellBranch,
1497 pub value: f64,
1498 pub moments: CellMomentVec,
1499}
1500
1501impl ResidentBytes for CellMomentState {
1502 fn resident_bytes(&self) -> usize {
1503 let spilled_bytes = if self.moments.spilled() {
1504 self.moments
1505 .capacity()
1506 .saturating_mul(std::mem::size_of::<f64>())
1507 } else {
1508 0
1509 };
1510 std::mem::size_of::<Self>().saturating_add(spilled_bytes)
1511 }
1512}
1513
1514#[derive(Clone, Debug, PartialEq)]
1515pub struct CellDerivativeMomentState {
1516 pub branch: ExactCellBranch,
1517 pub moments: CellMomentVec,
1518}
1519
1520impl ResidentBytes for CellDerivativeMomentState {
1521 fn resident_bytes(&self) -> usize {
1522 let spilled_bytes = if self.moments.spilled() {
1523 self.moments
1524 .capacity()
1525 .saturating_mul(std::mem::size_of::<f64>())
1526 } else {
1527 0
1528 };
1529 std::mem::size_of::<Self>().saturating_add(spilled_bytes)
1530 }
1531}
1532
1533#[derive(Clone, Copy, Debug, PartialEq)]
1534pub struct CellMomentStateRef<'a> {
1535 pub branch: ExactCellBranch,
1536 pub value: f64,
1537 pub moments: &'a [f64],
1538}
1539
1540#[derive(Clone, Debug)]
1541pub struct CellMomentScratch {
1542 moments: Vec<f64>,
1543}
1544
1545impl Default for CellMomentScratch {
1546 fn default() -> Self {
1547 Self {
1551 moments: Vec::with_capacity(MAX_AFFINE_ANCHOR_DEGREE + 1),
1552 }
1553 }
1554}
1555
1556impl CellMomentScratch {
1557 pub fn new() -> Self {
1558 Self::default()
1559 }
1560
1561 pub fn with_capacity(max_degree: usize) -> Self {
1562 Self {
1563 moments: Vec::with_capacity(max_degree + 1),
1564 }
1565 }
1566
1567 #[inline]
1568 fn prepare_moments(&mut self, len: usize) -> &mut [f64] {
1569 if self.moments.capacity() < len {
1570 CELL_MOMENT_REALLOCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1571 self.moments.reserve(len - self.moments.capacity());
1572 }
1573 if self.moments.len() < len {
1577 self.moments.resize(len, 0.0);
1578 }
1579 let out = &mut self.moments[..len];
1580 out.fill(0.0);
1581 out
1582 }
1583}
1584
1585pub(crate) static CELL_MOMENT_REALLOCS: std::sync::atomic::AtomicUsize =
1589 std::sync::atomic::AtomicUsize::new(0);
1590
1591pub const GL20_NODES: [f64; 20] = [
1598 -0.993_128_599_185_094_9,
1599 -0.963_971_927_277_913_8,
1600 -0.912_234_428_251_326,
1601 -0.839_116_971_822_218_8,
1602 -0.746_331_906_460_150_8,
1603 -0.636_053_680_726_515,
1604 -0.510_867_001_950_827_1,
1605 -0.373_706_088_715_419_6,
1606 -0.227_785_851_141_645_1,
1607 -0.076_526_521_133_497_33,
1608 0.076_526_521_133_497_33,
1609 0.227_785_851_141_645_1,
1610 0.373_706_088_715_419_6,
1611 0.510_867_001_950_827_1,
1612 0.636_053_680_726_515,
1613 0.746_331_906_460_150_8,
1614 0.839_116_971_822_218_8,
1615 0.912_234_428_251_326,
1616 0.963_971_927_277_913_8,
1617 0.993_128_599_185_094_9,
1618];
1619
1620pub const GL20_WEIGHTS: [f64; 20] = [
1622 0.017_614_007_139_152_12,
1623 0.040_601_429_800_386_94,
1624 0.062_672_048_334_109_06,
1625 0.083_276_741_576_704_75,
1626 0.101_930_119_817_240_4,
1627 0.118_194_531_961_518_4,
1628 0.131_688_638_449_176_6,
1629 0.142_096_109_318_382_1,
1630 0.149_172_986_472_603_7,
1631 0.152_753_387_130_725_9,
1632 0.152_753_387_130_725_9,
1633 0.149_172_986_472_603_7,
1634 0.142_096_109_318_382_1,
1635 0.131_688_638_449_176_6,
1636 0.118_194_531_961_518_4,
1637 0.101_930_119_817_240_4,
1638 0.083_276_741_576_704_75,
1639 0.062_672_048_334_109_06,
1640 0.040_601_429_800_386_94,
1641 0.017_614_007_139_152_12,
1642];
1643
1644fn dedup_sorted_tagged_breakpoints(points: &mut Vec<(f64, PartitionEdge)>) {
1650 points.sort_by(|lhs, rhs| {
1651 lhs.0
1652 .partial_cmp(&rhs.0)
1653 .unwrap_or(std::cmp::Ordering::Equal)
1654 });
1655 points.dedup_by(|lhs, rhs| {
1656 let coincide = if lhs.0 == rhs.0 {
1657 true
1658 } else if lhs.0.is_finite() && rhs.0.is_finite() {
1659 (lhs.0 - rhs.0).abs() <= 1e-12
1660 } else {
1661 false
1662 };
1663 if coincide && matches!(lhs.1, PartitionEdge::Fixed(_)) {
1664 rhs.1 = lhs.1;
1667 }
1668 coincide
1669 });
1670}
1671
1672#[inline]
1673pub fn interval_probe_point(left: f64, right: f64) -> Result<f64, String> {
1674 if !(left < right) {
1675 return Err(CubicCellKernelError::invalid_interval(format!(
1676 "interval probe requires ordered bounds, got [{left}, {right}]"
1677 ))
1678 .into());
1679 }
1680 if left.is_finite() && right.is_finite() {
1681 Ok(0.5 * (left + right))
1682 } else if left == f64::NEG_INFINITY && right == f64::INFINITY {
1683 Ok(0.0)
1684 } else if left == f64::NEG_INFINITY && right.is_finite() {
1685 Ok(right - 1.0)
1686 } else if left.is_finite() && right == f64::INFINITY {
1687 Ok(left + 1.0)
1688 } else {
1689 Err(CubicCellKernelError::invalid_interval(format!(
1690 "interval probe requires finite bounds or full infinities, got [{left}, {right}]"
1691 ))
1692 .into())
1693 }
1694}
1695
1696#[inline]
1697pub fn quartic_qprime_coefficients(c0: f64, c1: f64, c2: f64) -> [f64; 4] {
1698 [
1699 c0 * c1,
1700 1.0 + c1 * c1 + 2.0 * c0 * c2,
1701 3.0 * c1 * c2,
1702 2.0 * c2 * c2,
1703 ]
1704}
1705
1706#[inline]
1707pub fn sextic_qprime_coefficients(c0: f64, c1: f64, c2: f64, c3: f64) -> [f64; 6] {
1708 [
1709 c0 * c1,
1710 1.0 + c1 * c1 + 2.0 * c0 * c2,
1711 3.0 * c0 * c3 + 3.0 * c1 * c2,
1712 4.0 * c1 * c3 + 2.0 * c2 * c2,
1713 5.0 * c2 * c3,
1714 3.0 * c3 * c3,
1715 ]
1716}
1717
1718#[inline]
1723fn moment_boundary_term_with_powers(
1724 cell: DenestedCubicCell,
1725 left_pow_n: f64,
1726 right_pow_n: f64,
1727) -> f64 {
1728 let left_term = if cell.left.is_infinite() {
1729 0.0
1730 } else {
1731 left_pow_n * (-cell.q(cell.left)).exp()
1732 };
1733 let right_term = if cell.right.is_infinite() {
1734 0.0
1735 } else {
1736 right_pow_n * (-cell.q(cell.right)).exp()
1737 };
1738 right_term - left_term
1739}
1740
1741#[inline]
1742fn base_moments_match_direct(base: &[f64], direct: &[f64]) -> bool {
1743 base.iter()
1744 .zip(direct.iter())
1745 .all(|(&lhs, &rhs)| (lhs - rhs).abs() <= 1e-10 * (1.0 + lhs.abs().max(rhs.abs())))
1746}
1747
1748#[inline]
1749fn direct_non_affine_moments_if_base_matches(
1750 cell: DenestedCubicCell,
1751 base: &[f64],
1752 max_degree: usize,
1753) -> Option<Vec<f64>> {
1754 if !cell.left.is_finite() || !cell.right.is_finite() {
1755 return None;
1756 }
1757 let (moments, _) = evaluate_non_affine_cell_simd::<false>(cell, max_degree);
1765 if base_moments_match_direct(base, &moments) {
1766 Some(moments.into_vec())
1767 } else {
1768 None
1769 }
1770}
1771
1772pub fn reduce_quartic_moments(
1773 cell: DenestedCubicCell,
1774 base_m0_m2: [f64; 3],
1775 max_degree: usize,
1776) -> Result<Vec<f64>, String> {
1777 if max_degree <= 2 {
1778 return Ok(base_m0_m2[..=max_degree].to_vec());
1779 }
1780 if let Some(moments) = direct_non_affine_moments_if_base_matches(cell, &base_m0_m2, max_degree)
1781 {
1782 return Ok(moments);
1783 }
1784 let d = quartic_qprime_coefficients(cell.c0, cell.c1, cell.c2);
1785 let lead = d[3];
1786 if !lead.is_finite() || lead.abs() <= 1e-18 {
1787 return Err(CubicCellKernelError::invalid_cell_shape(format!(
1788 "quartic moment reduction requires nonzero leading coefficient, got {lead:.3e}"
1789 ))
1790 .into());
1791 }
1792 let mut moments = vec![0.0; max_degree + 1];
1793 moments[0] = base_m0_m2[0];
1794 moments[1] = base_m0_m2[1];
1795 moments[2] = base_m0_m2[2];
1796 let left_finite = cell.left.is_finite();
1801 let right_finite = cell.right.is_finite();
1802 let mut left_pow_n = if left_finite { 1.0 } else { 0.0 };
1803 let mut right_pow_n = if right_finite { 1.0 } else { 0.0 };
1804 for n in 0..=(max_degree - 3) {
1805 let b_n = moment_boundary_term_with_powers(cell, left_pow_n, right_pow_n);
1806 let mut numer = if n == 0 {
1807 0.0
1808 } else {
1809 (n as f64) * moments[n - 1]
1810 };
1811 for j in 0..=2 {
1812 numer -= d[j] * moments[n + j];
1813 }
1814 numer -= b_n;
1815 moments[n + 3] = numer / lead;
1816 if left_finite {
1817 left_pow_n *= cell.left;
1818 }
1819 if right_finite {
1820 right_pow_n *= cell.right;
1821 }
1822 }
1823 Ok(moments)
1824}
1825
1826pub fn reduce_sextic_moments(
1827 cell: DenestedCubicCell,
1828 base_m0_m4: [f64; 5],
1829 max_degree: usize,
1830) -> Result<Vec<f64>, String> {
1831 if max_degree <= 4 {
1832 return Ok(base_m0_m4[..=max_degree].to_vec());
1833 }
1834 if let Some(moments) = direct_non_affine_moments_if_base_matches(cell, &base_m0_m4, max_degree)
1835 {
1836 return Ok(moments);
1837 }
1838 let d = sextic_qprime_coefficients(cell.c0, cell.c1, cell.c2, cell.c3);
1839 let lead = d[5];
1840 if !lead.is_finite() {
1841 return Err(CubicCellKernelError::invalid_cell_shape(format!(
1842 "sextic moment reduction encountered non-finite leading coefficient: {lead:.3e}"
1843 ))
1844 .into());
1845 }
1846 if let Some(lower_branch) = degenerate_sextic_branch(cell, lead)? {
1847 if lower_branch == ExactCellBranch::Quartic {
1848 return evaluate_non_affine_cell_state(
1849 DenestedCubicCell { c3: 0.0, ..cell },
1850 ExactCellBranch::Quartic,
1851 max_degree,
1852 )
1853 .map(|state| state.moments.into_vec());
1854 }
1855 return evaluate_affine_cell_state(
1856 DenestedCubicCell {
1857 left: cell.left,
1858 right: cell.right,
1859 c0: cell.c0,
1860 c1: cell.c1,
1861 c2: 0.0,
1862 c3: 0.0,
1863 },
1864 max_degree,
1865 )
1866 .map(|state| state.moments.into_vec());
1867 }
1868 let mut moments = vec![0.0; max_degree + 1];
1869 for (idx, value) in base_m0_m4.into_iter().enumerate() {
1870 moments[idx] = value;
1871 }
1872 let left_finite = cell.left.is_finite();
1873 let right_finite = cell.right.is_finite();
1874 let mut left_pow_n = if left_finite { 1.0 } else { 0.0 };
1875 let mut right_pow_n = if right_finite { 1.0 } else { 0.0 };
1876 for n in 0..=(max_degree - 5) {
1877 let b_n = moment_boundary_term_with_powers(cell, left_pow_n, right_pow_n);
1878 let mut numer = if n == 0 {
1879 0.0
1880 } else {
1881 (n as f64) * moments[n - 1]
1882 };
1883 for j in 0..=4 {
1884 numer -= d[j] * moments[n + j];
1885 }
1886 numer -= b_n;
1887 moments[n + 5] = numer / lead;
1888 if left_finite {
1889 left_pow_n *= cell.left;
1890 }
1891 if right_finite {
1892 right_pow_n *= cell.right;
1893 }
1894 }
1895 Ok(moments)
1896}
1897
1898#[inline]
1899pub fn cell_first_derivative_from_moments(
1900 derivative_coefficients: &[f64],
1901 moments: &[f64],
1902) -> Result<f64, String> {
1903 let value = moment_dot_with_coefficients(derivative_coefficients, moments, "first derivative")?;
1904 Ok(value * INV_TWO_PI)
1905}
1906
1907#[inline]
1915pub fn cell_first_derivative_required_max_degree(derivative_coefficients: &[f64]) -> usize {
1916 derivative_coefficients.len().saturating_sub(1)
1917}
1918
1919#[inline]
1928pub fn cell_second_derivative_required_max_degree(
1929 first_coefficients_r: &[f64],
1930 first_coefficients_s: &[f64],
1931 second_coefficients_rs: &[f64],
1932) -> usize {
1933 let second_degree = second_coefficients_rs.len().saturating_sub(1);
1934 let product_degree = first_coefficients_r.len().saturating_sub(1)
1935 + first_coefficients_s.len().saturating_sub(1)
1936 + 3;
1937 second_degree.max(product_degree)
1938}
1939
1940#[inline]
1941pub fn cell_polynomial_integral_from_moments(
1942 polynomial_coefficients: &[f64],
1943 moments: &[f64],
1944 label: &str,
1945) -> Result<f64, String> {
1946 let value = moment_dot_with_coefficients(polynomial_coefficients, moments, label)?;
1947 Ok(value * INV_TWO_PI)
1948}
1949
1950#[inline]
1951pub fn cell_second_derivative_from_moments(
1952 cell: DenestedCubicCell,
1953 first_coefficients_r: &[f64],
1954 first_coefficients_s: &[f64],
1955 second_coefficients_rs: &[f64],
1956 moments: &[f64],
1957) -> Result<f64, String> {
1958 let second_degree = second_coefficients_rs.len().saturating_sub(1);
1959 let product_degree = first_coefficients_r.len().saturating_sub(1)
1960 + first_coefficients_s.len().saturating_sub(1)
1961 + 3;
1962 let needed = second_degree.max(product_degree) + 1;
1963 if needed > moments.len() {
1964 return Err(CubicCellKernelError::insufficient_moments(format!(
1965 "insufficient reduced moments for second derivative: need {}, have {}",
1966 needed,
1967 moments.len()
1968 ))
1969 .into());
1970 }
1971 let second_term = moment_dot_with_coefficients_unchecked(second_coefficients_rs, moments);
1972 let cubic = [cell.c0, cell.c1, cell.c2, cell.c3];
1979 const SCRATCH: usize = 32;
1983 let mut eta_r = [0.0_f64; SCRATCH];
1984 let mut eta_rs = [0.0_f64; SCRATCH];
1985 let er_len = poly_conv_into(&cubic, first_coefficients_r, &mut eta_r);
1986 let ers_len = poly_conv_into(&eta_r[..er_len], first_coefficients_s, &mut eta_rs);
1987 let mut eta_term = 0.0;
1988 for k in 0..ers_len {
1989 eta_term = eta_rs[k].mul_add(moments[k], eta_term);
1990 }
1991 Ok((second_term - eta_term) * INV_TWO_PI)
1992}
1993
1994#[inline]
2014pub fn cell_second_derivative_boundary_integrand(
2015 cell: DenestedCubicCell,
2016 first_coefficients_r: &[f64],
2017 first_coefficients_s: &[f64],
2018 second_coefficients_rs: &[f64],
2019 z: f64,
2020) -> f64 {
2021 let eta = cell.eta(z);
2022 let c_r = poly_eval_at(first_coefficients_r, z);
2023 let c_s = poly_eval_at(first_coefficients_s, z);
2024 let c_rs = poly_eval_at(second_coefficients_rs, z);
2025 (c_rs - eta * c_r * c_s) * (-cell.q(z)).exp() * INV_TWO_PI
2026}
2027
2028#[inline]
2050pub fn cell_third_derivative_boundary_integrand(
2051 cell: DenestedCubicCell,
2052 first_coefficients_r: &[f64],
2053 first_coefficients_s: &[f64],
2054 first_coefficients_t: &[f64],
2055 second_coefficients_rs: &[f64],
2056 second_coefficients_rt: &[f64],
2057 second_coefficients_st: &[f64],
2058 third_coefficients_rst: &[f64],
2059 z: f64,
2060) -> f64 {
2061 let eta = cell.eta(z);
2062 let c_r = poly_eval_at(first_coefficients_r, z);
2063 let c_s = poly_eval_at(first_coefficients_s, z);
2064 let c_t = poly_eval_at(first_coefficients_t, z);
2065 let c_rs = poly_eval_at(second_coefficients_rs, z);
2066 let c_rt = poly_eval_at(second_coefficients_rt, z);
2067 let c_st = poly_eval_at(second_coefficients_st, z);
2068 let c_rst = poly_eval_at(third_coefficients_rst, z);
2069 let amplitude =
2070 c_rst - eta * (c_rs * c_t + c_rt * c_s + c_st * c_r) + (eta * eta - 1.0) * c_r * c_s * c_t;
2071 amplitude * (-cell.q(z)).exp() * INV_TWO_PI
2072}
2073
2074pub fn cell_density_boundary_integrand(cell: DenestedCubicCell, g: &[f64], z: f64) -> f64 {
2088 poly_eval_at(g, z) * (-cell.q(z)).exp() * INV_TWO_PI
2089}
2090
2091#[inline]
2093fn poly_eval_at(coefficients: &[f64], z: f64) -> f64 {
2094 let mut acc = 0.0_f64;
2095 for &c in coefficients.iter().rev() {
2096 acc = acc.mul_add(z, c);
2097 }
2098 acc
2099}
2100
2101#[inline]
2102fn moment_dot_with_coefficients(
2103 coefficients: &[f64],
2104 moments: &[f64],
2105 label: &str,
2106) -> Result<f64, String> {
2107 if coefficients.len() > moments.len() {
2108 return Err(CubicCellKernelError::insufficient_moments(format!(
2109 "insufficient reduced moments for {label}: need {}, have {}",
2110 coefficients.len(),
2111 moments.len()
2112 ))
2113 .into());
2114 }
2115 Ok(moment_dot_with_coefficients_unchecked(
2116 coefficients,
2117 moments,
2118 ))
2119}
2120
2121#[inline]
2122fn moment_dot_with_coefficients_unchecked(coefficients: &[f64], moments: &[f64]) -> f64 {
2123 let mut acc = 0.0;
2124 for (idx, &coeff) in coefficients.iter().enumerate() {
2125 acc = coeff.mul_add(moments[idx], acc);
2126 }
2127 acc
2128}
2129
2130#[inline]
2140fn poly_conv_into(lhs: &[f64], rhs: &[f64], out: &mut [f64]) -> usize {
2141 if lhs.is_empty() || rhs.is_empty() {
2142 return 0;
2143 }
2144 let len = lhs.len() + rhs.len() - 1;
2145 assert!(out.len() >= len);
2146 for slot in out[..len].iter_mut() {
2147 *slot = 0.0;
2148 }
2149 for (i, &lv) in lhs.iter().enumerate() {
2150 for (j, &rv) in rhs.iter().enumerate() {
2151 out[i + j] = lv.mul_add(rv, out[i + j]);
2152 }
2153 }
2154 len
2155}
2156
2157#[inline]
2158fn require_moments_degree(
2159 required_degree: usize,
2160 moments: &[f64],
2161 label: &str,
2162) -> Result<(), String> {
2163 if required_degree >= moments.len() {
2164 return Err(CubicCellKernelError::insufficient_moments(format!(
2165 "insufficient reduced moments for {label}: need {}, have {}",
2166 required_degree + 1,
2167 moments.len()
2168 ))
2169 .into());
2170 }
2171 Ok::<(), _>(())
2172}
2173
2174#[inline]
2175fn require_scratch_capacity(
2176 required_len: usize,
2177 capacity: usize,
2178 label: &str,
2179) -> Result<(), String> {
2180 if required_len > capacity {
2181 return Err(CubicCellKernelError::insufficient_moments(format!(
2182 "{label} polynomial convolution scratch too small: need {required_len}, have {capacity}"
2183 ))
2184 .into());
2185 }
2186 Ok::<(), _>(())
2187}
2188
2189#[inline]
2190fn convolution_chain_len(lengths: &[usize]) -> usize {
2191 if lengths.is_empty() || lengths.contains(&0) {
2192 0
2193 } else {
2194 lengths.iter().sum::<usize>() - (lengths.len() - 1)
2195 }
2196}
2197
2198#[inline]
2199fn first_coefficients_degree(label: &str, coefficients: &[f64]) -> Result<usize, String> {
2200 coefficients
2201 .len()
2202 .checked_sub(1)
2203 .ok_or_else(|| format!("{label} first-derivative coefficients must be non-empty"))
2204}
2205
2206#[inline]
2207pub fn cell_third_derivative_from_moments(
2208 cell: DenestedCubicCell,
2209 first_coefficients_r: &[f64],
2210 first_coefficients_s: &[f64],
2211 first_coefficients_t: &[f64],
2212 second_coefficients_rs: &[f64],
2213 second_coefficients_rt: &[f64],
2214 second_coefficients_st: &[f64],
2215 third_coefficients_rst: &[f64],
2216 moments: &[f64],
2217) -> Result<f64, String> {
2218 let eta = [cell.c0, cell.c1, cell.c2, cell.c3];
2219 let r_degree = first_coefficients_degree("r", first_coefficients_r)?;
2220 let s_degree = first_coefficients_degree("s", first_coefficients_s)?;
2221 let t_degree = first_coefficients_degree("t", first_coefficients_t)?;
2222 let second_sum_degree = [
2223 second_coefficients_rs.len() + first_coefficients_t.len(),
2224 second_coefficients_rt.len() + first_coefficients_s.len(),
2225 second_coefficients_st.len() + first_coefficients_r.len(),
2226 ]
2227 .into_iter()
2228 .max()
2229 .unwrap_or(0)
2230 .saturating_sub(1);
2231 let triple_product_degree = r_degree + s_degree + t_degree;
2232 let needed = (third_coefficients_rst.len().saturating_sub(1))
2233 .max(3 + second_sum_degree)
2234 .max(6 + triple_product_degree);
2235 require_moments_degree(needed, moments, "third derivative")?;
2236
2237 let third_term = moment_dot_with_coefficients_unchecked(third_coefficients_rst, moments);
2238
2239 const SCRATCH: usize = 32;
2243 let max_linear_conv_len = [
2244 convolution_chain_len(&[
2245 eta.len(),
2246 second_coefficients_rs.len(),
2247 first_coefficients_t.len(),
2248 ]),
2249 convolution_chain_len(&[
2250 eta.len(),
2251 second_coefficients_rt.len(),
2252 first_coefficients_s.len(),
2253 ]),
2254 convolution_chain_len(&[
2255 eta.len(),
2256 second_coefficients_st.len(),
2257 first_coefficients_r.len(),
2258 ]),
2259 ]
2260 .into_iter()
2261 .max()
2262 .unwrap_or(0);
2263 let max_cubic_conv_len = convolution_chain_len(&[
2264 7,
2265 first_coefficients_r.len(),
2266 first_coefficients_s.len(),
2267 first_coefficients_t.len(),
2268 ]);
2269 require_scratch_capacity(
2270 max_linear_conv_len.max(max_cubic_conv_len),
2271 SCRATCH,
2272 "third derivative",
2273 )?;
2274 let mut buf_a = [0.0_f64; SCRATCH];
2275 let mut buf_b = [0.0_f64; SCRATCH];
2276
2277 let mut eta_second_term = 0.0;
2280 let conv_dot = |first: &[f64],
2281 second: &[f64],
2282 buf_a: &mut [f64; SCRATCH],
2283 buf_b: &mut [f64; SCRATCH]|
2284 -> f64 {
2285 let m = poly_conv_into(first, second, buf_a);
2286 let n = poly_conv_into(&eta, &buf_a[..m], buf_b);
2287 let mut acc = 0.0;
2288 for k in 0..n {
2289 acc = buf_b[k].mul_add(moments[k], acc);
2290 }
2291 acc
2292 };
2293 eta_second_term += conv_dot(
2294 second_coefficients_rs,
2295 first_coefficients_t,
2296 &mut buf_a,
2297 &mut buf_b,
2298 );
2299 eta_second_term += conv_dot(
2300 second_coefficients_rt,
2301 first_coefficients_s,
2302 &mut buf_a,
2303 &mut buf_b,
2304 );
2305 eta_second_term += conv_dot(
2306 second_coefficients_st,
2307 first_coefficients_r,
2308 &mut buf_a,
2309 &mut buf_b,
2310 );
2311
2312 let mut eta_sq_minus_one = [0.0_f64; 7];
2315 for (i, &eta_i) in eta.iter().enumerate() {
2316 for (j, &eta_j) in eta.iter().enumerate() {
2317 eta_sq_minus_one[i + j] = eta_i.mul_add(eta_j, eta_sq_minus_one[i + j]);
2318 }
2319 }
2320 eta_sq_minus_one[0] -= 1.0;
2321
2322 let rs_len = poly_conv_into(first_coefficients_r, first_coefficients_s, &mut buf_a);
2323 let rst_len = poly_conv_into(&buf_a[..rs_len], first_coefficients_t, &mut buf_b);
2324 let final_len = poly_conv_into(&eta_sq_minus_one, &buf_b[..rst_len], &mut buf_a);
2326 let mut cubic_coeff_term = 0.0;
2327 for k in 0..final_len {
2328 cubic_coeff_term = buf_a[k].mul_add(moments[k], cubic_coeff_term);
2329 }
2330
2331 Ok((third_term - eta_second_term + cubic_coeff_term) * INV_TWO_PI)
2332}
2333
2334#[inline]
2335pub fn cell_fourth_derivative_from_moments(
2336 cell: DenestedCubicCell,
2337 first_coefficients_r: &[f64],
2338 first_coefficients_s: &[f64],
2339 first_coefficients_t: &[f64],
2340 first_coefficients_u: &[f64],
2341 second_coefficients_rs: &[f64],
2342 second_coefficients_rt: &[f64],
2343 second_coefficients_ru: &[f64],
2344 second_coefficients_st: &[f64],
2345 second_coefficients_su: &[f64],
2346 second_coefficients_tu: &[f64],
2347 third_coefficients_rst: &[f64],
2348 third_coefficients_rsu: &[f64],
2349 third_coefficients_rtu: &[f64],
2350 third_coefficients_stu: &[f64],
2351 fourth_coefficients_rstu: &[f64],
2352 moments: &[f64],
2353) -> Result<f64, String> {
2354 let eta = [cell.c0, cell.c1, cell.c2, cell.c3];
2355 let r_degree = first_coefficients_degree("r", first_coefficients_r)?;
2356 let s_degree = first_coefficients_degree("s", first_coefficients_s)?;
2357 let t_degree = first_coefficients_degree("t", first_coefficients_t)?;
2358 let u_degree = first_coefficients_degree("u", first_coefficients_u)?;
2359 let linear_sum_degree = [
2360 third_coefficients_rst.len() + first_coefficients_u.len(),
2361 third_coefficients_rsu.len() + first_coefficients_t.len(),
2362 third_coefficients_rtu.len() + first_coefficients_s.len(),
2363 third_coefficients_stu.len() + first_coefficients_r.len(),
2364 second_coefficients_rs.len() + second_coefficients_tu.len(),
2365 second_coefficients_rt.len() + second_coefficients_su.len(),
2366 second_coefficients_ru.len() + second_coefficients_st.len(),
2367 ]
2368 .into_iter()
2369 .max()
2370 .unwrap_or(0)
2371 .saturating_sub(1);
2372 let quad_sum_degree = [
2373 second_coefficients_rs.len() + first_coefficients_t.len() + first_coefficients_u.len(),
2374 second_coefficients_rt.len() + first_coefficients_s.len() + first_coefficients_u.len(),
2375 second_coefficients_ru.len() + first_coefficients_s.len() + first_coefficients_t.len(),
2376 second_coefficients_st.len() + first_coefficients_r.len() + first_coefficients_u.len(),
2377 second_coefficients_su.len() + first_coefficients_r.len() + first_coefficients_t.len(),
2378 second_coefficients_tu.len() + first_coefficients_r.len() + first_coefficients_s.len(),
2379 ]
2380 .into_iter()
2381 .max()
2382 .unwrap_or(0)
2383 .saturating_sub(2);
2384 let quartic_product_degree = r_degree + s_degree + t_degree + u_degree;
2385 let needed = (fourth_coefficients_rstu.len().saturating_sub(1))
2386 .max(3 + linear_sum_degree)
2387 .max(6 + quad_sum_degree)
2388 .max(9 + quartic_product_degree);
2389 require_moments_degree(needed, moments, "fourth derivative")?;
2390
2391 let fourth_term = moment_dot_with_coefficients_unchecked(fourth_coefficients_rstu, moments);
2392
2393 const SCRATCH: usize = 32;
2397 let max_linear_conv_len = [
2398 convolution_chain_len(&[
2399 eta.len(),
2400 third_coefficients_rst.len(),
2401 first_coefficients_u.len(),
2402 ]),
2403 convolution_chain_len(&[
2404 eta.len(),
2405 third_coefficients_rsu.len(),
2406 first_coefficients_t.len(),
2407 ]),
2408 convolution_chain_len(&[
2409 eta.len(),
2410 third_coefficients_rtu.len(),
2411 first_coefficients_s.len(),
2412 ]),
2413 convolution_chain_len(&[
2414 eta.len(),
2415 third_coefficients_stu.len(),
2416 first_coefficients_r.len(),
2417 ]),
2418 convolution_chain_len(&[
2419 eta.len(),
2420 second_coefficients_rs.len(),
2421 second_coefficients_tu.len(),
2422 ]),
2423 convolution_chain_len(&[
2424 eta.len(),
2425 second_coefficients_rt.len(),
2426 second_coefficients_su.len(),
2427 ]),
2428 convolution_chain_len(&[
2429 eta.len(),
2430 second_coefficients_ru.len(),
2431 second_coefficients_st.len(),
2432 ]),
2433 ]
2434 .into_iter()
2435 .max()
2436 .unwrap_or(0);
2437 let max_quad_conv_len = [
2438 convolution_chain_len(&[
2439 7,
2440 second_coefficients_rs.len(),
2441 first_coefficients_t.len(),
2442 first_coefficients_u.len(),
2443 ]),
2444 convolution_chain_len(&[
2445 7,
2446 second_coefficients_rt.len(),
2447 first_coefficients_s.len(),
2448 first_coefficients_u.len(),
2449 ]),
2450 convolution_chain_len(&[
2451 7,
2452 second_coefficients_ru.len(),
2453 first_coefficients_s.len(),
2454 first_coefficients_t.len(),
2455 ]),
2456 convolution_chain_len(&[
2457 7,
2458 second_coefficients_st.len(),
2459 first_coefficients_r.len(),
2460 first_coefficients_u.len(),
2461 ]),
2462 convolution_chain_len(&[
2463 7,
2464 second_coefficients_su.len(),
2465 first_coefficients_r.len(),
2466 first_coefficients_t.len(),
2467 ]),
2468 convolution_chain_len(&[
2469 7,
2470 second_coefficients_tu.len(),
2471 first_coefficients_r.len(),
2472 first_coefficients_s.len(),
2473 ]),
2474 ]
2475 .into_iter()
2476 .max()
2477 .unwrap_or(0);
2478 let max_quartic_conv_len = convolution_chain_len(&[
2479 10,
2480 first_coefficients_r.len(),
2481 first_coefficients_s.len(),
2482 first_coefficients_t.len(),
2483 first_coefficients_u.len(),
2484 ]);
2485 require_scratch_capacity(
2486 max_linear_conv_len
2487 .max(max_quad_conv_len)
2488 .max(max_quartic_conv_len),
2489 SCRATCH,
2490 "fourth derivative",
2491 )?;
2492 let mut buf_a = [0.0_f64; SCRATCH];
2493 let mut buf_b = [0.0_f64; SCRATCH];
2494
2495 let conv_eta_dot = |first: &[f64],
2499 second: &[f64],
2500 buf_a: &mut [f64; SCRATCH],
2501 buf_b: &mut [f64; SCRATCH]|
2502 -> f64 {
2503 let m = poly_conv_into(first, second, buf_a);
2504 let n = poly_conv_into(&eta, &buf_a[..m], buf_b);
2505 let mut acc = 0.0;
2506 for k in 0..n {
2507 acc = buf_b[k].mul_add(moments[k], acc);
2508 }
2509 acc
2510 };
2511 let mut eta_linear_term = 0.0;
2512 eta_linear_term += conv_eta_dot(
2513 third_coefficients_rst,
2514 first_coefficients_u,
2515 &mut buf_a,
2516 &mut buf_b,
2517 );
2518 eta_linear_term += conv_eta_dot(
2519 third_coefficients_rsu,
2520 first_coefficients_t,
2521 &mut buf_a,
2522 &mut buf_b,
2523 );
2524 eta_linear_term += conv_eta_dot(
2525 third_coefficients_rtu,
2526 first_coefficients_s,
2527 &mut buf_a,
2528 &mut buf_b,
2529 );
2530 eta_linear_term += conv_eta_dot(
2531 third_coefficients_stu,
2532 first_coefficients_r,
2533 &mut buf_a,
2534 &mut buf_b,
2535 );
2536 eta_linear_term += conv_eta_dot(
2537 second_coefficients_rs,
2538 second_coefficients_tu,
2539 &mut buf_a,
2540 &mut buf_b,
2541 );
2542 eta_linear_term += conv_eta_dot(
2543 second_coefficients_rt,
2544 second_coefficients_su,
2545 &mut buf_a,
2546 &mut buf_b,
2547 );
2548 eta_linear_term += conv_eta_dot(
2549 second_coefficients_ru,
2550 second_coefficients_st,
2551 &mut buf_a,
2552 &mut buf_b,
2553 );
2554
2555 let mut eta_sq_minus_one = [0.0_f64; 7];
2556 for (i, &eta_i) in eta.iter().enumerate() {
2557 for (j, &eta_j) in eta.iter().enumerate() {
2558 eta_sq_minus_one[i + j] = eta_i.mul_add(eta_j, eta_sq_minus_one[i + j]);
2559 }
2560 }
2561 eta_sq_minus_one[0] -= 1.0;
2562
2563 let mut buf_c = [0.0_f64; SCRATCH];
2566 let conv_weighted_triple_dot = |weight: &[f64],
2567 a: &[f64],
2568 b: &[f64],
2569 c: &[f64],
2570 buf_a: &mut [f64; SCRATCH],
2571 buf_b: &mut [f64; SCRATCH],
2572 buf_c: &mut [f64; SCRATCH]|
2573 -> f64 {
2574 let ab_len = poly_conv_into(a, b, buf_a);
2575 let abc_len = poly_conv_into(&buf_a[..ab_len], c, buf_b);
2576 let final_len = poly_conv_into(weight, &buf_b[..abc_len], buf_c);
2577 let mut acc = 0.0;
2578 for k in 0..final_len {
2579 acc = buf_c[k].mul_add(moments[k], acc);
2580 }
2581 acc
2582 };
2583 let mut quad_coeff_term = 0.0;
2584 quad_coeff_term += conv_weighted_triple_dot(
2585 &eta_sq_minus_one,
2586 second_coefficients_rs,
2587 first_coefficients_t,
2588 first_coefficients_u,
2589 &mut buf_a,
2590 &mut buf_b,
2591 &mut buf_c,
2592 );
2593 quad_coeff_term += conv_weighted_triple_dot(
2594 &eta_sq_minus_one,
2595 second_coefficients_rt,
2596 first_coefficients_s,
2597 first_coefficients_u,
2598 &mut buf_a,
2599 &mut buf_b,
2600 &mut buf_c,
2601 );
2602 quad_coeff_term += conv_weighted_triple_dot(
2603 &eta_sq_minus_one,
2604 second_coefficients_ru,
2605 first_coefficients_s,
2606 first_coefficients_t,
2607 &mut buf_a,
2608 &mut buf_b,
2609 &mut buf_c,
2610 );
2611 quad_coeff_term += conv_weighted_triple_dot(
2612 &eta_sq_minus_one,
2613 second_coefficients_st,
2614 first_coefficients_r,
2615 first_coefficients_u,
2616 &mut buf_a,
2617 &mut buf_b,
2618 &mut buf_c,
2619 );
2620 quad_coeff_term += conv_weighted_triple_dot(
2621 &eta_sq_minus_one,
2622 second_coefficients_su,
2623 first_coefficients_r,
2624 first_coefficients_t,
2625 &mut buf_a,
2626 &mut buf_b,
2627 &mut buf_c,
2628 );
2629 quad_coeff_term += conv_weighted_triple_dot(
2630 &eta_sq_minus_one,
2631 second_coefficients_tu,
2632 first_coefficients_r,
2633 first_coefficients_s,
2634 &mut buf_a,
2635 &mut buf_b,
2636 &mut buf_c,
2637 );
2638
2639 let mut eta_sq = [0.0_f64; 7];
2642 for (i, &eta_i) in eta.iter().enumerate() {
2643 for (j, &eta_j) in eta.iter().enumerate() {
2644 eta_sq[i + j] = eta_i.mul_add(eta_j, eta_sq[i + j]);
2645 }
2646 }
2647 let mut cubic_weight = [0.0_f64; 10];
2648 for (i, &eta_sq_i) in eta_sq.iter().enumerate() {
2649 for (j, &eta_j) in eta.iter().enumerate() {
2650 cubic_weight[i + j] = (-eta_sq_i).mul_add(eta_j, cubic_weight[i + j]);
2651 }
2652 }
2653 for (idx, &eta_coeff) in eta.iter().enumerate() {
2654 cubic_weight[idx] += 3.0 * eta_coeff;
2655 }
2656
2657 let rs_len = poly_conv_into(first_coefficients_r, first_coefficients_s, &mut buf_a);
2662 let rst_len = poly_conv_into(&buf_a[..rs_len], first_coefficients_t, &mut buf_b);
2663 let rstu_len = poly_conv_into(&buf_b[..rst_len], first_coefficients_u, &mut buf_a);
2664 let final_len = poly_conv_into(&cubic_weight, &buf_a[..rstu_len], &mut buf_b);
2665 let mut quartic_coeff_term = 0.0;
2666 for k in 0..final_len {
2667 quartic_coeff_term = buf_b[k].mul_add(moments[k], quartic_coeff_term);
2668 }
2669
2670 Ok((fourth_term - eta_linear_term + quad_coeff_term + quartic_coeff_term) * INV_TWO_PI)
2671}
2672
2673#[inline]
2674pub fn global_cubic_from_local(span: LocalSpanCubic) -> (f64, f64, f64, f64) {
2675 let left = span.left;
2676 let q0 = span.c0 - span.c1 * left + span.c2 * left * left - span.c3 * left * left * left;
2677 let q1 = span.c1 - 2.0 * span.c2 * left + 3.0 * span.c3 * left * left;
2678 let q2 = span.c2 - 3.0 * span.c3 * left;
2679 let q3 = span.c3;
2680 (q0, q1, q2, q3)
2681}
2682
2683#[inline]
2707pub fn transformed_link_cubic(link_span: LocalSpanCubic, a: f64, b: f64) -> (f64, f64, f64, f64) {
2708 let shift = a - link_span.left;
2709 let d0 = link_span.c0
2710 + link_span.c1 * shift
2711 + link_span.c2 * shift * shift
2712 + link_span.c3 * shift * shift * shift;
2713 let d1 = b * (link_span.c1 + 2.0 * link_span.c2 * shift + 3.0 * link_span.c3 * shift * shift);
2714 let d2 = b * b * (link_span.c2 + 3.0 * link_span.c3 * shift);
2715 let d3 = link_span.c3 * b * b * b;
2716 (d0, d1, d2, d3)
2717}
2718
2719#[inline]
2720pub fn denested_cell_coefficients(
2721 score_span: LocalSpanCubic,
2722 link_span: LocalSpanCubic,
2723 a: f64,
2724 b: f64,
2725) -> [f64; 4] {
2726 let (h0, h1, h2, h3) = global_cubic_from_local(score_span);
2727 let (d0, d1, d2, d3) = transformed_link_cubic(link_span, a, b);
2728 [a + b * h0 + d0, b + b * h1 + d1, b * h2 + d2, b * h3 + d3]
2729}
2730
2731#[inline]
2732pub fn denested_cell_coefficient_partials(
2733 score_span: LocalSpanCubic,
2734 link_span: LocalSpanCubic,
2735 a: f64,
2736 b: f64,
2737) -> ([f64; 4], [f64; 4]) {
2738 let (h0, h1, h2, h3) = global_cubic_from_local(score_span);
2739 let shift = a - link_span.left;
2740 let alpha1 = link_span.c1;
2741 let alpha2 = link_span.c2;
2742 let alpha3 = link_span.c3;
2743 let dc_da = [
2744 1.0 + alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2745 b * (2.0 * alpha2 + 6.0 * alpha3 * shift),
2746 3.0 * alpha3 * b * b,
2747 0.0,
2748 ];
2749 let dc_db = [
2750 h0,
2751 1.0 + h1 + alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2752 h2 + 2.0 * b * (alpha2 + 3.0 * alpha3 * shift),
2753 h3 + 3.0 * alpha3 * b * b,
2754 ];
2755 (dc_da, dc_db)
2756}
2757
2758#[inline]
2759fn link_cubic_second_partials(
2760 link_span: LocalSpanCubic,
2761 a: f64,
2762 b: f64,
2763) -> ([f64; 4], [f64; 4], [f64; 4]) {
2764 let shift = a - link_span.left;
2765 let alpha2 = link_span.c2;
2766 let alpha3 = link_span.c3;
2767 let dc_daa = [
2768 2.0 * alpha2 + 6.0 * alpha3 * shift,
2769 6.0 * alpha3 * b,
2770 0.0,
2771 0.0,
2772 ];
2773 let dc_dab = [
2774 0.0,
2775 2.0 * alpha2 + 6.0 * alpha3 * shift,
2776 6.0 * alpha3 * b,
2777 0.0,
2778 ];
2779 let dc_dbb = [
2780 0.0,
2781 0.0,
2782 2.0 * (alpha2 + 3.0 * alpha3 * shift),
2783 6.0 * alpha3 * b,
2784 ];
2785 (dc_daa, dc_dab, dc_dbb)
2786}
2787
2788#[inline]
2789pub fn denested_cell_second_partials(
2790 score_span: LocalSpanCubic,
2791 link_span: LocalSpanCubic,
2792 a: f64,
2793 b: f64,
2794) -> ([f64; 4], [f64; 4], [f64; 4]) {
2795 let score_left = score_span.left;
2796 if !score_left.is_finite() {
2797 return ([f64::NAN; 4], [f64::NAN; 4], [f64::NAN; 4]);
2798 }
2799 link_cubic_second_partials(link_span, a, b)
2800}
2801
2802#[inline]
2803fn link_cubic_third_partials(
2804 link_span: LocalSpanCubic,
2805) -> ([f64; 4], [f64; 4], [f64; 4], [f64; 4]) {
2806 let alpha3 = link_span.c3;
2807 (
2808 [6.0 * alpha3, 0.0, 0.0, 0.0],
2809 [0.0, 6.0 * alpha3, 0.0, 0.0],
2810 [0.0, 0.0, 6.0 * alpha3, 0.0],
2811 [0.0, 0.0, 0.0, 6.0 * alpha3],
2812 )
2813}
2814
2815#[inline]
2816pub fn denested_cell_third_partials(
2817 link_span: LocalSpanCubic,
2818) -> ([f64; 4], [f64; 4], [f64; 4], [f64; 4]) {
2819 link_cubic_third_partials(link_span)
2820}
2821
2822#[inline]
2823pub fn score_basis_cell_coefficients(score_basis_span: LocalSpanCubic, b: f64) -> [f64; 4] {
2824 let (h0, h1, h2, h3) = global_cubic_from_local(score_basis_span);
2825 [b * h0, b * h1, b * h2, b * h3]
2826}
2827
2828#[inline]
2829pub fn link_basis_cell_coefficients(link_basis_span: LocalSpanCubic, a: f64, b: f64) -> [f64; 4] {
2830 let (d0, d1, d2, d3) = transformed_link_cubic(link_basis_span, a, b);
2831 [d0, d1, d2, d3]
2832}
2833
2834#[inline]
2835pub fn link_basis_cell_coefficient_partials(
2836 link_basis_span: LocalSpanCubic,
2837 a: f64,
2838 b: f64,
2839) -> ([f64; 4], [f64; 4]) {
2840 let shift = a - link_basis_span.left;
2841 let alpha1 = link_basis_span.c1;
2842 let alpha2 = link_basis_span.c2;
2843 let alpha3 = link_basis_span.c3;
2844 let dc_da = [
2845 alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2846 b * (2.0 * alpha2 + 6.0 * alpha3 * shift),
2847 3.0 * alpha3 * b * b,
2848 0.0,
2849 ];
2850 let dc_db = [
2851 0.0,
2852 alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2853 2.0 * b * (alpha2 + 3.0 * alpha3 * shift),
2854 3.0 * alpha3 * b * b,
2855 ];
2856 (dc_da, dc_db)
2857}
2858
2859#[inline]
2860pub fn link_basis_cell_second_partials(
2861 link_basis_span: LocalSpanCubic,
2862 a: f64,
2863 b: f64,
2864) -> ([f64; 4], [f64; 4], [f64; 4]) {
2865 link_cubic_second_partials(link_basis_span, a, b)
2866}
2867
2868#[inline]
2869pub fn link_basis_cell_third_partials(
2870 link_basis_span: LocalSpanCubic,
2871) -> ([f64; 4], [f64; 4], [f64; 4], [f64; 4]) {
2872 link_cubic_third_partials(link_basis_span)
2873}
2874
2875pub fn build_denested_partition_cells<FS, FL>(
2876 a: f64,
2877 b: f64,
2878 score_breaks: &[f64],
2879 link_breaks: &[f64],
2880 score_span_at: FS,
2881 link_span_at: FL,
2882) -> Result<Vec<DenestedPartitionCell>, String>
2883where
2884 FS: FnMut(f64) -> Result<LocalSpanCubic, String>,
2885 FL: FnMut(f64) -> Result<LocalSpanCubic, String>,
2886{
2887 build_denested_partition_cells_with_tails(
2888 a,
2889 b,
2890 score_breaks,
2891 link_breaks,
2892 score_span_at,
2893 link_span_at,
2894 )
2895}
2896
2897pub fn build_denested_partition_cells_with_tails<FS, FL>(
2906 a: f64,
2907 b: f64,
2908 score_breaks: &[f64],
2909 link_breaks: &[f64],
2910 mut score_span_at: FS,
2911 mut link_span_at: FL,
2912) -> Result<Vec<DenestedPartitionCell>, String>
2913where
2914 FS: FnMut(f64) -> Result<LocalSpanCubic, String>,
2915 FL: FnMut(f64) -> Result<LocalSpanCubic, String>,
2916{
2917 let mut split_points: Vec<(f64, PartitionEdge)> = score_breaks
2922 .iter()
2923 .map(|&sigma| (sigma, PartitionEdge::Fixed(sigma)))
2924 .collect();
2925 if b.abs() > 1e-12 {
2926 for &tau in link_breaks {
2927 let z = (tau - a) / b;
2928 if z.is_finite() {
2929 split_points.push((z, PartitionEdge::Crossing { tau }));
2930 }
2931 }
2932 }
2933 dedup_sorted_tagged_breakpoints(&mut split_points);
2934
2935 let mut out = Vec::new();
2936
2937 if split_points.is_empty() {
2938 let score_span = score_span_at(0.0)?;
2939 let link_span = link_span_at(a)?;
2940 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
2941 return Ok(vec![DenestedPartitionCell {
2942 cell: DenestedCubicCell {
2943 left: f64::NEG_INFINITY,
2944 right: f64::INFINITY,
2945 c0: coeffs[0],
2946 c1: coeffs[1],
2947 c2: 0.0,
2948 c3: 0.0,
2949 },
2950 score_span,
2951 link_span,
2952 left_edge: PartitionEdge::Fixed(f64::NEG_INFINITY),
2953 right_edge: PartitionEdge::Fixed(f64::INFINITY),
2954 }]);
2955 }
2956
2957 let (leftmost, leftmost_edge) = split_points[0];
2959 let left_probe = interval_probe_point(f64::NEG_INFINITY, leftmost)?;
2962 let left_score_span = score_span_at(left_probe)?;
2963 let left_link_span = link_span_at(a + b * left_probe)?;
2964 let left_coeffs = denested_cell_coefficients(left_score_span, left_link_span, a, b);
2965 if left_coeffs[2].abs() > NORMALIZED_CELL_BRANCH_TOL
2966 || left_coeffs[3].abs() > NORMALIZED_CELL_BRANCH_TOL
2967 {
2968 return Err(CubicCellKernelError::invalid_cell_shape(format!(
2969 "left tail cell must be affine (deviations constant outside support), \
2970 got c2={:.3e}, c3={:.3e}",
2971 left_coeffs[2], left_coeffs[3]
2972 ))
2973 .into());
2974 }
2975 out.push(DenestedPartitionCell {
2976 cell: DenestedCubicCell {
2977 left: f64::NEG_INFINITY,
2978 right: leftmost,
2979 c0: left_coeffs[0],
2980 c1: left_coeffs[1],
2981 c2: 0.0,
2982 c3: 0.0,
2983 },
2984 score_span: left_score_span,
2985 link_span: left_link_span,
2986 left_edge: PartitionEdge::Fixed(f64::NEG_INFINITY),
2987 right_edge: leftmost_edge,
2988 });
2989
2990 for window in split_points.windows(2) {
2992 let (left, left_edge) = window[0];
2993 let (right, right_edge) = window[1];
2994 if !left.is_finite() || !right.is_finite() || right - left <= 1e-12 {
2995 continue;
2996 }
2997 let mid = interval_probe_point(left, right)?;
2998 let score_span = score_span_at(mid)?;
2999 let link_span = link_span_at(a + b * mid)?;
3000 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
3001 out.push(DenestedPartitionCell {
3002 cell: DenestedCubicCell {
3003 left,
3004 right,
3005 c0: coeffs[0],
3006 c1: coeffs[1],
3007 c2: coeffs[2],
3008 c3: coeffs[3],
3009 },
3010 score_span,
3011 link_span,
3012 left_edge,
3013 right_edge,
3014 });
3015 }
3016
3017 let (rightmost, rightmost_edge) = *split_points.last().unwrap();
3019 let right_probe = interval_probe_point(rightmost, f64::INFINITY)?;
3020 let right_score_span = score_span_at(right_probe)?;
3021 let right_link_span = link_span_at(a + b * right_probe)?;
3022 let right_coeffs = denested_cell_coefficients(right_score_span, right_link_span, a, b);
3023 if right_coeffs[2].abs() > NORMALIZED_CELL_BRANCH_TOL
3024 || right_coeffs[3].abs() > NORMALIZED_CELL_BRANCH_TOL
3025 {
3026 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3027 "right tail cell must be affine (deviations constant outside support), \
3028 got c2={:.3e}, c3={:.3e}",
3029 right_coeffs[2], right_coeffs[3]
3030 ))
3031 .into());
3032 }
3033 out.push(DenestedPartitionCell {
3034 cell: DenestedCubicCell {
3035 left: rightmost,
3036 right: f64::INFINITY,
3037 c0: right_coeffs[0],
3038 c1: right_coeffs[1],
3039 c2: 0.0,
3040 c3: 0.0,
3041 },
3042 score_span: right_score_span,
3043 link_span: right_link_span,
3044 left_edge: rightmost_edge,
3045 right_edge: PartitionEdge::Fixed(f64::INFINITY),
3046 });
3047
3048 Ok(out)
3049}
3050
3051#[inline]
3052pub fn normalized_non_affine_coefficients(
3053 left: f64,
3054 right: f64,
3055 c0: f64,
3056 c1: f64,
3057 c2: f64,
3058 c3: f64,
3059) -> Result<(f64, f64), String> {
3060 let width = right - left;
3061 if !width.is_finite() || width <= 0.0 {
3062 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3063 "normalized cubic coefficients require a positive finite cell width, got left={left}, right={right}"
3064 ))
3065 .into());
3066 }
3067 let anchor_scale = c0.abs() + c1.abs();
3068 if !anchor_scale.is_finite() {
3069 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3070 "normalized cubic coefficients require finite affine coefficients, got c0={c0}, c1={c1}"
3071 ))
3072 .into());
3073 }
3074 let mid = 0.5 * (left + right);
3075 let half = 0.5 * width;
3076 let k2 = half * half * (c2 + 3.0 * c3 * mid);
3077 let k3 = c3 * half * half * half;
3078 Ok((k2, k3))
3079}
3080
3081#[inline]
3082pub fn branch_cell(cell: DenestedCubicCell) -> Result<ExactCellBranch, String> {
3083 let tol = effective_branch_tol(cell);
3084 if !cell.left.is_finite() || !cell.right.is_finite() {
3085 if cell.c2.abs() <= tol && cell.c3.abs() <= tol {
3086 return Ok(ExactCellBranch::Affine);
3087 }
3088 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3089 "non-affine cells require finite bounds, got [{}, {}] with c2={:.6e}, c3={:.6e}",
3090 cell.left, cell.right, cell.c2, cell.c3
3091 ))
3092 .into());
3093 }
3094 let (k2, k3) = normalized_non_affine_coefficients(
3095 cell.left, cell.right, cell.c0, cell.c1, cell.c2, cell.c3,
3096 )?;
3097 if k2.abs() <= tol && k3.abs() <= tol {
3098 Ok(ExactCellBranch::Affine)
3099 } else if k3.abs() <= tol {
3100 Ok(ExactCellBranch::Quartic)
3101 } else {
3102 Ok(ExactCellBranch::Sextic)
3103 }
3104}
3105
3106#[inline]
3107fn degenerate_sextic_branch(
3108 cell: DenestedCubicCell,
3109 lead: f64,
3110) -> Result<Option<ExactCellBranch>, String> {
3111 let (normalized_k2, normalized_k3) = normalized_non_affine_coefficients(
3115 cell.left, cell.right, cell.c0, cell.c1, cell.c2, cell.c3,
3116 )?;
3117 if normalized_k3.abs() > NORMALIZED_CELL_BRANCH_TOL && lead.abs() > 1e-18 {
3118 return Ok(None);
3119 }
3120 if normalized_k2.abs() > NORMALIZED_CELL_BRANCH_TOL {
3121 Ok(Some(ExactCellBranch::Quartic))
3122 } else {
3123 Ok(Some(ExactCellBranch::Affine))
3124 }
3125}
3126
3127#[inline]
3128fn validate_bvn_args(h: f64, k: f64, rho: f64) -> Result<(), String> {
3129 if !h.is_finite() && !h.is_infinite() {
3130 return Err(CubicCellKernelError::bivariate_normal_domain(
3131 "bivariate normal cdf requires finite or infinite h",
3132 )
3133 .into());
3134 }
3135 if !k.is_finite() && !k.is_infinite() {
3136 return Err(CubicCellKernelError::bivariate_normal_domain(
3137 "bivariate normal cdf requires finite or infinite k",
3138 )
3139 .into());
3140 }
3141 if !rho.is_finite() {
3142 return Err(CubicCellKernelError::bivariate_normal_domain(format!(
3143 "bivariate normal cdf requires finite correlation, got {rho}"
3144 ))
3145 .into());
3146 }
3147 Ok::<(), _>(())
3148}
3149
3150#[inline]
3151fn bvn_gl_sum(h: f64, k: f64, rho_clamped: f64, asr: f64) -> f64 {
3152 if rho_clamped == 0.0 {
3159 return 0.0;
3160 }
3161 let hs = 0.5 * (h * h + k * k);
3162 let hk = h * k;
3163 let half_asr = 0.5 * asr;
3164 let (sin_mid, cos_mid) = half_asr.sin_cos();
3165 let mut sum = 0.0;
3166 for i in 0..10 {
3167 let node = GL20_NODES[i].abs();
3168 let weight = GL20_WEIGHTS[i];
3169 let (sin_delta, cos_delta) = (half_asr * node).sin_cos();
3170
3171 let sn_lo = sin_mid * cos_delta - cos_mid * sin_delta;
3172 let one_minus_lo = 1.0 - sn_lo * sn_lo;
3173 let expo_lo = ((sn_lo * hk) - hs) / one_minus_lo;
3174
3175 let sn_hi = sin_mid * cos_delta + cos_mid * sin_delta;
3176 let one_minus_hi = 1.0 - sn_hi * sn_hi;
3177 let expo_hi = ((sn_hi * hk) - hs) / one_minus_hi;
3178
3179 sum += weight * (expo_lo.exp() + expo_hi.exp());
3180 }
3181 sum
3182}
3183
3184pub fn bivariate_normal_cdf(h: f64, k: f64, rho: f64) -> Result<f64, String> {
3185 validate_bvn_args(h, k, rho)?;
3186 if h == f64::NEG_INFINITY || k == f64::NEG_INFINITY {
3187 return Ok(0.0);
3188 }
3189 if h == f64::INFINITY {
3190 return Ok(normal_cdf(k));
3191 }
3192 if k == f64::INFINITY {
3193 return Ok(normal_cdf(h));
3194 }
3195
3196 let rho_clamped = rho.clamp(-1.0, 1.0);
3197 if rho_clamped >= 1.0 - 1e-12 {
3198 return Ok(normal_cdf(h.min(k)));
3199 }
3200 if rho_clamped <= -1.0 + 1e-12 {
3201 return Ok((normal_cdf(h) - normal_cdf(-k)).clamp(0.0, 1.0));
3202 }
3203 if rho_clamped == 0.0 {
3204 return Ok((normal_cdf(h) * normal_cdf(k)).clamp(0.0, 1.0));
3205 }
3206 if h == 0.0 && k == 0.0 {
3207 return Ok((0.25 + rho_clamped.asin() / std::f64::consts::TAU).clamp(0.0, 1.0));
3208 }
3209
3210 let asr = rho_clamped.asin();
3211 let sum = bvn_gl_sum(h, k, rho_clamped, asr);
3212 Ok((normal_cdf(h) * normal_cdf(k) + asr * sum / (4.0 * std::f64::consts::PI)).clamp(0.0, 1.0))
3213}
3214
3215#[inline]
3216fn bvn_gl_sum_interval(h: f64, left: f64, right: f64, rho_clamped: f64, asr: f64) -> f64 {
3217 if rho_clamped == 0.0 {
3218 return 0.0;
3219 }
3220 let h2 = h * h;
3221 let right_hs = 0.5 * (h2 + right * right);
3222 let left_hs = 0.5 * (h2 + left * left);
3223 let half_asr = 0.5 * asr;
3224 let (sin_mid, cos_mid) = half_asr.sin_cos();
3225 let mut sum = 0.0;
3226 for i in 0..10 {
3227 let node = GL20_NODES[i].abs();
3228 let weight = GL20_WEIGHTS[i];
3229 let (sin_delta, cos_delta) = (half_asr * node).sin_cos();
3230
3231 let sn_lo = sin_mid * cos_delta - cos_mid * sin_delta;
3232 let one_minus_lo = 1.0 - sn_lo * sn_lo;
3233 let lo_right = (((sn_lo * h * right) - right_hs) / one_minus_lo).exp();
3234 let lo_left = (((sn_lo * h * left) - left_hs) / one_minus_lo).exp();
3235
3236 let sn_hi = sin_mid * cos_delta + cos_mid * sin_delta;
3237 let one_minus_hi = 1.0 - sn_hi * sn_hi;
3238 let hi_right = (((sn_hi * h * right) - right_hs) / one_minus_hi).exp();
3239 let hi_left = (((sn_hi * h * left) - left_hs) / one_minus_hi).exp();
3240
3241 sum += weight * ((lo_right - lo_left) + (hi_right - hi_left));
3242 }
3243 sum
3244}
3245
3246fn bivariate_normal_cdf_interval(h: f64, left: f64, right: f64, rho: f64) -> Result<f64, String> {
3247 if right <= left {
3248 return Ok(0.0);
3249 }
3250 if left == f64::NEG_INFINITY && right == f64::INFINITY {
3251 return Ok(normal_cdf(h));
3252 }
3253 if !left.is_finite() || !right.is_finite() {
3254 let upper = bivariate_normal_cdf(h, right, rho)?;
3255 let lower = bivariate_normal_cdf(h, left, rho)?;
3256 return Ok((upper - lower).clamp(0.0, 1.0));
3257 }
3258 validate_bvn_args(h, left, rho)?;
3259 validate_bvn_args(h, right, rho)?;
3260 if h == f64::NEG_INFINITY {
3261 return Ok(0.0);
3262 }
3263 if h == f64::INFINITY {
3264 return Ok((normal_cdf(right) - normal_cdf(left)).clamp(0.0, 1.0));
3265 }
3266
3267 let rho_clamped = rho.clamp(-1.0, 1.0);
3268 if rho_clamped >= 1.0 - 1e-12 || rho_clamped <= -1.0 + 1e-12 {
3269 let upper = bivariate_normal_cdf(h, right, rho_clamped)?;
3270 let lower = bivariate_normal_cdf(h, left, rho_clamped)?;
3271 return Ok((upper - lower).clamp(0.0, 1.0));
3272 }
3273
3274 let cdf_h = normal_cdf(h);
3275 let normal_part = cdf_h * (normal_cdf(right) - normal_cdf(left));
3276 if rho_clamped == 0.0 {
3277 return Ok(normal_part.clamp(0.0, 1.0));
3278 }
3279 let asr = rho_clamped.asin();
3280 let sum = bvn_gl_sum_interval(h, left, right, rho_clamped, asr);
3281 Ok((normal_part + asr * sum / (4.0 * std::f64::consts::PI)).clamp(0.0, 1.0))
3282}
3283
3284fn exp_neg_half_square(x: f64) -> f64 {
3285 if x.is_infinite() {
3286 0.0
3287 } else {
3288 (-0.5 * x * x).exp()
3289 }
3290}
3291
3292fn truncated_gaussian_zeroth_moment(a: f64, b: f64) -> f64 {
3336 let inv_sqrt2 = 1.0 / std::f64::consts::SQRT_2;
3337 let za = a * inv_sqrt2;
3338 let zb = b * inv_sqrt2;
3339 let erf_diff = if za >= 0.0 {
3340 libm::erfc(za) - libm::erfc(zb)
3341 } else if zb <= 0.0 {
3342 libm::erfc(-zb) - libm::erfc(-za)
3343 } else if zb <= 0.5 && -za <= 0.5 {
3344 libm::erf(zb) + libm::erf(-za)
3349 } else {
3350 2.0 - libm::erfc(zb) - libm::erfc(-za)
3351 };
3352 (std::f64::consts::PI / 2.0).sqrt() * erf_diff
3354}
3355
3356fn fill_truncated_gaussian_moments(a: f64, b: f64, out: &mut [f64]) {
3378 if out.is_empty() {
3379 return;
3380 }
3381 out[0] = truncated_gaussian_zeroth_moment(a, b);
3382 if out.len() == 1 {
3383 return;
3384 }
3385 let ea = exp_neg_half_square(a);
3386 let eb = exp_neg_half_square(b);
3387 out[1] = ea - eb;
3388 if out.len() == 2 {
3389 return;
3390 }
3391 let a_finite = a.is_finite();
3392 let b_finite = b.is_finite();
3393 let mut a_pow_n_minus_1 = a; let mut b_pow_n_minus_1 = b;
3401 for n in 2..out.len() {
3402 let left = if a_finite { a_pow_n_minus_1 * ea } else { 0.0 };
3403 let right = if b_finite { b_pow_n_minus_1 * eb } else { 0.0 };
3404 out[n] = left - right + (n as f64 - 1.0) * out[n - 2];
3405 a_pow_n_minus_1 *= a;
3406 b_pow_n_minus_1 *= b;
3407 }
3408}
3409
3410const MAX_AFFINE_ANCHOR_DEGREE: usize = 64;
3415
3416pub fn affine_anchor_moment_vector(
3417 alpha: f64,
3418 beta: f64,
3419 left: f64,
3420 right: f64,
3421 max_degree: usize,
3422) -> Vec<f64> {
3423 let mut out = vec![0.0; max_degree + 1];
3424 affine_anchor_moment_vector_into(alpha, beta, left, right, max_degree, &mut out);
3425 out
3426}
3427
3428fn affine_anchor_moment_vector_into(
3429 alpha: f64,
3430 beta: f64,
3431 left: f64,
3432 right: f64,
3433 max_degree: usize,
3434 out: &mut [f64],
3435) {
3436 assert_eq!(out.len(), max_degree + 1);
3437 let s = (1.0 + beta * beta).sqrt();
3438 let mu = -alpha * beta / (1.0 + beta * beta);
3439 let y_left = if left.is_infinite() {
3440 if left.is_sign_positive() {
3441 f64::INFINITY
3442 } else {
3443 f64::NEG_INFINITY
3444 }
3445 } else {
3446 s * (left - mu)
3447 };
3448 let y_right = if right.is_infinite() {
3449 if right.is_sign_positive() {
3450 f64::INFINITY
3451 } else {
3452 f64::NEG_INFINITY
3453 }
3454 } else {
3455 s * (right - mu)
3456 };
3457 let anchor = (-alpha * alpha / (2.0 * s * s)).exp() / s;
3458 assert!(
3459 max_degree <= MAX_AFFINE_ANCHOR_DEGREE,
3460 "affine_anchor_moment_vector max_degree {} exceeds compile-time bound {}",
3461 max_degree,
3462 MAX_AFFINE_ANCHOR_DEGREE
3463 );
3464 let mut t = [0.0_f64; MAX_AFFINE_ANCHOR_DEGREE + 1];
3465 fill_truncated_gaussian_moments(y_left, y_right, &mut t[..=max_degree]);
3466 let mut mu_pow = [1.0_f64; MAX_AFFINE_ANCHOR_DEGREE + 1];
3472 for k in 1..=max_degree {
3473 mu_pow[k] = mu_pow[k - 1] * mu;
3474 }
3475 let inv_s = 1.0 / s;
3476 let mut inv_s_pow = [1.0_f64; MAX_AFFINE_ANCHOR_DEGREE + 1];
3477 for k in 1..=max_degree {
3478 inv_s_pow[k] = inv_s_pow[k - 1] * inv_s;
3479 }
3480 out.fill(0.0);
3481 for n in 0..=max_degree {
3482 let mut acc = 0.0;
3483 let mut binom = 1.0;
3485 for k in 0..=n {
3486 let term = binom * mu_pow[n - k] * inv_s_pow[k];
3487 acc = term.mul_add(t[k], acc);
3488 if k < n {
3489 binom = binom * (n - k) as f64 / (k + 1) as f64;
3490 }
3491 }
3492 out[n] = anchor * acc;
3493 }
3494}
3495
3496fn affine_value_from_moment_primitive(alpha: f64, beta: f64, left: f64, right: f64) -> f64 {
3497 let s = (1.0 + beta * beta).sqrt();
3509 let h = alpha / s;
3510 let rho = -beta / s;
3511 bivariate_normal_cdf_interval(h, left, right, rho).unwrap_or(0.0)
3512}
3513
3514pub fn evaluate_affine_cell_state(
3521 cell: DenestedCubicCell,
3522 max_degree: usize,
3523) -> Result<CellMomentState, String> {
3524 let alpha = cell.c0;
3525 let beta = cell.c1;
3526 let value = affine_value_from_moment_primitive(alpha, beta, cell.left, cell.right);
3527 let moments = affine_anchor_moment_vector(alpha, beta, cell.left, cell.right, max_degree);
3528 Ok(CellMomentState {
3529 branch: ExactCellBranch::Affine,
3530 value,
3531 moments: moments.into(),
3532 })
3533}
3534
3535fn evaluate_affine_cell_derivative_state(
3536 cell: DenestedCubicCell,
3537 max_degree: usize,
3538) -> Result<CellDerivativeMomentState, String> {
3539 let alpha = cell.c0;
3540 let beta = cell.c1;
3541 let moments = affine_anchor_moment_vector(alpha, beta, cell.left, cell.right, max_degree);
3542 Ok(CellDerivativeMomentState {
3543 branch: ExactCellBranch::Affine,
3544 moments: moments.into(),
3545 })
3546}
3547
3548#[inline]
3555fn accumulate_moments_unrolled4(moments: &mut [f64], mw: f64, z: f64) {
3556 let mut z_pow = 1.0_f64;
3557 for slot in moments.iter_mut() {
3558 *slot = mw.mul_add(z_pow, *slot);
3559 z_pow *= z;
3560 }
3561}
3562
3563#[inline(always)]
3606fn evaluate_non_affine_cell_with_rule<const COMPUTE_VALUE: bool>(
3607 cell: DenestedCubicCell,
3608 max_degree: usize,
3609 gl_nodes: &[f64],
3610 gl_weights: &[f64],
3611) -> (CellMomentVec, f64) {
3612 let mut moments: CellMomentVec = smallvec![0.0_f64; max_degree + 1];
3613 let mut value_integral = 0.0_f64;
3614 let center = 0.5 * (cell.left + cell.right);
3615 let half_width = 0.5 * (cell.right - cell.left);
3616 let c0 = cell.c0;
3617 let c1 = cell.c1;
3618 let c2 = cell.c2;
3619 let c3 = cell.c3;
3620 let moments_slice: &mut [f64] = &mut moments;
3621 assert_eq!(gl_nodes.len(), gl_weights.len());
3622 use wide::f64x4;
3623 let center_v = f64x4::splat(center);
3624 let half_width_v = f64x4::splat(half_width);
3625 let c0_v = f64x4::splat(c0);
3626 let c1_v = f64x4::splat(c1);
3627 let c2_v = f64x4::splat(c2);
3628 let c3_v = f64x4::splat(c3);
3629 let neg_half_v = f64x4::splat(-0.5);
3630 let n_total = gl_nodes.len();
3631 let n_simd = n_total - (n_total % 4);
3632 let mut i = 0;
3633 while i < n_simd {
3634 let node_v = f64x4::from([
3635 gl_nodes[i],
3636 gl_nodes[i + 1],
3637 gl_nodes[i + 2],
3638 gl_nodes[i + 3],
3639 ]);
3640 let weight_v = f64x4::from([
3641 gl_weights[i],
3642 gl_weights[i + 1],
3643 gl_weights[i + 2],
3644 gl_weights[i + 3],
3645 ]);
3646 let z_v = half_width_v.mul_add(node_v, center_v);
3647 let eta_v = c3_v
3649 .mul_add(z_v, c2_v)
3650 .mul_add(z_v, c1_v)
3651 .mul_add(z_v, c0_v);
3652 let z2_v = z_v * z_v;
3653 let neg_q_v = neg_half_v * (z2_v + eta_v * eta_v);
3654 let exp_negq_v = neg_q_v.exp();
3655 let moment_weight_v = weight_v * exp_negq_v;
3656 let z_arr = z_v.to_array();
3657 let mw_arr = moment_weight_v.to_array();
3658 if COMPUTE_VALUE {
3659 for lane in 0..4 {
3660 let z = z_arr[lane];
3661 let mw = mw_arr[lane];
3662 accumulate_moments_unrolled4(moments_slice, mw, z);
3663 let node = gl_nodes[i + lane];
3676 let weight = gl_weights[i + lane];
3677 let z_ref = center + half_width * node;
3678 let eta_ref = c0 + c1 * z_ref + c2 * z_ref * z_ref + c3 * z_ref * z_ref * z_ref;
3679 value_integral += weight * (-0.5 * z_ref * z_ref).exp() * normal_cdf(eta_ref);
3680 }
3681 } else {
3682 for lane in 0..4 {
3683 let z = z_arr[lane];
3684 let mw = mw_arr[lane];
3685 accumulate_moments_unrolled4(moments_slice, mw, z);
3686 }
3687 }
3688 i += 4;
3689 }
3690 while i < n_total {
3691 let node = gl_nodes[i];
3692 let weight = gl_weights[i];
3693 let z = center + half_width * node;
3694 let eta = c3.mul_add(z, c2).mul_add(z, c1).mul_add(z, c0);
3695 let q = 0.5 * (z * z + eta * eta);
3696 let moment_weight = weight * (-q).exp();
3697 accumulate_moments_unrolled4(moments_slice, moment_weight, z);
3698 if COMPUTE_VALUE {
3699 let eta_ref = c0 + c1 * z + c2 * z * z + c3 * z * z * z;
3704 value_integral += weight * (-0.5 * z * z).exp() * normal_cdf(eta_ref);
3705 }
3706 i += 1;
3707 }
3708 for moment in moments_slice.iter_mut() {
3712 *moment *= half_width;
3713 }
3714 let value = if COMPUTE_VALUE {
3715 value_integral * half_width
3716 } else {
3717 value_integral
3718 };
3719 (moments, value)
3720}
3721
3722const NON_AFFINE_LADDER_RTOL: f64 = 1e-15;
3748
3749const NON_AFFINE_LADDER_RUNGS: [usize; 5] = [12, 24, 48, 96, 192];
3752
3753fn non_affine_ladder_rules() -> &'static [(Vec<f64>, Vec<f64>)] {
3760 static RULES: std::sync::OnceLock<Vec<(Vec<f64>, Vec<f64>)>> = std::sync::OnceLock::new();
3761 RULES.get_or_init(|| {
3762 NON_AFFINE_LADDER_RUNGS
3763 .iter()
3764 .map(|&n| gauss_legendre_rule(n))
3765 .collect()
3766 })
3767}
3768
3769use gam_math::special::gauss_legendre as gauss_legendre_rule;
3773
3774fn non_affine_ladder_converged(coarse: &CellMomentVec, fine: &CellMomentVec) -> bool {
3789 let mut scale = 0.0_f64;
3790 let mut err = 0.0_f64;
3791 for (&c, &f) in coarse.iter().zip(fine.iter()) {
3792 scale = scale.max(f.abs());
3793 err = err.max((c - f).abs());
3794 }
3795 if !(scale.is_finite() && err.is_finite()) {
3796 return false;
3797 }
3798 err <= NON_AFFINE_LADDER_RTOL * scale
3799}
3800
3801pub(crate) static NON_AFFINE_LADDER_CERT_COUNTS: [AtomicU64; NON_AFFINE_LADDER_RUNGS.len() + 1] = [
3809 AtomicU64::new(0),
3810 AtomicU64::new(0),
3811 AtomicU64::new(0),
3812 AtomicU64::new(0),
3813 AtomicU64::new(0),
3814 AtomicU64::new(0),
3815];
3816
3817pub fn non_affine_ladder_cert_histogram() -> (Vec<(usize, u64)>, u64) {
3820 let per_rung = NON_AFFINE_LADDER_RUNGS
3821 .iter()
3822 .enumerate()
3823 .map(|(i, &n)| (n, NON_AFFINE_LADDER_CERT_COUNTS[i].load(Ordering::Relaxed)))
3824 .collect();
3825 let terminal =
3826 NON_AFFINE_LADDER_CERT_COUNTS[NON_AFFINE_LADDER_RUNGS.len()].load(Ordering::Relaxed);
3827 (per_rung, terminal)
3828}
3829
3830#[inline]
3835fn evaluate_non_affine_cell_simd<const COMPUTE_VALUE: bool>(
3836 cell: DenestedCubicCell,
3837 max_degree: usize,
3838) -> (CellMomentVec, f64) {
3839 let mut prev: Option<(CellMomentVec, f64)> = None;
3840 for (i, (nodes, weights)) in non_affine_ladder_rules().iter().enumerate() {
3841 let cur =
3842 evaluate_non_affine_cell_with_rule::<COMPUTE_VALUE>(cell, max_degree, nodes, weights);
3843 if let Some(prev) = prev.as_ref()
3844 && non_affine_ladder_converged(&prev.0, &cur.0)
3845 {
3846 NON_AFFINE_LADDER_CERT_COUNTS[i].fetch_add(1, Ordering::Relaxed);
3847 return cur;
3848 }
3849 prev = Some(cur);
3850 }
3851 NON_AFFINE_LADDER_CERT_COUNTS[NON_AFFINE_LADDER_RUNGS.len()].fetch_add(1, Ordering::Relaxed);
3852 evaluate_non_affine_cell_with_rule::<COMPUTE_VALUE>(cell, max_degree, &GL_NODES, &GL_WEIGHTS)
3853}
3854
3855fn evaluate_non_affine_cell_value_terminal(cell: DenestedCubicCell) -> f64 {
3875 let center = 0.5 * (cell.left + cell.right);
3876 let half_width = 0.5 * (cell.right - cell.left);
3877 let c0 = cell.c0;
3878 let c1 = cell.c1;
3879 let c2 = cell.c2;
3880 let c3 = cell.c3;
3881 let mut value_integral = 0.0_f64;
3882 for (&node, &weight) in GL_NODES.iter().zip(GL_WEIGHTS.iter()) {
3883 let z = center + half_width * node;
3884 let eta = c0 + c1 * z + c2 * z * z + c3 * z * z * z;
3885 value_integral += weight * (-0.5 * z * z).exp() * normal_cdf(eta);
3886 }
3887 value_integral * half_width
3888}
3889
3890fn evaluate_non_affine_cell_state(
3891 cell: DenestedCubicCell,
3892 branch: ExactCellBranch,
3893 max_degree: usize,
3894) -> Result<CellMomentState, String> {
3895 let (moments, _) = evaluate_non_affine_cell_simd::<false>(cell, max_degree);
3896 let value_integral = evaluate_non_affine_cell_value_terminal(cell);
3897 Ok(CellMomentState {
3902 branch,
3903 value: value_integral / (std::f64::consts::TAU).sqrt(),
3904 moments,
3905 })
3906}
3907
3908fn evaluate_non_affine_cell_derivative_state(
3909 cell: DenestedCubicCell,
3910 branch: ExactCellBranch,
3911 max_degree: usize,
3912) -> Result<CellDerivativeMomentState, String> {
3913 let (moments, _) = evaluate_non_affine_cell_simd::<false>(cell, max_degree);
3914 Ok(CellDerivativeMomentState { branch, moments })
3915}
3916
3917pub fn evaluate_cell_moments(
3923 cell: DenestedCubicCell,
3924 max_degree: usize,
3925) -> Result<CellMomentState, String> {
3926 if !TAIL_CELL_MOMENT_CACHE_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
3927 return evaluate_cell_moments_uncached(cell, max_degree);
3928 }
3929 tail_cell_moment_cache().evaluate(cell, max_degree)
3930}
3931
3932pub fn evaluate_cell_moments_uncached(
3937 cell: DenestedCubicCell,
3938 max_degree: usize,
3939) -> Result<CellMomentState, String> {
3940 evaluate_cell_state_dispatched(
3941 cell,
3942 max_degree,
3943 evaluate_affine_cell_state,
3944 evaluate_non_affine_cell_state,
3945 )
3946}
3947
3948pub fn evaluate_cell_derivative_moments_uncached(
3955 cell: DenestedCubicCell,
3956 max_degree: usize,
3957) -> Result<CellDerivativeMomentState, String> {
3958 evaluate_cell_state_dispatched(
3959 cell,
3960 max_degree,
3961 evaluate_affine_cell_derivative_state,
3962 evaluate_non_affine_cell_derivative_state,
3963 )
3964}
3965
3966fn evaluate_cell_state_dispatched<S>(
3975 cell: DenestedCubicCell,
3976 max_degree: usize,
3977 affine: fn(DenestedCubicCell, usize) -> Result<S, String>,
3978 non_affine: fn(DenestedCubicCell, ExactCellBranch, usize) -> Result<S, String>,
3979) -> Result<S, String> {
3980 let left_inf = !cell.left.is_finite();
3981 let right_inf = !cell.right.is_finite();
3982 if left_inf || right_inf {
3983 if cell.c2.abs() > NORMALIZED_CELL_BRANCH_TOL || cell.c3.abs() > NORMALIZED_CELL_BRANCH_TOL
3987 {
3988 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3989 "semi-infinite cell [{}, {}] must be affine (c2=c3=0), got c2={:.3e}, c3={:.3e}",
3990 cell.left, cell.right, cell.c2, cell.c3
3991 ))
3992 .into());
3993 }
3994 return affine(cell, max_degree);
3995 }
3996 if cell.right <= cell.left {
3997 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3998 "finite cell must have left < right, got [{}, {}]",
3999 cell.left, cell.right
4000 ))
4001 .into());
4002 }
4003 let branch = branch_cell(cell)?;
4004 if branch == ExactCellBranch::Affine {
4005 return affine(cell, max_degree);
4006 }
4007 if branch == ExactCellBranch::Sextic {
4008 let lead = sextic_qprime_coefficients(cell.c0, cell.c1, cell.c2, cell.c3)[5];
4009 if !lead.is_finite() {
4010 return Err(CubicCellKernelError::invalid_cell_shape(format!(
4011 "sextic cell evaluation encountered non-finite leading coefficient: {lead:.3e}"
4012 ))
4013 .into());
4014 }
4015 if let Some(lower_branch) = degenerate_sextic_branch(cell, lead)? {
4016 return match lower_branch {
4017 ExactCellBranch::Quartic => non_affine(
4018 DenestedCubicCell { c3: 0.0, ..cell },
4019 ExactCellBranch::Quartic,
4020 max_degree,
4021 ),
4022 ExactCellBranch::Affine => affine(
4023 DenestedCubicCell {
4024 c2: 0.0,
4025 c3: 0.0,
4026 ..cell
4027 },
4028 max_degree,
4029 ),
4030 ExactCellBranch::Sextic => Err(CubicCellKernelError::invalid_cell_shape(
4031 "internal: degenerate_sextic_branch returned Sextic as a lowered branch",
4032 )
4033 .into()),
4034 };
4035 }
4036 }
4037 non_affine(cell, branch, max_degree)
4038}
4039
4040pub fn evaluate_cell_moments_cached(
4047 cell: DenestedCubicCell,
4048 max_degree: usize,
4049 cache: &CellMomentLruCache,
4050 stats: Option<&CellMomentCacheStats>,
4051) -> Result<CellMomentState, String> {
4052 if matches!(branch_cell(cell), Ok(ExactCellBranch::Affine)) {
4061 if let Some(stats) = stats {
4062 stats.misses.fetch_add(1, Ordering::Relaxed);
4063 }
4064 return evaluate_cell_moments_uncached(cell, max_degree);
4065 }
4066 let key = CellFingerprint::new(cell);
4067 let existing_derivative = match cache.get(&key) {
4068 Some(cached) => {
4069 if let Some(state) = cached.state_for_degree(max_degree) {
4070 if let Some(stats) = stats {
4071 stats.hits.fetch_add(1, Ordering::Relaxed);
4072 }
4073 return Ok(state);
4074 }
4075 cached.derivative_state.clone()
4079 }
4080 None => None,
4081 };
4082 if let Some(stats) = stats {
4083 stats.misses.fetch_add(1, Ordering::Relaxed);
4084 }
4085 let state = evaluate_cell_moments(cell, max_degree)?;
4086 let shared = Arc::new(state);
4091 let mut entry = CachedCellMoments::new(Arc::clone(&shared));
4092 if let Some(derivative) = existing_derivative {
4093 entry = entry.with_derivative(derivative);
4094 }
4095 cache.insert(key, entry);
4096 Ok(Arc::try_unwrap(shared).unwrap_or_else(|a| (*a).clone()))
4097}
4098
4099pub fn evaluate_cell_derivative_moments_cached(
4105 cell: DenestedCubicCell,
4106 max_degree: usize,
4107 cache: &CellMomentLruCache,
4108 stats: Option<&CellMomentCacheStats>,
4109) -> Result<CellDerivativeMomentState, String> {
4110 if matches!(branch_cell(cell), Ok(ExactCellBranch::Affine)) {
4114 if let Some(stats) = stats {
4115 stats.misses.fetch_add(1, Ordering::Relaxed);
4116 }
4117 return evaluate_cell_derivative_moments_uncached(cell, max_degree);
4118 }
4119 let key = CellFingerprint::new(cell);
4120 let existing_value = match cache.get(&key) {
4121 Some(cached) => {
4122 if let Some(state) = cached.derivative_state_for_degree(max_degree) {
4123 if let Some(stats) = stats {
4124 stats.hits.fetch_add(1, Ordering::Relaxed);
4125 }
4126 return Ok(state);
4127 }
4128 cached.state.clone()
4132 }
4133 None => None,
4134 };
4135 if let Some(stats) = stats {
4136 stats.misses.fetch_add(1, Ordering::Relaxed);
4137 }
4138 let state = evaluate_cell_derivative_moments_uncached(cell, max_degree)?;
4139 let shared = Arc::new(state);
4144 let mut entry = CachedCellMoments::new_derivative(Arc::clone(&shared));
4145 if let Some(value) = existing_value {
4146 entry = entry.with_value(value);
4147 }
4148 cache.insert(key, entry);
4149 Ok(Arc::try_unwrap(shared).unwrap_or_else(|a| (*a).clone()))
4150}
4151
4152pub fn evaluate_cell_moments_with_scratch<'a>(
4159 cell: DenestedCubicCell,
4160 max_degree: usize,
4161 scratch: &'a mut CellMomentScratch,
4162) -> Result<CellMomentStateRef<'a>, String> {
4163 let state = evaluate_cell_moments(cell, max_degree)?;
4164 let out = scratch.prepare_moments(max_degree + 1);
4165 out.copy_from_slice(&state.moments);
4166 Ok(CellMomentStateRef {
4167 branch: state.branch,
4168 value: state.value,
4169 moments: out,
4170 })
4171}
4172
4173#[cfg(test)]
4174mod tests {
4175 use super::*;
4176 use gam_math::probability::normal_pdf;
4177
4178 #[inline]
4179 pub(super) fn polynomial_value(coefficients: &[f64], z: f64) -> f64 {
4180 coefficients
4181 .iter()
4182 .rev()
4183 .fold(0.0, |acc, &coeff| acc * z + coeff)
4184 }
4185
4186 fn reset_cell_moment_test_reallocs() {
4187 super::CELL_MOMENT_REALLOCS.store(0, std::sync::atomic::Ordering::Relaxed);
4188 }
4189
4190 fn cell_moment_test_reallocs() -> usize {
4191 super::CELL_MOMENT_REALLOCS.load(std::sync::atomic::Ordering::Relaxed)
4192 }
4193
4194 fn assert_close_rel(label: &str, actual: f64, expected: f64, tol: f64) {
4195 let denom = expected.abs().max(1.0);
4196 let rel = (actual - expected).abs() / denom;
4197 assert!(
4198 rel <= tol,
4199 "{label}: actual={actual:.17e} expected={expected:.17e} rel={rel:.3e} tol={tol:.3e}"
4200 );
4201 }
4202
4203 #[test]
4218 fn link_basis_cell_fourth_ab_partials_vanish_third_are_nonzero() {
4219 let span = LocalSpanCubic {
4220 left: -0.4,
4221 right: 1.6,
4222 c0: 0.37,
4223 c1: -0.81,
4224 c2: 0.53,
4225 c3: -0.29,
4226 };
4227 let a0 = 0.23_f64;
4228 let b0 = 0.61_f64;
4229 let h = 1e-2_f64;
4230
4231 let stencil = |order: usize| -> &'static [(i64, f64)] {
4233 match order {
4234 0 => &[(0, 1.0)],
4235 1 => &[(-1, -0.5), (1, 0.5)],
4236 2 => &[(-1, 1.0), (0, -2.0), (1, 1.0)],
4237 3 => &[(-2, -0.5), (-1, 1.0), (1, -1.0), (2, 0.5)],
4238 4 => &[(-2, 1.0), (-1, -4.0), (0, 6.0), (1, -4.0), (2, 1.0)],
4239 _ => &[(0, 1.0)],
4240 }
4241 };
4242 let fd = |k: usize, na: usize, nb: usize| -> f64 {
4244 let mut acc = 0.0;
4245 for &(ia, wa) in stencil(na) {
4246 for &(ib, wb) in stencil(nb) {
4247 let a = a0 + (ia as f64) * h;
4248 let b = b0 + (ib as f64) * h;
4249 acc += wa * wb * link_basis_cell_coefficients(span, a, b)[k];
4250 }
4251 }
4252 acc / h.powi((na + nb) as i32)
4253 };
4254
4255 let (p3_aaa, p3_aab, p3_abb, p3_bbb) = link_basis_cell_third_partials(span);
4256
4257 let mut max_third = 0.0_f64;
4261 for k in 0..4 {
4262 for (label, (na, nb), analytic) in [
4263 ("aaa", (3usize, 0usize), p3_aaa[k]),
4264 ("aab", (2, 1), p3_aab[k]),
4265 ("abb", (1, 2), p3_abb[k]),
4266 ("bbb", (0, 3), p3_bbb[k]),
4267 ] {
4268 let got = fd(k, na, nb);
4269 assert!(
4270 (got - analytic).abs() <= 1e-4 + 1e-3 * analytic.abs(),
4271 "3rd partial {label}[{k}] analytic {analytic:+.6e} vs FD {got:+.6e}"
4272 );
4273 max_third = max_third.max(analytic.abs());
4274 }
4275 }
4276 assert!(
4277 max_third > 1e-1,
4278 "expected an appreciable nonzero 3rd (a,b)-partial; max |analytic| = {max_third:.3e}"
4279 );
4280
4281 for k in 0..4 {
4285 for (na, nb) in [(4usize, 0usize), (3, 1), (2, 2), (1, 3), (0, 4)] {
4286 let got = fd(k, na, nb);
4287 assert!(
4288 got.abs() <= 1e-2,
4289 "4th (a,b)-partial ∂^{na}_a∂^{nb}_b of cell coeff[{k}] must vanish, FD = {got:+.6e}"
4290 );
4291 }
4292 }
4293 }
4294
4295 #[test]
4296 fn non_affine_cell_state_grid_matches_public_cell_moments_reference() {
4297 let cells = [
4298 DenestedCubicCell {
4299 left: -1.25,
4300 right: -0.2,
4301 c0: -0.35,
4302 c1: 0.85,
4303 c2: 0.04,
4304 c3: -0.015,
4305 },
4306 DenestedCubicCell {
4307 left: -0.2,
4308 right: 0.55,
4309 c0: 0.12,
4310 c1: -0.65,
4311 c2: -0.025,
4312 c3: 0.02,
4313 },
4314 DenestedCubicCell {
4315 left: 0.55,
4316 right: 1.6,
4317 c0: 0.42,
4318 c1: 0.35,
4319 c2: 0.018,
4320 c3: 0.012,
4321 },
4322 ];
4323 for cell in cells {
4324 let branch = branch_cell(cell).expect("branch");
4325 assert_ne!(branch, ExactCellBranch::Affine);
4326 for max_degree in [0usize, 2, 4, 9, 16] {
4327 let direct = evaluate_non_affine_cell_state(cell, branch, max_degree)
4328 .expect("direct non-affine transport");
4329 let public = evaluate_cell_moments(cell, max_degree).expect("public evaluator");
4330 assert_eq!(direct.branch, public.branch);
4331 assert_eq!(direct.moments.len(), public.moments.len());
4332 let value_scale = direct.value.abs().max(public.value.abs()).max(1.0);
4333 assert!(
4334 (direct.value - public.value).abs() <= 1e-10 * value_scale,
4335 "value mismatch for {cell:?} degree {max_degree}: direct={} public={}",
4336 direct.value,
4337 public.value
4338 );
4339 for (degree, (lhs, rhs)) in
4340 direct.moments.iter().zip(public.moments.iter()).enumerate()
4341 {
4342 let scale = lhs.abs().max(rhs.abs()).max(1.0);
4343 assert!(
4344 (lhs - rhs).abs() <= 1e-10 * scale,
4345 "moment {degree} mismatch for {cell:?} degree {max_degree}: {lhs} vs {rhs}"
4346 );
4347 }
4348 }
4349 }
4350 }
4351
4352 #[test]
4353 fn affine_tail_cell_memo_matches_uncached_grid_and_records_hits() {
4354 let cache = TailCellMomentCache::new();
4360 let c0s = [-2.0, -0.25, 0.0, 1.5];
4361 let c1s = [-1.2, -0.05, 0.0, 0.8];
4362 let endpoints = [-4.0, -1.0, 0.0, 2.5, 6.0];
4363 let degrees = [0_usize, 4, 9, 16, 24];
4364
4365 for &c0 in &c0s {
4366 for &c1 in &c1s {
4367 for &endpoint in &endpoints {
4368 for &max_degree in °rees {
4369 for &(left, right) in
4370 &[(f64::NEG_INFINITY, endpoint), (endpoint, f64::INFINITY)]
4371 {
4372 let cell = DenestedCubicCell {
4373 left,
4374 right,
4375 c0,
4376 c1,
4377 c2: 0.0,
4378 c3: 0.0,
4379 };
4380 let expected = evaluate_cell_moments_uncached(cell, max_degree)
4381 .expect("uncached affine tail moments");
4382 let actual = cache
4383 .evaluate(cell, max_degree)
4384 .expect("cached affine tail moments miss");
4385 let repeat = cache
4386 .evaluate(cell, max_degree)
4387 .expect("cached affine tail moments hit");
4388 assert_eq!(actual.branch, expected.branch);
4389 assert_eq!(repeat.branch, expected.branch);
4390 assert_close_rel(
4391 "tail value miss",
4392 actual.value,
4393 expected.value,
4394 1e-14,
4395 );
4396 assert_close_rel("tail value hit", repeat.value, expected.value, 1e-14);
4397 assert_eq!(actual.moments.len(), expected.moments.len());
4398 assert_eq!(repeat.moments.len(), expected.moments.len());
4399 for (idx, ((a, r), e)) in actual
4400 .moments
4401 .iter()
4402 .zip(repeat.moments.iter())
4403 .zip(expected.moments.iter())
4404 .enumerate()
4405 {
4406 assert_close_rel(
4407 &format!("tail moment miss[{idx}]"),
4408 *a,
4409 *e,
4410 1e-14,
4411 );
4412 assert_close_rel(&format!("tail moment hit[{idx}]"), *r, *e, 1e-14);
4413 }
4414 }
4415 }
4416 }
4417 }
4418 }
4419
4420 let stats = cache.stats();
4421 assert_eq!(stats.misses, stats.entries);
4422 assert!(
4423 stats.hits >= stats.misses,
4424 "expected repeat hits: {stats:?}"
4425 );
4426 assert!(
4427 stats.hit_rate() >= 0.5,
4428 "unexpected low hit rate: {stats:?}"
4429 );
4430 }
4431
4432 fn reference_bivariate_normal_cdf_20(h: f64, k: f64, rho: f64) -> f64 {
4433 if h == f64::NEG_INFINITY || k == f64::NEG_INFINITY {
4434 return 0.0;
4435 }
4436 if h == f64::INFINITY {
4437 return normal_cdf(k);
4438 }
4439 if k == f64::INFINITY {
4440 return normal_cdf(h);
4441 }
4442 let rho_clamped = rho.clamp(-1.0, 1.0);
4443 if rho_clamped >= 1.0 - 1e-12 {
4444 return normal_cdf(h.min(k));
4445 }
4446 if rho_clamped <= -1.0 + 1e-12 {
4447 return (normal_cdf(h) - normal_cdf(-k)).clamp(0.0, 1.0);
4448 }
4449
4450 let hs = 0.5 * (h * h + k * k);
4451 let asr = rho_clamped.asin();
4452 let mut sum = 0.0;
4453 for (&node, &weight) in GL20_NODES.iter().zip(GL20_WEIGHTS.iter()) {
4454 let sn = (0.5 * asr * (node + 1.0)).sin();
4455 let one_minus = 1.0 - sn * sn;
4456 let expo = ((sn * h * k) - hs) / one_minus;
4457 sum += weight * expo.exp();
4458 }
4459 (normal_cdf(h) * normal_cdf(k) + asr * sum / (4.0 * std::f64::consts::PI)).clamp(0.0, 1.0)
4460 }
4461
4462 #[test]
4463 fn non_affine_cell_state_reference_grid_matches_public_moments() {
4464 let c0s = [-0.4, 0.0, 0.35];
4465 let c1s = [-0.8, 0.25, 1.1];
4466 let c2s = [-0.12, 0.08];
4467 let c3s = [-0.04, 0.03];
4468 let intervals = [(-1.25, -0.2), (-0.5, 0.75), (0.1, 1.4)];
4469 let degrees = [3usize, 6, 9, 12];
4470
4471 for &c0 in &c0s {
4472 for &c1 in &c1s {
4473 for &c2 in &c2s {
4474 for &c3 in &c3s {
4475 for &(left, right) in &intervals {
4476 let cell = DenestedCubicCell {
4477 left,
4478 right,
4479 c0,
4480 c1,
4481 c2,
4482 c3,
4483 };
4484 let branch = branch_cell(cell).expect("branch");
4485 assert_ne!(branch, ExactCellBranch::Affine);
4486 for °ree in °rees {
4487 let direct = evaluate_non_affine_cell_state(cell, branch, degree)
4488 .expect("direct non-affine state");
4489 let public = evaluate_cell_moments(cell, degree)
4490 .expect("public non-affine state");
4491 assert_eq!(direct.branch, public.branch);
4492 let value_scale =
4493 direct.value.abs().max(public.value.abs()).max(1.0);
4494 assert!(
4495 (direct.value - public.value).abs() / value_scale <= 1.0e-15,
4496 "value mismatch for {cell:?}, degree {degree}: direct={:.17e}, public={:.17e}",
4497 direct.value,
4498 public.value
4499 );
4500 assert_eq!(direct.moments.len(), public.moments.len());
4501 for (idx, (&a, &b)) in
4502 direct.moments.iter().zip(public.moments.iter()).enumerate()
4503 {
4504 let scale = a.abs().max(b.abs()).max(1.0);
4505 assert!(
4506 (a - b).abs() / scale <= 1.0e-15,
4507 "moment {idx} mismatch for {cell:?}, degree {degree}: direct={a:.17e}, public={b:.17e}"
4508 );
4509 }
4510 }
4511 }
4512 }
4513 }
4514 }
4515 }
4516 }
4517
4518 #[test]
4519 fn bivariate_normal_cdf_matches_reference_grid_to_1e_minus_10() {
4520 let hs = [-8.0, -5.0, -3.0, -1.5, -0.5, 0.0, 0.25, 1.0, 2.5, 5.0, 8.0];
4521 let ks = [-8.0, -4.0, -2.0, -0.75, 0.0, 0.4, 1.25, 3.0, 6.0, 8.0];
4522 let rhos = [
4523 -0.999_999_999_999,
4524 -0.999,
4525 -0.95,
4526 -0.7,
4527 -0.3,
4528 -1.0e-12,
4529 0.0,
4530 1.0e-12,
4531 0.3,
4532 0.7,
4533 0.95,
4534 0.999,
4535 0.999_999_999_999,
4536 ];
4537 for &h in &hs {
4538 for &k in &ks {
4539 for &rho in &rhos {
4540 let actual = bivariate_normal_cdf(h, k, rho).expect("bvn");
4541 let expected = reference_bivariate_normal_cdf_20(h, k, rho);
4542 let scale = expected.abs().max(1.0e-300);
4543 let rel = (actual - expected).abs() / scale;
4544 assert!(
4545 rel < 1.0e-10 || (actual - expected).abs() < 1.0e-14,
4546 "h={h} k={k} rho={rho} actual={actual:.17e} expected={expected:.17e} rel={rel:.3e}"
4547 );
4548 }
4549 }
4550 }
4551 }
4552
4553 #[test]
4554 fn bivariate_normal_cdf_matches_reference_lcg_property_samples() {
4555 let mut seed = 0x5eed_cafe_f00d_u64;
4556 let mut next_unit = || {
4557 seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
4558 ((seed >> 11) as f64) * (1.0 / ((1_u64 << 53) as f64))
4559 };
4560 for _ in 0..4096 {
4561 let h = -8.0 + 16.0 * next_unit();
4562 let k = -8.0 + 16.0 * next_unit();
4563 let rho = -0.999 + 1.998 * next_unit();
4564 let actual = bivariate_normal_cdf(h, k, rho).expect("bvn");
4565 let expected = reference_bivariate_normal_cdf_20(h, k, rho);
4566 let scale = expected.abs().max(1.0e-300);
4567 let rel = (actual - expected).abs() / scale;
4568 assert!(
4569 rel < 1.0e-10 || (actual - expected).abs() < 1.0e-14,
4570 "h={h} k={k} rho={rho} actual={actual:.17e} expected={expected:.17e} rel={rel:.3e}"
4571 );
4572 }
4573 }
4574
4575 #[test]
4576 fn affine_bvn_interval_primitive_matches_two_cdf_difference() {
4577 let hs = [-6.0, -2.0, -0.25, 0.0, 0.8, 3.0, 6.0];
4578 let bounds = [
4579 (-5.0, -2.0),
4580 (-3.0, -0.1),
4581 (-1.0, 0.0),
4582 (-0.25, 0.75),
4583 (0.2, 3.5),
4584 (2.0, 7.0),
4585 ];
4586 let rhos = [-0.98, -0.8, -0.25, 0.0, 0.25, 0.8, 0.98];
4587 for &h in &hs {
4588 for &(left, right) in &bounds {
4589 for &rho in &rhos {
4590 let actual =
4591 bivariate_normal_cdf_interval(h, left, right, rho).expect("interval");
4592 let expected = (reference_bivariate_normal_cdf_20(h, right, rho)
4593 - reference_bivariate_normal_cdf_20(h, left, rho))
4594 .clamp(0.0, 1.0);
4595 let scale = expected.abs().max(1.0e-300);
4596 let rel = (actual - expected).abs() / scale;
4597 assert!(
4598 rel < 1.0e-10 || (actual - expected).abs() < 1.0e-12,
4599 "h={h} left={left} right={right} rho={rho} actual={actual:.17e} expected={expected:.17e} rel={rel:.3e}"
4600 );
4601 }
4602 }
4603 }
4604 }
4605
4606 fn simpson_integral<F>(left: f64, right: f64, steps: usize, f: F) -> f64
4607 where
4608 F: Fn(f64) -> f64,
4609 {
4610 let n = if steps.is_multiple_of(2) {
4611 steps
4612 } else {
4613 steps + 1
4614 };
4615 let h = (right - left) / n as f64;
4616 let mut acc = f(left) + f(right);
4617 for k in 1..n {
4618 let x = left + h * k as f64;
4619 let w = if k % 2 == 0 { 2.0 } else { 4.0 };
4620 acc += w * f(x);
4621 }
4622 acc * h / 3.0
4623 }
4624
4625 #[test]
4626 fn global_transform_preserves_local_span_polynomial() {
4627 let span = LocalSpanCubic {
4628 left: -1.2,
4629 right: 0.8,
4630 c0: 0.3,
4631 c1: -0.25,
4632 c2: 0.11,
4633 c3: -0.04,
4634 };
4635 let (g0, g1, g2, g3) = global_cubic_from_local(span);
4636 for &x in &[-1.2, -0.7, -0.1, 0.4, 0.8] {
4637 let local = span.evaluate(x);
4638 let global = g0 + g1 * x + g2 * x * x + g3 * x * x * x;
4639 assert!((local - global).abs() < 1e-12);
4640 }
4641 }
4642
4643 #[test]
4644 fn bivariate_normal_cdf_independent_factorizes() {
4645 let h = -0.35;
4646 let k = 0.8;
4647 let out = bivariate_normal_cdf(h, k, 0.0).expect("bvn");
4648 let target = normal_cdf(h) * normal_cdf(k);
4649 assert!((out - target).abs() < 1e-12);
4650 }
4651
4652 #[test]
4653 fn evaluate_affine_cell_state_matches_numeric_integrals() {
4654 let cell = DenestedCubicCell {
4655 left: -0.9,
4656 right: 0.8,
4657 c0: 0.15,
4658 c1: -0.35,
4659 c2: 0.0,
4660 c3: 0.0,
4661 };
4662 let state = evaluate_affine_cell_state(cell, 6).expect("affine cell");
4663 let value_numeric = simpson_integral(cell.left, cell.right, 4000, |z| {
4664 super::normal_cdf(cell.eta(z)) * normal_pdf(z)
4665 });
4666 assert_eq!(state.branch, ExactCellBranch::Affine);
4667 assert!((state.value - value_numeric).abs() < 1e-9);
4668 for degree in 0..=6 {
4669 let target = simpson_integral(cell.left, cell.right, 4000, |z| {
4670 z.powi(degree as i32) * (-cell.q(z)).exp()
4671 });
4672 assert!((state.moments[degree] - target).abs() < 1e-9);
4673 }
4674 }
4675
4676 #[test]
4677 fn affine_cell_value_matches_zero_moment_derivative() {
4678 let cell = DenestedCubicCell {
4679 left: -1.1,
4680 right: 0.7,
4681 c0: 0.23,
4682 c1: -0.41,
4683 c2: 0.0,
4684 c3: 0.0,
4685 };
4686 let h = 1e-6;
4687 let plus = evaluate_affine_cell_state(
4688 DenestedCubicCell {
4689 c0: cell.c0 + h,
4690 ..cell
4691 },
4692 0,
4693 )
4694 .expect("affine plus");
4695 let minus = evaluate_affine_cell_state(
4696 DenestedCubicCell {
4697 c0: cell.c0 - h,
4698 ..cell
4699 },
4700 0,
4701 )
4702 .expect("affine minus");
4703 let center = evaluate_affine_cell_state(cell, 0).expect("affine center");
4704 let d_value = (plus.value - minus.value) / (2.0 * h);
4705 let target = INV_TWO_PI * center.moments[0];
4706 assert!((d_value - target).abs() < 1e-8);
4707 }
4708
4709 #[test]
4710 fn coefficient_partials_match_exact_span_derivatives() {
4711 let score_span = LocalSpanCubic {
4712 left: -0.75,
4713 right: 0.25,
4714 c0: 0.08,
4715 c1: -0.03,
4716 c2: 0.02,
4717 c3: -0.01,
4718 };
4719 let link_span = LocalSpanCubic {
4720 left: -0.6,
4721 right: 0.9,
4722 c0: -0.05,
4723 c1: 0.04,
4724 c2: -0.02,
4725 c3: 0.015,
4726 };
4727 let a = 0.3;
4728 let b = -0.7;
4729 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
4730 for &z in &[-0.75, -0.4, -0.1, 0.2] {
4731 let u = a + b * z;
4732 let eta_a = 1.0 + link_span.first_derivative(u);
4733 let eta_b = z + score_span.evaluate(z) + z * link_span.first_derivative(u);
4734 assert!((polynomial_value(&dc_da, z) - eta_a).abs() < 1e-12);
4735 assert!((polynomial_value(&dc_db, z) - eta_b).abs() < 1e-12);
4736 }
4737 }
4738
4739 #[test]
4740 fn second_coefficient_partials_match_exact_span_derivatives() {
4741 let score_span = LocalSpanCubic {
4742 left: -0.75,
4743 right: 0.25,
4744 c0: 0.08,
4745 c1: -0.03,
4746 c2: 0.02,
4747 c3: -0.01,
4748 };
4749 let link_span = LocalSpanCubic {
4750 left: -0.6,
4751 right: 0.9,
4752 c0: -0.05,
4753 c1: 0.04,
4754 c2: -0.02,
4755 c3: 0.015,
4756 };
4757 let a = 0.3;
4758 let b = -0.7;
4759 let second_partials = denested_cell_second_partials(score_span, link_span, a, b);
4760 let dc_daa = second_partials.0;
4761 let dc_dab = second_partials.1;
4762 let dc_dbb = second_partials.2;
4763 for &z in &[-0.75, -0.4, -0.1, 0.2] {
4764 let u = a + b * z;
4765 let eta_aa = link_span.second_derivative(u);
4766 let eta_ab = z * link_span.second_derivative(u);
4767 let eta_bb = z * z * link_span.second_derivative(u);
4768 assert!((polynomial_value(&dc_daa, z) - eta_aa).abs() < 1e-12);
4769 assert!((polynomial_value(&dc_dab, z) - eta_ab).abs() < 1e-12);
4770 assert!((polynomial_value(&dc_dbb, z) - eta_bb).abs() < 1e-12);
4771 }
4772 }
4773
4774 #[test]
4775 fn higher_derivative_moment_helpers_reject_empty_first_coefficients() {
4776 let cell = DenestedCubicCell {
4777 left: -1.0,
4778 right: 1.0,
4779 c0: 0.0,
4780 c1: 1.0,
4781 c2: 0.0,
4782 c3: 0.0,
4783 };
4784 let moments = [1.0; 16];
4785
4786 let third_err = cell_third_derivative_from_moments(
4787 cell,
4788 &[],
4789 &[1.0],
4790 &[1.0],
4791 &[],
4792 &[],
4793 &[],
4794 &[],
4795 &moments,
4796 )
4797 .expect_err("empty first coefficients should be rejected");
4798 assert!(third_err.contains("r first-derivative coefficients must be non-empty"));
4799
4800 let fourth_err = cell_fourth_derivative_from_moments(
4801 cell,
4802 &[1.0],
4803 &[],
4804 &[1.0],
4805 &[1.0],
4806 &[],
4807 &[],
4808 &[],
4809 &[],
4810 &[],
4811 &[],
4812 &[],
4813 &[],
4814 &[],
4815 &[],
4816 &[],
4817 &moments,
4818 )
4819 .expect_err("empty first coefficients should be rejected");
4820 assert!(fourth_err.contains("s first-derivative coefficients must be non-empty"));
4821 }
4822
4823 #[test]
4824 fn fourth_derivative_rejects_overlong_scratch_convolutions() {
4825 let cell = DenestedCubicCell {
4826 left: -1.0,
4827 right: 1.0,
4828 c0: 0.0,
4829 c1: 1.0,
4830 c2: 0.0,
4831 c3: 0.0,
4832 };
4833 let long_first = [1.0; 10];
4834 let zero = [0.0; 1];
4835 let moments = [1.0; 64];
4836
4837 let err = cell_fourth_derivative_from_moments(
4838 cell,
4839 &long_first,
4840 &long_first,
4841 &long_first,
4842 &long_first,
4843 &zero,
4844 &zero,
4845 &zero,
4846 &zero,
4847 &zero,
4848 &zero,
4849 &zero,
4850 &zero,
4851 &zero,
4852 &zero,
4853 &zero,
4854 &moments,
4855 )
4856 .expect_err("oversized convolution should be rejected before writing scratch");
4857 assert!(err.contains("fourth derivative polynomial convolution scratch too small"));
4858 }
4859
4860 #[test]
4861 fn score_and_link_basis_cell_coefficients_match_direct_construction() {
4862 let score_basis_span = LocalSpanCubic {
4863 left: -0.7,
4864 right: 0.4,
4865 c0: 0.2,
4866 c1: -0.04,
4867 c2: 0.03,
4868 c3: -0.01,
4869 };
4870 let link_basis_span = LocalSpanCubic {
4871 left: -0.5,
4872 right: 1.1,
4873 c0: -0.03,
4874 c1: 0.05,
4875 c2: -0.02,
4876 c3: 0.01,
4877 };
4878 let a = 0.25;
4879 let b = -0.8;
4880 let score_coeffs = score_basis_cell_coefficients(score_basis_span, b);
4881 let link_coeffs = link_basis_cell_coefficients(link_basis_span, a, b);
4882 for &z in &[-0.7, -0.1, 0.2, 0.4] {
4883 let score_poly = polynomial_value(&score_coeffs, z);
4884 let link_poly = polynomial_value(&link_coeffs, z);
4885 assert!((score_poly - b * score_basis_span.evaluate(z)).abs() < 1e-12);
4886 assert!((link_poly - link_basis_span.evaluate(a + b * z)).abs() < 1e-12);
4887 }
4888 }
4889
4890 #[test]
4891 fn link_basis_partials_match_exact_span_derivatives() {
4892 let link_basis_span = LocalSpanCubic {
4893 left: -0.5,
4894 right: 1.1,
4895 c0: -0.03,
4896 c1: 0.05,
4897 c2: -0.02,
4898 c3: 0.01,
4899 };
4900 let a = 0.25;
4901 let b = -0.8;
4902 let (dc_da, dc_db) = link_basis_cell_coefficient_partials(link_basis_span, a, b);
4903 let (dc_daa, dc_dab, dc_dbb) = link_basis_cell_second_partials(link_basis_span, a, b);
4904 for &z in &[-0.6, -0.2, 0.15, 0.5] {
4905 let u = a + b * z;
4906 let eta_a = link_basis_span.first_derivative(u);
4907 let eta_b = z * link_basis_span.first_derivative(u);
4908 let eta_aa = link_basis_span.second_derivative(u);
4909 let eta_ab = z * link_basis_span.second_derivative(u);
4910 let eta_bb = z * z * link_basis_span.second_derivative(u);
4911 assert!((polynomial_value(&dc_da, z) - eta_a).abs() < 1e-12);
4912 assert!((polynomial_value(&dc_db, z) - eta_b).abs() < 1e-12);
4913 assert!((polynomial_value(&dc_daa, z) - eta_aa).abs() < 1e-12);
4914 assert!((polynomial_value(&dc_dab, z) - eta_ab).abs() < 1e-12);
4915 assert!((polynomial_value(&dc_dbb, z) - eta_bb).abs() < 1e-12);
4916 }
4917 }
4918
4919 #[test]
4920 fn denested_third_partials_match_exact_span_derivatives() {
4921 let link_span = LocalSpanCubic {
4922 left: -0.6,
4923 right: 0.9,
4924 c0: -0.05,
4925 c1: 0.04,
4926 c2: -0.02,
4927 c3: 0.015,
4928 };
4929 let (dc_daaa, dc_daab, dc_dabb, dc_dbbb) = denested_cell_third_partials(link_span);
4930 let link_third = 6.0 * link_span.c3;
4931 for &z in &[-0.75, -0.4, -0.1, 0.2] {
4932 let eta_aaa = link_third;
4933 let eta_aab = z * link_third;
4934 let eta_abb = z * z * link_third;
4935 let eta_bbb = z * z * z * link_third;
4936 assert!((polynomial_value(&dc_daaa, z) - eta_aaa).abs() < 1e-12);
4937 assert!((polynomial_value(&dc_daab, z) - eta_aab).abs() < 1e-12);
4938 assert!((polynomial_value(&dc_dabb, z) - eta_abb).abs() < 1e-12);
4939 assert!((polynomial_value(&dc_dbbb, z) - eta_bbb).abs() < 1e-12);
4940 }
4941 }
4942
4943 #[test]
4944 fn link_basis_third_partials_match_exact_span_derivatives() {
4945 let link_basis_span = LocalSpanCubic {
4946 left: -0.5,
4947 right: 1.1,
4948 c0: -0.03,
4949 c1: 0.05,
4950 c2: -0.02,
4951 c3: 0.01,
4952 };
4953 let (dc_daaa, dc_daab, dc_dabb, dc_dbbb) = link_basis_cell_third_partials(link_basis_span);
4954 let link_third = 6.0 * link_basis_span.c3;
4955 for &z in &[-0.6, -0.2, 0.15, 0.5] {
4956 let eta_aaa = link_third;
4957 let eta_aab = z * link_third;
4958 let eta_abb = z * z * link_third;
4959 let eta_bbb = z * z * z * link_third;
4960 assert!((polynomial_value(&dc_daaa, z) - eta_aaa).abs() < 1e-12);
4961 assert!((polynomial_value(&dc_daab, z) - eta_aab).abs() < 1e-12);
4962 assert!((polynomial_value(&dc_dabb, z) - eta_abb).abs() < 1e-12);
4963 assert!((polynomial_value(&dc_dbbb, z) - eta_bbb).abs() < 1e-12);
4964 }
4965 }
4966
4967 #[test]
4968 fn branch_selection_uses_normalized_non_affine_coefficients() {
4969 let affine = DenestedCubicCell {
4970 left: -1.0,
4971 right: 1.0,
4972 c0: 0.1,
4973 c1: -0.4,
4974 c2: 1e-13,
4975 c3: -1e-13,
4976 };
4977 let quartic = DenestedCubicCell {
4978 c2: 2e-4,
4979 c3: 1e-13,
4980 ..affine
4981 };
4982 let sextic = DenestedCubicCell {
4983 c2: 2e-4,
4984 c3: 5e-3,
4985 ..affine
4986 };
4987 assert_eq!(branch_cell(affine).unwrap(), ExactCellBranch::Affine);
4988 assert_eq!(branch_cell(quartic).unwrap(), ExactCellBranch::Quartic);
4989 assert_eq!(branch_cell(sextic).unwrap(), ExactCellBranch::Sextic);
4990 }
4991
4992 #[test]
4993 fn affine_anchor_moments_match_whole_line_closed_forms() {
4994 let out = affine_anchor_moment_vector(0.0, 0.0, f64::NEG_INFINITY, f64::INFINITY, 4);
4995 let sqrt_2pi = (2.0 * std::f64::consts::PI).sqrt();
5003 assert!((out[0] - sqrt_2pi).abs() < 1e-12);
5004 assert!(out[1].abs() < 1e-12);
5005 assert!((out[2] - sqrt_2pi).abs() < 1e-12);
5006 }
5007
5008 #[test]
5009 fn affine_anchor_moments_match_shifted_gaussian_whole_line() {
5010 let alpha = 0.7;
5011 let beta = -0.4;
5012 let out = affine_anchor_moment_vector(alpha, beta, f64::NEG_INFINITY, f64::INFINITY, 4);
5013 let s = (1.0 + beta * beta).sqrt();
5014 let mu = -alpha * beta / (1.0 + beta * beta);
5015 let scale = (-alpha * alpha / (2.0 * s * s)).exp() / s;
5022 let sqrt_2pi = (2.0 * std::f64::consts::PI).sqrt();
5023 assert!((out[0] - scale * sqrt_2pi).abs() < 1e-12);
5024 assert!((out[1] - scale * sqrt_2pi * mu).abs() < 1e-12);
5025 assert!((out[2] - scale * sqrt_2pi * (mu * mu + 1.0 / (s * s))).abs() < 1e-10);
5026 }
5027
5028 #[test]
5029 fn quartic_recurrence_reduces_higher_moments() {
5030 let cell = DenestedCubicCell {
5031 left: -1.0,
5032 right: 0.9,
5033 c0: 0.2,
5034 c1: -0.3,
5035 c2: 0.18,
5036 c3: 0.0,
5037 };
5038 let exact = |k: usize| {
5039 simpson_integral(cell.left, cell.right, 2000, |z| {
5040 z.powi(k as i32) * (-cell.q(z)).exp()
5041 })
5042 };
5043 let reduced = reduce_quartic_moments(cell, [exact(0), exact(1), exact(2)], 6)
5044 .expect("quartic reduction");
5045 for k in 0..=6 {
5046 let target = exact(k);
5047 assert!(
5048 (reduced[k] - target).abs() < 1e-7,
5049 "quartic reduced moment M{k} mismatch: {} vs {}",
5050 reduced[k],
5051 target
5052 );
5053 }
5054 }
5055
5056 #[test]
5057 fn sextic_recurrence_reduces_higher_moments() {
5058 let cell = DenestedCubicCell {
5059 left: -0.8,
5060 right: 0.7,
5061 c0: -0.1,
5062 c1: 0.25,
5063 c2: -0.14,
5064 c3: 0.22,
5065 };
5066 let exact = |k: usize| {
5067 simpson_integral(cell.left, cell.right, 3000, |z| {
5068 z.powi(k as i32) * (-cell.q(z)).exp()
5069 })
5070 };
5071 let reduced =
5072 reduce_sextic_moments(cell, [exact(0), exact(1), exact(2), exact(3), exact(4)], 9)
5073 .expect("sextic reduction");
5074 for k in 0..=9 {
5075 let target = exact(k);
5076 assert!(
5077 (reduced[k] - target).abs() < 1e-7,
5078 "sextic reduced moment M{k} mismatch: {} vs {}",
5079 reduced[k],
5080 target
5081 );
5082 }
5083 }
5084
5085 #[test]
5086 fn degenerate_sextic_branch_preserves_quadratic_coefficient() {
5087 let cell = DenestedCubicCell {
5088 left: -1.0,
5089 right: 1.0,
5090 c0: 0.0,
5091 c1: 0.0,
5092 c2: 0.1,
5093 c3: 2.0e-10,
5094 };
5095 assert_eq!(branch_cell(cell).unwrap(), ExactCellBranch::Sextic);
5096
5097 let state = evaluate_cell_moments(cell, 9).expect("degenerate sextic cell");
5098 let quartic_cell = DenestedCubicCell { c3: 0.0, ..cell };
5099 let quartic = evaluate_cell_moments(quartic_cell, 9).expect("quartic cell");
5100 let affine = evaluate_affine_cell_state(
5101 DenestedCubicCell {
5102 c2: 0.0,
5103 c3: 0.0,
5104 ..cell
5105 },
5106 9,
5107 )
5108 .expect("affine cell");
5109
5110 assert_eq!(state.branch, ExactCellBranch::Quartic);
5111 for k in 0..=9 {
5112 assert!(
5113 (state.moments[k] - quartic.moments[k]).abs() < 1e-12,
5114 "lowered moment M{k} should match the quartic cell: {} vs {}",
5115 state.moments[k],
5116 quartic.moments[k]
5117 );
5118 }
5119 assert!(
5120 (state.moments[0] - affine.moments[0]).abs() > 1e-4,
5121 "degenerate sextic handling must not drop the nonzero c2 term"
5122 );
5123 }
5124
5125 #[test]
5126 fn moment_reduced_first_and_second_derivatives_match_numeric_integrals() {
5127 let cell = DenestedCubicCell {
5128 left: -0.9,
5129 right: 0.6,
5130 c0: 0.15,
5131 c1: -0.2,
5132 c2: 0.08,
5133 c3: 0.17,
5134 };
5135 let moments = reduce_sextic_moments(
5136 cell,
5137 [
5138 simpson_integral(cell.left, cell.right, 3000, |z| (-cell.q(z)).exp()),
5139 simpson_integral(cell.left, cell.right, 3000, |z| z * (-cell.q(z)).exp()),
5140 simpson_integral(cell.left, cell.right, 3000, |z| z * z * (-cell.q(z)).exp()),
5141 simpson_integral(cell.left, cell.right, 3000, |z| {
5142 z.powi(3) * (-cell.q(z)).exp()
5143 }),
5144 simpson_integral(cell.left, cell.right, 3000, |z| {
5145 z.powi(4) * (-cell.q(z)).exp()
5146 }),
5147 ],
5148 9,
5149 )
5150 .expect("reduced moments");
5151
5152 let r = [0.7, -0.1, 0.3];
5153 let s = [0.2, 0.5];
5154 let second = [0.4, -0.2, 0.1];
5155 let exact_first = cell_first_derivative_from_moments(&r, &moments).expect("first");
5156 let exact_second =
5157 cell_second_derivative_from_moments(cell, &r, &s, &second, &moments).expect("second");
5158
5159 let numeric_first = simpson_integral(cell.left, cell.right, 3000, |z| {
5160 polynomial_value(&r, z) * (-cell.q(z)).exp() / (2.0 * std::f64::consts::PI)
5161 });
5162 let numeric_second = simpson_integral(cell.left, cell.right, 3000, |z| {
5163 let eta = cell.eta(z);
5164 (polynomial_value(&second, z) - eta * polynomial_value(&r, z) * polynomial_value(&s, z))
5165 * (-cell.q(z)).exp()
5166 / (2.0 * std::f64::consts::PI)
5167 });
5168
5169 assert!((exact_first - numeric_first).abs() < 1e-7);
5170 assert!((exact_second - numeric_second).abs() < 1e-7);
5171 }
5172
5173 #[test]
5174 fn moment_reduced_third_derivative_matches_numeric_integral() {
5175 let cell = DenestedCubicCell {
5176 left: -0.85,
5177 right: 0.7,
5178 c0: -0.12,
5179 c1: 0.18,
5180 c2: 0.09,
5181 c3: -0.11,
5182 };
5183 let moments = evaluate_cell_moments(cell, 12).expect("cell moments");
5184 let r = [0.35, -0.12, 0.08];
5185 let s = [0.17, 0.09];
5186 let t = [-0.21, 0.14, -0.04];
5187 let rs = [0.11, -0.07, 0.05];
5188 let rt = [-0.06, 0.03];
5189 let st = [0.08, -0.02, 0.01];
5190 let rst = [0.04, -0.05, 0.02];
5191
5192 let exact_third = cell_third_derivative_from_moments(
5193 cell,
5194 &r,
5195 &s,
5196 &t,
5197 &rs,
5198 &rt,
5199 &st,
5200 &rst,
5201 &moments.moments,
5202 )
5203 .expect("third derivative");
5204 let numeric_third = simpson_integral(cell.left, cell.right, 4000, |z| {
5205 let eta = cell.eta(z);
5206 let rz = polynomial_value(&r, z);
5207 let sz = polynomial_value(&s, z);
5208 let tz = polynomial_value(&t, z);
5209 let rsz = polynomial_value(&rs, z);
5210 let rtz = polynomial_value(&rt, z);
5211 let stz = polynomial_value(&st, z);
5212 let rstz = polynomial_value(&rst, z);
5213 (rstz - eta * (rsz * tz + rtz * sz + stz * rz) + (eta * eta - 1.0) * rz * sz * tz)
5214 * (-cell.q(z)).exp()
5215 / (2.0 * std::f64::consts::PI)
5216 });
5217
5218 assert!((exact_third - numeric_third).abs() < 1e-7);
5219 }
5220
5221 #[test]
5222 fn moment_reduced_fourth_derivative_matches_numeric_integral() {
5223 let cell = DenestedCubicCell {
5224 left: -0.8,
5225 right: 0.65,
5226 c0: 0.11,
5227 c1: -0.22,
5228 c2: 0.07,
5229 c3: 0.13,
5230 };
5231 let moments = evaluate_cell_moments(cell, 16).expect("cell moments");
5232 let r = [0.21, -0.13, 0.06];
5233 let s = [-0.18, 0.04];
5234 let t = [0.09, 0.07, -0.03];
5235 let u = [-0.14, 0.05];
5236 let rs = [0.08, -0.03, 0.02];
5237 let rt = [-0.05, 0.01];
5238 let ru = [0.04, -0.02, 0.01];
5239 let st = [0.03, 0.02];
5240 let su = [-0.02, 0.05, -0.01];
5241 let tu = [0.07, -0.04];
5242 let rst = [0.03, -0.01, 0.02];
5243 let rsu = [-0.02, 0.04];
5244 let rtu = [0.01, 0.02, -0.01];
5245 let stu = [-0.03, 0.02];
5246 let rstu = [0.02, -0.01, 0.01];
5247
5248 let exact_fourth = cell_fourth_derivative_from_moments(
5249 cell,
5250 &r,
5251 &s,
5252 &t,
5253 &u,
5254 &rs,
5255 &rt,
5256 &ru,
5257 &st,
5258 &su,
5259 &tu,
5260 &rst,
5261 &rsu,
5262 &rtu,
5263 &stu,
5264 &rstu,
5265 &moments.moments,
5266 )
5267 .expect("fourth derivative");
5268 let numeric_fourth = simpson_integral(cell.left, cell.right, 5000, |z| {
5269 let eta = cell.eta(z);
5270 let rz = polynomial_value(&r, z);
5271 let sz = polynomial_value(&s, z);
5272 let tz = polynomial_value(&t, z);
5273 let uz = polynomial_value(&u, z);
5274 let rsz = polynomial_value(&rs, z);
5275 let rtz = polynomial_value(&rt, z);
5276 let ruz = polynomial_value(&ru, z);
5277 let stz = polynomial_value(&st, z);
5278 let suz = polynomial_value(&su, z);
5279 let tuz = polynomial_value(&tu, z);
5280 let rstz = polynomial_value(&rst, z);
5281 let rsuz = polynomial_value(&rsu, z);
5282 let rtuz = polynomial_value(&rtu, z);
5283 let stuz = polynomial_value(&stu, z);
5284 let rstuz = polynomial_value(&rstu, z);
5285 let linear =
5286 rstz * uz + rsuz * tz + rtuz * sz + stuz * rz + rsz * tuz + rtz * suz + ruz * stz;
5287 let quadratic = rsz * tz * uz
5288 + rtz * sz * uz
5289 + ruz * sz * tz
5290 + stz * rz * uz
5291 + suz * rz * tz
5292 + tuz * rz * sz;
5293 let quartic = rz * sz * tz * uz;
5294 (rstuz - eta * linear
5295 + (eta * eta - 1.0) * quadratic
5296 + (-eta * eta * eta + 3.0 * eta) * quartic)
5297 * (-cell.q(z)).exp()
5298 / (2.0 * std::f64::consts::PI)
5299 });
5300
5301 assert!((exact_fourth - numeric_fourth).abs() < 2e-7);
5302 }
5303
5304 #[test]
5305 fn denested_cell_parameter_derivatives_match_exact_integrands() {
5306 let score_span = LocalSpanCubic {
5307 left: -0.75,
5308 right: 0.25,
5309 c0: 0.08,
5310 c1: -0.03,
5311 c2: 0.02,
5312 c3: -0.01,
5313 };
5314 let link_span = LocalSpanCubic {
5315 left: -0.6,
5316 right: 0.9,
5317 c0: -0.05,
5318 c1: 0.04,
5319 c2: -0.02,
5320 c3: 0.015,
5321 };
5322 let a = 0.3;
5323 let b = -0.7;
5324 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
5325 let cell = DenestedCubicCell {
5326 left: score_span.left,
5327 right: score_span.right,
5328 c0: coeffs[0],
5329 c1: coeffs[1],
5330 c2: coeffs[2],
5331 c3: coeffs[3],
5332 };
5333 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
5334 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
5335 let (dc_daa, dc_dab, dc_dbb) = denested_cell_second_partials(score_span, link_span, a, b);
5336 let (dc_daaa, dc_daab, dc_dabb, dc_dbbb) = denested_cell_third_partials(link_span);
5337 let zero = [0.0; 4];
5338 let link_third = 6.0 * link_span.c3;
5339
5340 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
5341 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
5342 let eta_aa = |z: f64| link_span.second_derivative(a + b * z);
5343 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
5344 let eta_bb = |z: f64| z * z * link_span.second_derivative(a + b * z);
5345 let eta_aaa = |z: f64| link_third + 0.0 * z;
5346 let eta_aab = |z: f64| z * link_third;
5347 let eta_abb = |z: f64| z * z * link_third;
5348 let eta_bbb = |z: f64| z * z * z * link_third;
5349
5350 let exact_a = cell_first_derivative_from_moments(&dc_da, &state.moments).expect("a");
5351 let exact_b = cell_first_derivative_from_moments(&dc_db, &state.moments).expect("b");
5352 let exact_aa =
5353 cell_second_derivative_from_moments(cell, &dc_da, &dc_da, &dc_daa, &state.moments)
5354 .expect("aa");
5355 let exact_ab =
5356 cell_second_derivative_from_moments(cell, &dc_da, &dc_db, &dc_dab, &state.moments)
5357 .expect("ab");
5358 let exact_bb =
5359 cell_second_derivative_from_moments(cell, &dc_db, &dc_db, &dc_dbb, &state.moments)
5360 .expect("bb");
5361 let exact_aaa = cell_third_derivative_from_moments(
5362 cell,
5363 &dc_da,
5364 &dc_da,
5365 &dc_da,
5366 &dc_daa,
5367 &dc_daa,
5368 &dc_daa,
5369 &dc_daaa,
5370 &state.moments,
5371 )
5372 .expect("aaa");
5373 let exact_aab = cell_third_derivative_from_moments(
5374 cell,
5375 &dc_da,
5376 &dc_da,
5377 &dc_db,
5378 &dc_daa,
5379 &dc_dab,
5380 &dc_dab,
5381 &dc_daab,
5382 &state.moments,
5383 )
5384 .expect("aab");
5385 let exact_abb = cell_third_derivative_from_moments(
5386 cell,
5387 &dc_da,
5388 &dc_db,
5389 &dc_db,
5390 &dc_dab,
5391 &dc_dab,
5392 &dc_dbb,
5393 &dc_dabb,
5394 &state.moments,
5395 )
5396 .expect("abb");
5397 let exact_bbb = cell_third_derivative_from_moments(
5398 cell,
5399 &dc_db,
5400 &dc_db,
5401 &dc_db,
5402 &dc_dbb,
5403 &dc_dbb,
5404 &dc_dbb,
5405 &dc_dbbb,
5406 &state.moments,
5407 )
5408 .expect("bbb");
5409 let exact_aaaa = cell_fourth_derivative_from_moments(
5410 cell,
5411 &dc_da,
5412 &dc_da,
5413 &dc_da,
5414 &dc_da,
5415 &dc_daa,
5416 &dc_daa,
5417 &dc_daa,
5418 &dc_daa,
5419 &dc_daa,
5420 &dc_daa,
5421 &dc_daaa,
5422 &dc_daaa,
5423 &dc_daaa,
5424 &dc_daaa,
5425 &zero,
5426 &state.moments,
5427 )
5428 .expect("aaaa");
5429 let exact_aaab = cell_fourth_derivative_from_moments(
5430 cell,
5431 &dc_da,
5432 &dc_da,
5433 &dc_da,
5434 &dc_db,
5435 &dc_daa,
5436 &dc_daa,
5437 &dc_dab,
5438 &dc_daa,
5439 &dc_dab,
5440 &dc_dab,
5441 &dc_daaa,
5442 &dc_daab,
5443 &dc_daab,
5444 &dc_daab,
5445 &zero,
5446 &state.moments,
5447 )
5448 .expect("aaab");
5449 let exact_aabb = cell_fourth_derivative_from_moments(
5450 cell,
5451 &dc_da,
5452 &dc_da,
5453 &dc_db,
5454 &dc_db,
5455 &dc_daa,
5456 &dc_dab,
5457 &dc_dab,
5458 &dc_dab,
5459 &dc_dab,
5460 &dc_dbb,
5461 &dc_daab,
5462 &dc_daab,
5463 &dc_dabb,
5464 &dc_dabb,
5465 &zero,
5466 &state.moments,
5467 )
5468 .expect("aabb");
5469 let exact_abbb = cell_fourth_derivative_from_moments(
5470 cell,
5471 &dc_da,
5472 &dc_db,
5473 &dc_db,
5474 &dc_db,
5475 &dc_dab,
5476 &dc_dab,
5477 &dc_dab,
5478 &dc_dbb,
5479 &dc_dbb,
5480 &dc_dbb,
5481 &dc_dabb,
5482 &dc_dabb,
5483 &dc_dabb,
5484 &dc_dbbb,
5485 &zero,
5486 &state.moments,
5487 )
5488 .expect("abbb");
5489 let exact_bbbb = cell_fourth_derivative_from_moments(
5490 cell,
5491 &dc_db,
5492 &dc_db,
5493 &dc_db,
5494 &dc_db,
5495 &dc_dbb,
5496 &dc_dbb,
5497 &dc_dbb,
5498 &dc_dbb,
5499 &dc_dbb,
5500 &dc_dbb,
5501 &dc_dbbb,
5502 &dc_dbbb,
5503 &dc_dbbb,
5504 &dc_dbbb,
5505 &zero,
5506 &state.moments,
5507 )
5508 .expect("bbbb");
5509
5510 let numeric_a = simpson_integral(cell.left, cell.right, 5000, |z| {
5511 eta_a(z) * (-cell.q(z)).exp() * INV_TWO_PI
5512 });
5513 let numeric_b = simpson_integral(cell.left, cell.right, 5000, |z| {
5514 eta_b(z) * (-cell.q(z)).exp() * INV_TWO_PI
5515 });
5516 let numeric_aa = simpson_integral(cell.left, cell.right, 5000, |z| {
5517 (eta_aa(z) - cell.eta(z) * eta_a(z) * eta_a(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5518 });
5519 let numeric_ab = simpson_integral(cell.left, cell.right, 5000, |z| {
5520 (eta_ab(z) - cell.eta(z) * eta_a(z) * eta_b(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5521 });
5522 let numeric_bb = simpson_integral(cell.left, cell.right, 5000, |z| {
5523 (eta_bb(z) - cell.eta(z) * eta_b(z) * eta_b(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5524 });
5525 let numeric_aaa = simpson_integral(cell.left, cell.right, 5000, |z| {
5526 let eta = cell.eta(z);
5527 (eta_aaa(z) - 3.0 * eta * eta_aa(z) * eta_a(z) + (eta * eta - 1.0) * eta_a(z).powi(3))
5528 * (-cell.q(z)).exp()
5529 * INV_TWO_PI
5530 });
5531 let numeric_aab = simpson_integral(cell.left, cell.right, 5000, |z| {
5532 let eta = cell.eta(z);
5533 let a_z = eta_a(z);
5534 let b_z = eta_b(z);
5535 (eta_aab(z) - eta * (eta_aa(z) * b_z + 2.0 * eta_ab(z) * a_z)
5536 + (eta * eta - 1.0) * a_z * a_z * b_z)
5537 * (-cell.q(z)).exp()
5538 * INV_TWO_PI
5539 });
5540 let numeric_abb = simpson_integral(cell.left, cell.right, 5000, |z| {
5541 let eta = cell.eta(z);
5542 let a_z = eta_a(z);
5543 let b_z = eta_b(z);
5544 (eta_abb(z) - eta * (2.0 * eta_ab(z) * b_z + eta_bb(z) * a_z)
5545 + (eta * eta - 1.0) * a_z * b_z * b_z)
5546 * (-cell.q(z)).exp()
5547 * INV_TWO_PI
5548 });
5549 let numeric_bbb = simpson_integral(cell.left, cell.right, 5000, |z| {
5550 let eta = cell.eta(z);
5551 (eta_bbb(z) - 3.0 * eta * eta_bb(z) * eta_b(z) + (eta * eta - 1.0) * eta_b(z).powi(3))
5552 * (-cell.q(z)).exp()
5553 * INV_TWO_PI
5554 });
5555 let numeric_aaaa = simpson_integral(cell.left, cell.right, 5000, |z| {
5556 let eta = cell.eta(z);
5557 let eta_a_z = eta_a(z);
5558 let eta_aa_z = eta_aa(z);
5559 let eta_aaa_z = eta_aaa(z);
5560 (-eta * (4.0 * eta_aaa_z * eta_a_z + 3.0 * eta_aa_z * eta_aa_z)
5561 + (eta * eta - 1.0) * (6.0 * eta_aa_z * eta_a_z * eta_a_z)
5562 + (-eta * eta * eta + 3.0 * eta) * eta_a_z.powi(4))
5563 * (-cell.q(z)).exp()
5564 * INV_TWO_PI
5565 });
5566 let numeric_aaab = simpson_integral(cell.left, cell.right, 5000, |z| {
5567 let eta = cell.eta(z);
5568 let a_z = eta_a(z);
5569 let b_z = eta_b(z);
5570 let aa_z = eta_aa(z);
5571 let ab_z = eta_ab(z);
5572 let aaa_z = eta_aaa(z);
5573 let aab_z = eta_aab(z);
5574 (-eta * (aaa_z * b_z + 3.0 * aab_z * a_z + 3.0 * aa_z * ab_z)
5575 + (eta * eta - 1.0) * (3.0 * aa_z * a_z * b_z + 3.0 * ab_z * a_z * a_z)
5576 + (-eta * eta * eta + 3.0 * eta) * a_z.powi(3) * b_z)
5577 * (-cell.q(z)).exp()
5578 * INV_TWO_PI
5579 });
5580 let numeric_aabb = simpson_integral(cell.left, cell.right, 5000, |z| {
5581 let eta = cell.eta(z);
5582 let a_z = eta_a(z);
5583 let b_z = eta_b(z);
5584 let aa_z = eta_aa(z);
5585 let ab_z = eta_ab(z);
5586 let bb_z = eta_bb(z);
5587 let aab_z = eta_aab(z);
5588 let abb_z = eta_abb(z);
5589 (-eta * (2.0 * aab_z * b_z + 2.0 * abb_z * a_z + aa_z * bb_z + 2.0 * ab_z * ab_z)
5590 + (eta * eta - 1.0)
5591 * (aa_z * b_z * b_z + 4.0 * ab_z * a_z * b_z + bb_z * a_z * a_z)
5592 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * b_z * b_z)
5593 * (-cell.q(z)).exp()
5594 * INV_TWO_PI
5595 });
5596 let numeric_abbb = simpson_integral(cell.left, cell.right, 5000, |z| {
5597 let eta = cell.eta(z);
5598 let a_z = eta_a(z);
5599 let b_z = eta_b(z);
5600 let ab_z = eta_ab(z);
5601 let bb_z = eta_bb(z);
5602 let abb_z = eta_abb(z);
5603 let bbb_z = eta_bbb(z);
5604 (-eta * (3.0 * abb_z * b_z + bbb_z * a_z + 3.0 * ab_z * bb_z)
5605 + (eta * eta - 1.0) * (3.0 * ab_z * b_z * b_z + 3.0 * bb_z * a_z * b_z)
5606 + (-eta * eta * eta + 3.0 * eta) * a_z * b_z.powi(3))
5607 * (-cell.q(z)).exp()
5608 * INV_TWO_PI
5609 });
5610 let numeric_bbbb = simpson_integral(cell.left, cell.right, 5000, |z| {
5611 let eta = cell.eta(z);
5612 let eta_b_z = eta_b(z);
5613 let eta_bb_z = eta_bb(z);
5614 let eta_bbb_z = eta_bbb(z);
5615 (-eta * (4.0 * eta_bbb_z * eta_b_z + 3.0 * eta_bb_z * eta_bb_z)
5616 + (eta * eta - 1.0) * (6.0 * eta_bb_z * eta_b_z * eta_b_z)
5617 + (-eta * eta * eta + 3.0 * eta) * eta_b_z.powi(4))
5618 * (-cell.q(z)).exp()
5619 * INV_TWO_PI
5620 });
5621
5622 assert!((exact_a - numeric_a).abs() < 1e-8);
5623 assert!((exact_b - numeric_b).abs() < 1e-8);
5624 assert!((exact_aa - numeric_aa).abs() < 1e-8);
5625 assert!((exact_ab - numeric_ab).abs() < 1e-8);
5626 assert!((exact_bb - numeric_bb).abs() < 1e-8);
5627 assert!((exact_aaa - numeric_aaa).abs() < 2e-7);
5628 assert!((exact_aab - numeric_aab).abs() < 2e-7);
5629 assert!((exact_abb - numeric_abb).abs() < 2e-7);
5630 assert!((exact_bbb - numeric_bbb).abs() < 2e-7);
5631 assert!((exact_aaaa - numeric_aaaa).abs() < 2e-6);
5632 assert!((exact_aaab - numeric_aaab).abs() < 2e-6);
5633 assert!((exact_aabb - numeric_aabb).abs() < 2e-6);
5634 assert!((exact_abbb - numeric_abbb).abs() < 2e-6);
5635 assert!((exact_bbbb - numeric_bbbb).abs() < 2e-6);
5636 }
5637
5638 #[test]
5639 fn link_basis_cell_derivatives_match_exact_integrands() {
5640 let score_span = LocalSpanCubic {
5641 left: -0.75,
5642 right: 0.25,
5643 c0: 0.08,
5644 c1: -0.03,
5645 c2: 0.02,
5646 c3: -0.01,
5647 };
5648 let link_span = LocalSpanCubic {
5649 left: -0.6,
5650 right: 0.9,
5651 c0: -0.05,
5652 c1: 0.04,
5653 c2: -0.02,
5654 c3: 0.015,
5655 };
5656 let link_basis_span = LocalSpanCubic {
5657 left: -0.6,
5658 right: 0.9,
5659 c0: 0.02,
5660 c1: -0.01,
5661 c2: 0.03,
5662 c3: -0.02,
5663 };
5664 let a = 0.3;
5665 let b = -0.7;
5666 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
5667 let cell = DenestedCubicCell {
5668 left: score_span.left,
5669 right: score_span.right,
5670 c0: coeffs[0],
5671 c1: coeffs[1],
5672 c2: coeffs[2],
5673 c3: coeffs[3],
5674 };
5675 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
5676 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
5677 let second_partials = denested_cell_second_partials(score_span, link_span, a, b);
5678 let dc_daa = second_partials.0;
5679 let dc_dab = second_partials.1;
5680 let dc_dbb = second_partials.2;
5681 let denested_third = denested_cell_third_partials(link_span);
5682 let dc_daaa = denested_third.0;
5683 let dc_dbbb = denested_third.3;
5684
5685 let coeff_w = link_basis_cell_coefficients(link_basis_span, a, b);
5686 let (coeff_aw, coeff_bw) = link_basis_cell_coefficient_partials(link_basis_span, a, b);
5687 let (coeff_aaw, coeff_abw, coeff_bbw) =
5688 link_basis_cell_second_partials(link_basis_span, a, b);
5689 let link_basis_third = link_basis_cell_third_partials(link_basis_span);
5690 let coeff_aaaw = link_basis_third.0;
5691 let coeff_bbbw = link_basis_third.3;
5692 let zero = [0.0; 4];
5693 let basis_third = 6.0 * link_basis_span.c3;
5694
5695 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
5696 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
5697 let eta_aa = |z: f64| link_span.second_derivative(a + b * z);
5698 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
5699 let eta_bb = |z: f64| z * z * link_span.second_derivative(a + b * z);
5700 let eta_w = |z: f64| link_basis_span.evaluate(a + b * z);
5701 let eta_aw = |z: f64| link_basis_span.first_derivative(a + b * z);
5702 let eta_bw = |z: f64| z * link_basis_span.first_derivative(a + b * z);
5703 let eta_aaw = |z: f64| link_basis_span.second_derivative(a + b * z);
5704 let eta_abw = |z: f64| z * link_basis_span.second_derivative(a + b * z);
5705 let eta_bbw = |z: f64| z * z * link_basis_span.second_derivative(a + b * z);
5706 let eta_aaaw = |z: f64| basis_third + 0.0 * z;
5707 let eta_bbbw = |z: f64| z * z * z * basis_third;
5708
5709 let exact_w = cell_first_derivative_from_moments(&coeff_w, &state.moments).expect("w");
5710 let exact_aw =
5711 cell_second_derivative_from_moments(cell, &dc_da, &coeff_w, &coeff_aw, &state.moments)
5712 .expect("aw");
5713 let exact_bw =
5714 cell_second_derivative_from_moments(cell, &dc_db, &coeff_w, &coeff_bw, &state.moments)
5715 .expect("bw");
5716 let exact_ww =
5717 cell_second_derivative_from_moments(cell, &coeff_w, &coeff_w, &zero, &state.moments)
5718 .expect("ww");
5719 let exact_aaw = cell_third_derivative_from_moments(
5720 cell,
5721 &dc_da,
5722 &dc_da,
5723 &coeff_w,
5724 &dc_daa,
5725 &coeff_aw,
5726 &coeff_aw,
5727 &coeff_aaw,
5728 &state.moments,
5729 )
5730 .expect("aaw");
5731 let exact_abw = cell_third_derivative_from_moments(
5732 cell,
5733 &dc_da,
5734 &dc_db,
5735 &coeff_w,
5736 &dc_dab,
5737 &coeff_aw,
5738 &coeff_bw,
5739 &coeff_abw,
5740 &state.moments,
5741 )
5742 .expect("abw");
5743 let exact_bbw = cell_third_derivative_from_moments(
5744 cell,
5745 &dc_db,
5746 &dc_db,
5747 &coeff_w,
5748 &dc_dbb,
5749 &coeff_bw,
5750 &coeff_bw,
5751 &coeff_bbw,
5752 &state.moments,
5753 )
5754 .expect("bbw");
5755 let exact_www = cell_third_derivative_from_moments(
5756 cell,
5757 &coeff_w,
5758 &coeff_w,
5759 &coeff_w,
5760 &zero,
5761 &zero,
5762 &zero,
5763 &zero,
5764 &state.moments,
5765 )
5766 .expect("www");
5767 let exact_aaaw = cell_fourth_derivative_from_moments(
5768 cell,
5769 &dc_da,
5770 &dc_da,
5771 &dc_da,
5772 &coeff_w,
5773 &dc_daa,
5774 &dc_daa,
5775 &coeff_aw,
5776 &dc_daa,
5777 &coeff_aw,
5778 &coeff_aw,
5779 &dc_daaa,
5780 &coeff_aaw,
5781 &coeff_aaw,
5782 &coeff_aaw,
5783 &coeff_aaaw,
5784 &state.moments,
5785 )
5786 .expect("aaaw");
5787 let exact_aaww = cell_fourth_derivative_from_moments(
5788 cell,
5789 &dc_da,
5790 &dc_da,
5791 &coeff_w,
5792 &coeff_w,
5793 &dc_daa,
5794 &coeff_aw,
5795 &coeff_aw,
5796 &coeff_aw,
5797 &coeff_aw,
5798 &zero,
5799 &coeff_aaw,
5800 &coeff_aaw,
5801 &zero,
5802 &zero,
5803 &zero,
5804 &state.moments,
5805 )
5806 .expect("aaww");
5807 let exact_abww = cell_fourth_derivative_from_moments(
5808 cell,
5809 &dc_da,
5810 &dc_db,
5811 &coeff_w,
5812 &coeff_w,
5813 &dc_dab,
5814 &coeff_aw,
5815 &coeff_aw,
5816 &coeff_bw,
5817 &coeff_bw,
5818 &zero,
5819 &coeff_abw,
5820 &coeff_abw,
5821 &zero,
5822 &zero,
5823 &zero,
5824 &state.moments,
5825 )
5826 .expect("abww");
5827 let exact_bbww = cell_fourth_derivative_from_moments(
5828 cell,
5829 &dc_db,
5830 &dc_db,
5831 &coeff_w,
5832 &coeff_w,
5833 &dc_dbb,
5834 &coeff_bw,
5835 &coeff_bw,
5836 &coeff_bw,
5837 &coeff_bw,
5838 &zero,
5839 &coeff_bbw,
5840 &coeff_bbw,
5841 &zero,
5842 &zero,
5843 &zero,
5844 &state.moments,
5845 )
5846 .expect("bbww");
5847 let exact_bbbw = cell_fourth_derivative_from_moments(
5848 cell,
5849 &dc_db,
5850 &dc_db,
5851 &dc_db,
5852 &coeff_w,
5853 &dc_dbb,
5854 &dc_dbb,
5855 &coeff_bw,
5856 &dc_dbb,
5857 &coeff_bw,
5858 &coeff_bw,
5859 &dc_dbbb,
5860 &coeff_bbw,
5861 &coeff_bbw,
5862 &coeff_bbw,
5863 &coeff_bbbw,
5864 &state.moments,
5865 )
5866 .expect("bbbw");
5867 let exact_wwww = cell_fourth_derivative_from_moments(
5868 cell,
5869 &coeff_w,
5870 &coeff_w,
5871 &coeff_w,
5872 &coeff_w,
5873 &zero,
5874 &zero,
5875 &zero,
5876 &zero,
5877 &zero,
5878 &zero,
5879 &zero,
5880 &zero,
5881 &zero,
5882 &zero,
5883 &zero,
5884 &state.moments,
5885 )
5886 .expect("wwww");
5887
5888 let numeric_w = simpson_integral(cell.left, cell.right, 5000, |z| {
5889 eta_w(z) * (-cell.q(z)).exp() * INV_TWO_PI
5890 });
5891 let numeric_aw = simpson_integral(cell.left, cell.right, 5000, |z| {
5892 (eta_aw(z) - cell.eta(z) * eta_a(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5893 });
5894 let numeric_bw = simpson_integral(cell.left, cell.right, 5000, |z| {
5895 (eta_bw(z) - cell.eta(z) * eta_b(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5896 });
5897 let numeric_ww = simpson_integral(cell.left, cell.right, 5000, |z| {
5898 (-cell.eta(z) * eta_w(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5899 });
5900 let numeric_aaw = simpson_integral(cell.left, cell.right, 5000, |z| {
5901 let eta = cell.eta(z);
5902 let w_z = eta_w(z);
5903 let a_z = eta_a(z);
5904 (eta_aaw(z) - eta * (eta_aa(z) * w_z + 2.0 * eta_aw(z) * a_z)
5905 + (eta * eta - 1.0) * a_z * a_z * w_z)
5906 * (-cell.q(z)).exp()
5907 * INV_TWO_PI
5908 });
5909 let numeric_abw = simpson_integral(cell.left, cell.right, 5000, |z| {
5910 let eta = cell.eta(z);
5911 let w_z = eta_w(z);
5912 let a_z = eta_a(z);
5913 let b_z = eta_b(z);
5914 (eta_abw(z) - eta * (eta_ab(z) * w_z + eta_aw(z) * b_z + eta_bw(z) * a_z)
5915 + (eta * eta - 1.0) * a_z * b_z * w_z)
5916 * (-cell.q(z)).exp()
5917 * INV_TWO_PI
5918 });
5919 let numeric_bbw = simpson_integral(cell.left, cell.right, 5000, |z| {
5920 let eta = cell.eta(z);
5921 let w_z = eta_w(z);
5922 let b_z = eta_b(z);
5923 (eta_bbw(z) - eta * (eta_bb(z) * w_z + 2.0 * eta_bw(z) * b_z)
5924 + (eta * eta - 1.0) * b_z * b_z * w_z)
5925 * (-cell.q(z)).exp()
5926 * INV_TWO_PI
5927 });
5928 let numeric_www = simpson_integral(cell.left, cell.right, 5000, |z| {
5929 let eta = cell.eta(z);
5930 let w_z = eta_w(z);
5931 ((eta * eta - 1.0) * w_z * w_z * w_z) * (-cell.q(z)).exp() * INV_TWO_PI
5932 });
5933 let numeric_aaaw = simpson_integral(cell.left, cell.right, 5000, |z| {
5934 let eta = cell.eta(z);
5935 let a_z = eta_a(z);
5936 let w_z = eta_w(z);
5937 let aa_z = eta_aa(z);
5938 let aw_z = eta_aw(z);
5939 (eta_aaaw(z)
5940 - eta * ((dc_daaa[0] + 0.0 * z) * w_z + 3.0 * eta_aaw(z) * a_z + 3.0 * aa_z * aw_z)
5941 + (eta * eta - 1.0) * (3.0 * aa_z * a_z * w_z + 3.0 * aw_z * a_z * a_z)
5942 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * a_z * w_z)
5943 * (-cell.q(z)).exp()
5944 * INV_TWO_PI
5945 });
5946 let numeric_aaww = simpson_integral(cell.left, cell.right, 5000, |z| {
5947 let eta = cell.eta(z);
5948 let a_z = eta_a(z);
5949 let w_z = eta_w(z);
5950 let aw_z = eta_aw(z);
5951 (-(2.0 * eta * (eta_aaw(z) * w_z + aw_z * aw_z))
5952 + (eta * eta - 1.0) * (eta_aa(z) * w_z * w_z + 4.0 * aw_z * a_z * w_z)
5953 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * w_z * w_z)
5954 * (-cell.q(z)).exp()
5955 * INV_TWO_PI
5956 });
5957 let numeric_abww = simpson_integral(cell.left, cell.right, 5000, |z| {
5958 let eta = cell.eta(z);
5959 let a_z = eta_a(z);
5960 let b_z = eta_b(z);
5961 let w_z = eta_w(z);
5962 let aw_z = eta_aw(z);
5963 let bw_z = eta_bw(z);
5964 (-(2.0 * eta * (eta_abw(z) * w_z + aw_z * bw_z))
5965 + (eta * eta - 1.0)
5966 * (eta_ab(z) * w_z * w_z + 2.0 * aw_z * b_z * w_z + 2.0 * bw_z * a_z * w_z)
5967 + (-eta * eta * eta + 3.0 * eta) * a_z * b_z * w_z * w_z)
5968 * (-cell.q(z)).exp()
5969 * INV_TWO_PI
5970 });
5971 let numeric_bbww = simpson_integral(cell.left, cell.right, 5000, |z| {
5972 let eta = cell.eta(z);
5973 let b_z = eta_b(z);
5974 let w_z = eta_w(z);
5975 let bw_z = eta_bw(z);
5976 (-(2.0 * eta * (eta_bbw(z) * w_z + bw_z * bw_z))
5977 + (eta * eta - 1.0) * (eta_bb(z) * w_z * w_z + 4.0 * bw_z * b_z * w_z)
5978 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * w_z * w_z)
5979 * (-cell.q(z)).exp()
5980 * INV_TWO_PI
5981 });
5982 let numeric_bbbw = simpson_integral(cell.left, cell.right, 5000, |z| {
5983 let eta = cell.eta(z);
5984 let b_z = eta_b(z);
5985 let w_z = eta_w(z);
5986 let bb_z = eta_bb(z);
5987 let bw_z = eta_bw(z);
5988 (eta_bbbw(z)
5989 - eta
5990 * ((dc_dbbb[3] * z * z * z) * w_z + 3.0 * eta_bbw(z) * b_z + 3.0 * bb_z * bw_z)
5991 + (eta * eta - 1.0) * (3.0 * bb_z * b_z * w_z + 3.0 * bw_z * b_z * b_z)
5992 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * b_z * w_z)
5993 * (-cell.q(z)).exp()
5994 * INV_TWO_PI
5995 });
5996 let numeric_wwww = simpson_integral(cell.left, cell.right, 5000, |z| {
5997 let eta = cell.eta(z);
5998 let w_z = eta_w(z);
5999 ((-eta * eta * eta + 3.0 * eta) * w_z * w_z * w_z * w_z)
6000 * (-cell.q(z)).exp()
6001 * INV_TWO_PI
6002 });
6003
6004 assert!((exact_w - numeric_w).abs() < 1e-8);
6005 assert!((exact_aw - numeric_aw).abs() < 1e-7);
6006 assert!((exact_bw - numeric_bw).abs() < 1e-7);
6007 assert!((exact_ww - numeric_ww).abs() < 1e-7);
6008 assert!((exact_aaw - numeric_aaw).abs() < 2e-6);
6009 assert!((exact_abw - numeric_abw).abs() < 2e-6);
6010 assert!((exact_bbw - numeric_bbw).abs() < 2e-6);
6011 assert!((exact_www - numeric_www).abs() < 2e-6);
6012 assert!((exact_aaaw - numeric_aaaw).abs() < 3e-6);
6013 assert!((exact_aaww - numeric_aaww).abs() < 3e-6);
6014 assert!((exact_abww - numeric_abww).abs() < 3e-6);
6015 assert!((exact_bbww - numeric_bbww).abs() < 3e-6);
6016 assert!((exact_bbbw - numeric_bbbw).abs() < 3e-6);
6017 assert!((exact_wwww - numeric_wwww).abs() < 3e-6);
6018 }
6019
6020 #[test]
6021 fn score_basis_cell_derivatives_match_exact_integrands() {
6022 let score_span = LocalSpanCubic {
6023 left: -0.75,
6024 right: 0.25,
6025 c0: 0.08,
6026 c1: -0.03,
6027 c2: 0.02,
6028 c3: -0.01,
6029 };
6030 let score_basis_span = LocalSpanCubic {
6031 left: -0.75,
6032 right: 0.25,
6033 c0: -0.04,
6034 c1: 0.06,
6035 c2: -0.01,
6036 c3: 0.02,
6037 };
6038 let link_span = LocalSpanCubic {
6039 left: -0.6,
6040 right: 0.9,
6041 c0: -0.05,
6042 c1: 0.04,
6043 c2: -0.02,
6044 c3: 0.015,
6045 };
6046 let a = 0.3;
6047 let b = -0.7;
6048 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
6049 let cell = DenestedCubicCell {
6050 left: score_span.left,
6051 right: score_span.right,
6052 c0: coeffs[0],
6053 c1: coeffs[1],
6054 c2: coeffs[2],
6055 c3: coeffs[3],
6056 };
6057 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
6058 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
6059 let second_partials = denested_cell_second_partials(score_span, link_span, a, b);
6060 let dc_daa = second_partials.0;
6061 let dc_dab = second_partials.1;
6062 let dc_dbb = second_partials.2;
6063 let denested_third = denested_cell_third_partials(link_span);
6064 let dc_dbbb = denested_third.3;
6065
6066 let coeff_h = score_basis_cell_coefficients(score_basis_span, b);
6067 let coeff_bh = score_basis_cell_coefficients(score_basis_span, 1.0);
6068 let zero = [0.0; 4];
6069
6070 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
6071 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
6072 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
6073 let eta_bb = |z: f64| z * z * link_span.second_derivative(a + b * z);
6074 let eta_h = |z: f64| b * score_basis_span.evaluate(z);
6075 let eta_bh = |z: f64| score_basis_span.evaluate(z);
6076
6077 let exact_h = cell_first_derivative_from_moments(&coeff_h, &state.moments).expect("h");
6078 let exact_ah =
6079 cell_second_derivative_from_moments(cell, &dc_da, &coeff_h, &zero, &state.moments)
6080 .expect("ah");
6081 let exact_bh =
6082 cell_second_derivative_from_moments(cell, &dc_db, &coeff_h, &coeff_bh, &state.moments)
6083 .expect("bh");
6084 let exact_hh =
6085 cell_second_derivative_from_moments(cell, &coeff_h, &coeff_h, &zero, &state.moments)
6086 .expect("hh");
6087 let exact_abh = cell_third_derivative_from_moments(
6088 cell,
6089 &dc_da,
6090 &dc_db,
6091 &coeff_h,
6092 &dc_dab,
6093 &zero,
6094 &coeff_bh,
6095 &zero,
6096 &state.moments,
6097 )
6098 .expect("abh");
6099 let exact_bbh = cell_third_derivative_from_moments(
6100 cell,
6101 &dc_db,
6102 &dc_db,
6103 &coeff_h,
6104 &dc_dbb,
6105 &coeff_bh,
6106 &coeff_bh,
6107 &zero,
6108 &state.moments,
6109 )
6110 .expect("bbh");
6111 let exact_bhh = cell_third_derivative_from_moments(
6112 cell,
6113 &dc_db,
6114 &coeff_h,
6115 &coeff_h,
6116 &coeff_bh,
6117 &coeff_bh,
6118 &zero,
6119 &zero,
6120 &state.moments,
6121 )
6122 .expect("bhh");
6123 let exact_hhh = cell_third_derivative_from_moments(
6124 cell,
6125 &coeff_h,
6126 &coeff_h,
6127 &coeff_h,
6128 &zero,
6129 &zero,
6130 &zero,
6131 &zero,
6132 &state.moments,
6133 )
6134 .expect("hhh");
6135 let exact_bbbh = cell_fourth_derivative_from_moments(
6136 cell,
6137 &dc_db,
6138 &dc_db,
6139 &dc_db,
6140 &coeff_h,
6141 &dc_dbb,
6142 &dc_dbb,
6143 &coeff_bh,
6144 &dc_dbb,
6145 &coeff_bh,
6146 &coeff_bh,
6147 &dc_dbbb,
6148 &zero,
6149 &zero,
6150 &zero,
6151 &zero,
6152 &state.moments,
6153 )
6154 .expect("bbbh");
6155 let exact_aahh = cell_fourth_derivative_from_moments(
6156 cell,
6157 &dc_da,
6158 &dc_da,
6159 &coeff_h,
6160 &coeff_h,
6161 &dc_daa,
6162 &zero,
6163 &zero,
6164 &zero,
6165 &zero,
6166 &zero,
6167 &zero,
6168 &zero,
6169 &zero,
6170 &zero,
6171 &zero,
6172 &state.moments,
6173 )
6174 .expect("aahh");
6175 let exact_abhh = cell_fourth_derivative_from_moments(
6176 cell,
6177 &dc_da,
6178 &dc_db,
6179 &coeff_h,
6180 &coeff_h,
6181 &dc_dab,
6182 &zero,
6183 &zero,
6184 &coeff_bh,
6185 &coeff_bh,
6186 &zero,
6187 &zero,
6188 &zero,
6189 &zero,
6190 &zero,
6191 &zero,
6192 &state.moments,
6193 )
6194 .expect("abhh");
6195 let exact_bbhh = cell_fourth_derivative_from_moments(
6196 cell,
6197 &dc_db,
6198 &dc_db,
6199 &coeff_h,
6200 &coeff_h,
6201 &dc_dbb,
6202 &coeff_bh,
6203 &coeff_bh,
6204 &coeff_bh,
6205 &coeff_bh,
6206 &zero,
6207 &zero,
6208 &zero,
6209 &zero,
6210 &zero,
6211 &zero,
6212 &state.moments,
6213 )
6214 .expect("bbhh");
6215 let exact_bhhh = cell_fourth_derivative_from_moments(
6216 cell,
6217 &dc_db,
6218 &coeff_h,
6219 &coeff_h,
6220 &coeff_h,
6221 &coeff_bh,
6222 &coeff_bh,
6223 &coeff_bh,
6224 &zero,
6225 &zero,
6226 &zero,
6227 &zero,
6228 &zero,
6229 &zero,
6230 &zero,
6231 &zero,
6232 &state.moments,
6233 )
6234 .expect("bhhh");
6235 let exact_hhhh = cell_fourth_derivative_from_moments(
6236 cell,
6237 &coeff_h,
6238 &coeff_h,
6239 &coeff_h,
6240 &coeff_h,
6241 &zero,
6242 &zero,
6243 &zero,
6244 &zero,
6245 &zero,
6246 &zero,
6247 &zero,
6248 &zero,
6249 &zero,
6250 &zero,
6251 &zero,
6252 &state.moments,
6253 )
6254 .expect("hhhh");
6255
6256 let numeric_h = simpson_integral(cell.left, cell.right, 5000, |z| {
6257 eta_h(z) * (-cell.q(z)).exp() * INV_TWO_PI
6258 });
6259 let numeric_ah = simpson_integral(cell.left, cell.right, 5000, |z| {
6260 (-cell.eta(z) * eta_a(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6261 });
6262 let numeric_bh = simpson_integral(cell.left, cell.right, 5000, |z| {
6263 (eta_bh(z) - cell.eta(z) * eta_b(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6264 });
6265 let numeric_hh = simpson_integral(cell.left, cell.right, 5000, |z| {
6266 (-cell.eta(z) * eta_h(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6267 });
6268 let numeric_abh = simpson_integral(cell.left, cell.right, 5000, |z| {
6269 let eta = cell.eta(z);
6270 (-(eta * (eta_ab(z) * eta_h(z) + eta_bh(z) * eta_a(z)))
6271 + (eta * eta - 1.0) * eta_a(z) * eta_b(z) * eta_h(z))
6272 * (-cell.q(z)).exp()
6273 * INV_TWO_PI
6274 });
6275 let numeric_bbh = simpson_integral(cell.left, cell.right, 5000, |z| {
6276 let eta = cell.eta(z);
6277 (-(eta * (eta_bb(z) * eta_h(z) + 2.0 * eta_bh(z) * eta_b(z)))
6278 + (eta * eta - 1.0) * eta_b(z) * eta_b(z) * eta_h(z))
6279 * (-cell.q(z)).exp()
6280 * INV_TWO_PI
6281 });
6282 let numeric_bhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6283 let eta = cell.eta(z);
6284 (-(2.0 * eta * eta_bh(z) * eta_h(z))
6285 + (eta * eta - 1.0) * eta_b(z) * eta_h(z) * eta_h(z))
6286 * (-cell.q(z)).exp()
6287 * INV_TWO_PI
6288 });
6289 let numeric_hhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6290 let eta = cell.eta(z);
6291 ((eta * eta - 1.0) * eta_h(z) * eta_h(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6292 });
6293 let numeric_bbbh = simpson_integral(cell.left, cell.right, 5000, |z| {
6294 let eta = cell.eta(z);
6295 let b_z = eta_b(z);
6296 let h_z = eta_h(z);
6297 let bb_z = eta_bb(z);
6298 let bh_z = eta_bh(z);
6299 (-(eta * ((dc_dbbb[3] * z * z * z) * h_z + 3.0 * bb_z * bh_z))
6300 + (eta * eta - 1.0) * (3.0 * bb_z * b_z * h_z + 3.0 * bh_z * b_z * b_z)
6301 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * b_z * h_z)
6302 * (-cell.q(z)).exp()
6303 * INV_TWO_PI
6304 });
6305 let numeric_aahh = simpson_integral(cell.left, cell.right, 5000, |z| {
6306 let eta = cell.eta(z);
6307 let a_z = eta_a(z);
6308 let h_z = eta_h(z);
6309 ((eta * eta - 1.0) * polynomial_value(&dc_daa, z) * h_z * h_z
6310 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * h_z * h_z)
6311 * (-cell.q(z)).exp()
6312 * INV_TWO_PI
6313 });
6314 let numeric_abhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6315 let eta = cell.eta(z);
6316 let a_z = eta_a(z);
6317 let b_z = eta_b(z);
6318 let h_z = eta_h(z);
6319 ((eta * eta - 1.0) * (eta_ab(z) * h_z * h_z + 2.0 * eta_bh(z) * a_z * h_z)
6320 + (-eta * eta * eta + 3.0 * eta) * a_z * b_z * h_z * h_z)
6321 * (-cell.q(z)).exp()
6322 * INV_TWO_PI
6323 });
6324 let numeric_bbhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6325 let eta = cell.eta(z);
6326 let b_z = eta_b(z);
6327 let h_z = eta_h(z);
6328 let bh_z = eta_bh(z);
6329 (-(2.0 * eta * bh_z * bh_z)
6330 + (eta * eta - 1.0) * (eta_bb(z) * h_z * h_z + 4.0 * bh_z * b_z * h_z)
6331 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * h_z * h_z)
6332 * (-cell.q(z)).exp()
6333 * INV_TWO_PI
6334 });
6335 let numeric_bhhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6336 let eta = cell.eta(z);
6337 let h_z = eta_h(z);
6338 (-(eta * (3.0 * eta_bh(z) * h_z * h_z))
6339 + (eta * eta - 1.0) * (3.0 * eta_bh(z) * h_z * h_z)
6340 + (-eta * eta * eta + 3.0 * eta) * eta_b(z) * h_z * h_z * h_z)
6341 * (-cell.q(z)).exp()
6342 * INV_TWO_PI
6343 });
6344 let numeric_hhhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6345 let eta = cell.eta(z);
6346 let h_z = eta_h(z);
6347 ((-eta * eta * eta + 3.0 * eta) * h_z * h_z * h_z * h_z)
6348 * (-cell.q(z)).exp()
6349 * INV_TWO_PI
6350 });
6351
6352 assert!((exact_h - numeric_h).abs() < 1e-8);
6353 assert!((exact_ah - numeric_ah).abs() < 1e-7);
6354 assert!((exact_bh - numeric_bh).abs() < 1e-7);
6355 assert!((exact_hh - numeric_hh).abs() < 1e-7);
6356 assert!((exact_abh - numeric_abh).abs() < 2e-6);
6357 assert!((exact_bbh - numeric_bbh).abs() < 2e-6);
6358 assert!((exact_bhh - numeric_bhh).abs() < 2e-6);
6359 assert!((exact_hhh - numeric_hhh).abs() < 2e-6);
6360 assert!((exact_bbbh - numeric_bbbh).abs() < 3e-6);
6361 assert!((exact_aahh - numeric_aahh).abs() < 3e-6);
6362 assert!((exact_abhh - numeric_abhh).abs() < 3e-6);
6363 assert!((exact_bbhh - numeric_bbhh).abs() < 3e-6);
6364 assert!((exact_bhhh - numeric_bhhh).abs() < 3e-6);
6365 assert!((exact_hhhh - numeric_hhhh).abs() < 3e-6);
6366 }
6367
6368 #[test]
6369 fn cross_basis_cell_derivatives_match_exact_integrands() {
6370 let score_span = LocalSpanCubic {
6371 left: -0.75,
6372 right: 0.25,
6373 c0: 0.08,
6374 c1: -0.03,
6375 c2: 0.02,
6376 c3: -0.01,
6377 };
6378 let score_basis_span = LocalSpanCubic {
6379 left: -0.75,
6380 right: 0.25,
6381 c0: -0.04,
6382 c1: 0.06,
6383 c2: -0.01,
6384 c3: 0.02,
6385 };
6386 let link_span = LocalSpanCubic {
6387 left: -0.6,
6388 right: 0.9,
6389 c0: -0.05,
6390 c1: 0.04,
6391 c2: -0.02,
6392 c3: 0.015,
6393 };
6394 let link_basis_span = LocalSpanCubic {
6395 left: -0.6,
6396 right: 0.9,
6397 c0: 0.02,
6398 c1: -0.01,
6399 c2: 0.03,
6400 c3: -0.02,
6401 };
6402 let a = 0.3;
6403 let b = -0.7;
6404 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
6405 let cell = DenestedCubicCell {
6406 left: score_span.left,
6407 right: score_span.right,
6408 c0: coeffs[0],
6409 c1: coeffs[1],
6410 c2: coeffs[2],
6411 c3: coeffs[3],
6412 };
6413 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
6414 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
6415 let (dc_daa, dc_dab, _) = denested_cell_second_partials(score_span, link_span, a, b);
6416
6417 let coeff_h = score_basis_cell_coefficients(score_basis_span, b);
6418 let coeff_bh = score_basis_cell_coefficients(score_basis_span, 1.0);
6419 let coeff_w = link_basis_cell_coefficients(link_basis_span, a, b);
6420 let (coeff_aw, coeff_bw) = link_basis_cell_coefficient_partials(link_basis_span, a, b);
6421 let (coeff_aaw, coeff_abw, _) = link_basis_cell_second_partials(link_basis_span, a, b);
6422 let zero = [0.0; 4];
6423
6424 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
6425 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
6426 let eta_h = |z: f64| b * score_basis_span.evaluate(z);
6427 let eta_bh = |z: f64| score_basis_span.evaluate(z);
6428 let eta_w = |z: f64| link_basis_span.evaluate(a + b * z);
6429 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
6430 let eta_aw = |z: f64| link_basis_span.first_derivative(a + b * z);
6431 let eta_bw = |z: f64| z * link_basis_span.first_derivative(a + b * z);
6432
6433 let exact_hw =
6434 cell_second_derivative_from_moments(cell, &coeff_h, &coeff_w, &zero, &state.moments)
6435 .expect("hw");
6436 let exact_ahw = cell_third_derivative_from_moments(
6437 cell,
6438 &dc_da,
6439 &coeff_h,
6440 &coeff_w,
6441 &zero,
6442 &coeff_aw,
6443 &zero,
6444 &zero,
6445 &state.moments,
6446 )
6447 .expect("ahw");
6448 let exact_bhw = cell_third_derivative_from_moments(
6449 cell,
6450 &dc_db,
6451 &coeff_h,
6452 &coeff_w,
6453 &coeff_bh,
6454 &coeff_bw,
6455 &zero,
6456 &zero,
6457 &state.moments,
6458 )
6459 .expect("bhw");
6460 let exact_hhw = cell_third_derivative_from_moments(
6461 cell,
6462 &coeff_h,
6463 &coeff_h,
6464 &coeff_w,
6465 &zero,
6466 &zero,
6467 &zero,
6468 &zero,
6469 &state.moments,
6470 )
6471 .expect("hhw");
6472 let exact_hww = cell_third_derivative_from_moments(
6473 cell,
6474 &coeff_h,
6475 &coeff_w,
6476 &coeff_w,
6477 &zero,
6478 &zero,
6479 &zero,
6480 &zero,
6481 &state.moments,
6482 )
6483 .expect("hww");
6484 let exact_aahw = cell_fourth_derivative_from_moments(
6485 cell,
6486 &dc_da,
6487 &dc_da,
6488 &coeff_h,
6489 &coeff_w,
6490 &dc_daa,
6491 &zero,
6492 &coeff_aw,
6493 &zero,
6494 &coeff_aw,
6495 &zero,
6496 &zero,
6497 &coeff_aaw,
6498 &zero,
6499 &zero,
6500 &zero,
6501 &state.moments,
6502 )
6503 .expect("aahw");
6504 let exact_hhww = cell_fourth_derivative_from_moments(
6505 cell,
6506 &coeff_h,
6507 &coeff_h,
6508 &coeff_w,
6509 &coeff_w,
6510 &zero,
6511 &zero,
6512 &zero,
6513 &zero,
6514 &zero,
6515 &zero,
6516 &zero,
6517 &zero,
6518 &zero,
6519 &zero,
6520 &zero,
6521 &state.moments,
6522 )
6523 .expect("hhww");
6524 let exact_hhhw = cell_fourth_derivative_from_moments(
6525 cell,
6526 &coeff_h,
6527 &coeff_h,
6528 &coeff_h,
6529 &coeff_w,
6530 &zero,
6531 &zero,
6532 &zero,
6533 &zero,
6534 &zero,
6535 &zero,
6536 &zero,
6537 &zero,
6538 &zero,
6539 &zero,
6540 &zero,
6541 &state.moments,
6542 )
6543 .expect("hhhw");
6544 let exact_abhw = cell_fourth_derivative_from_moments(
6545 cell,
6546 &dc_da,
6547 &dc_db,
6548 &coeff_h,
6549 &coeff_w,
6550 &dc_dab,
6551 &zero,
6552 &coeff_aw,
6553 &coeff_bh,
6554 &coeff_bw,
6555 &zero,
6556 &zero,
6557 &coeff_abw,
6558 &zero,
6559 &zero,
6560 &zero,
6561 &state.moments,
6562 )
6563 .expect("abhw");
6564 let exact_ahww = cell_fourth_derivative_from_moments(
6565 cell,
6566 &dc_da,
6567 &coeff_h,
6568 &coeff_w,
6569 &coeff_w,
6570 &zero,
6571 &coeff_aw,
6572 &coeff_aw,
6573 &zero,
6574 &zero,
6575 &zero,
6576 &zero,
6577 &zero,
6578 &zero,
6579 &zero,
6580 &zero,
6581 &state.moments,
6582 )
6583 .expect("ahww");
6584 let exact_bhww = cell_fourth_derivative_from_moments(
6585 cell,
6586 &dc_db,
6587 &coeff_h,
6588 &coeff_w,
6589 &coeff_w,
6590 &coeff_bh,
6591 &coeff_bw,
6592 &coeff_bw,
6593 &zero,
6594 &zero,
6595 &zero,
6596 &zero,
6597 &zero,
6598 &zero,
6599 &zero,
6600 &zero,
6601 &state.moments,
6602 )
6603 .expect("bhww");
6604 let exact_hwww = cell_fourth_derivative_from_moments(
6605 cell,
6606 &coeff_h,
6607 &coeff_w,
6608 &coeff_w,
6609 &coeff_w,
6610 &zero,
6611 &zero,
6612 &zero,
6613 &zero,
6614 &zero,
6615 &zero,
6616 &zero,
6617 &zero,
6618 &zero,
6619 &zero,
6620 &zero,
6621 &state.moments,
6622 )
6623 .expect("hwww");
6624
6625 let numeric_hw = simpson_integral(cell.left, cell.right, 5000, |z| {
6626 (-cell.eta(z) * eta_h(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6627 });
6628 let numeric_ahw = simpson_integral(cell.left, cell.right, 5000, |z| {
6629 let eta = cell.eta(z);
6630 (-(eta * eta_aw(z) * eta_h(z)) + (eta * eta - 1.0) * eta_a(z) * eta_h(z) * eta_w(z))
6631 * (-cell.q(z)).exp()
6632 * INV_TWO_PI
6633 });
6634 let numeric_bhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6635 let eta = cell.eta(z);
6636 (-(eta * (eta_bh(z) * eta_w(z) + eta_bw(z) * eta_h(z)))
6637 + (eta * eta - 1.0) * eta_b(z) * eta_h(z) * eta_w(z))
6638 * (-cell.q(z)).exp()
6639 * INV_TWO_PI
6640 });
6641 let numeric_hhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6642 let eta = cell.eta(z);
6643 ((eta * eta - 1.0) * eta_h(z) * eta_h(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6644 });
6645 let numeric_hww = simpson_integral(cell.left, cell.right, 5000, |z| {
6646 let eta = cell.eta(z);
6647 ((eta * eta - 1.0) * eta_h(z) * eta_w(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6648 });
6649 let numeric_aahw = simpson_integral(cell.left, cell.right, 5000, |z| {
6650 let eta = cell.eta(z);
6651 (-(eta * polynomial_value(&coeff_aaw, z) * eta_h(z))
6652 + (eta * eta - 1.0)
6653 * (polynomial_value(&dc_daa, z) * eta_h(z) * eta_w(z)
6654 + 2.0 * eta_aw(z) * eta_a(z) * eta_h(z))
6655 + (-eta * eta * eta + 3.0 * eta) * eta_a(z) * eta_a(z) * eta_h(z) * eta_w(z))
6656 * (-cell.q(z)).exp()
6657 * INV_TWO_PI
6658 });
6659 let numeric_hhww = simpson_integral(cell.left, cell.right, 5000, |z| {
6660 let eta = cell.eta(z);
6661 ((-eta * eta * eta + 3.0 * eta) * eta_h(z) * eta_h(z) * eta_w(z) * eta_w(z))
6662 * (-cell.q(z)).exp()
6663 * INV_TWO_PI
6664 });
6665 let numeric_hhhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6666 let eta = cell.eta(z);
6667 ((-eta * eta * eta + 3.0 * eta) * eta_h(z) * eta_h(z) * eta_h(z) * eta_w(z))
6668 * (-cell.q(z)).exp()
6669 * INV_TWO_PI
6670 });
6671 let numeric_abhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6672 let eta = cell.eta(z);
6673 (-(eta * polynomial_value(&coeff_abw, z) * eta_h(z) + eta * eta_aw(z) * eta_bh(z))
6674 + (eta * eta - 1.0)
6675 * (eta_ab(z) * eta_h(z) * eta_w(z)
6676 + eta_aw(z) * eta_b(z) * eta_h(z)
6677 + eta_bh(z) * eta_a(z) * eta_w(z)
6678 + eta_bw(z) * eta_a(z) * eta_h(z))
6679 + (-eta * eta * eta + 3.0 * eta) * eta_a(z) * eta_b(z) * eta_h(z) * eta_w(z))
6680 * (-cell.q(z)).exp()
6681 * INV_TWO_PI
6682 });
6683 let numeric_ahww = simpson_integral(cell.left, cell.right, 5000, |z| {
6684 let eta = cell.eta(z);
6685 (2.0 * (eta * eta - 1.0) * eta_aw(z) * eta_h(z) * eta_w(z)
6686 + (-eta * eta * eta + 3.0 * eta) * eta_a(z) * eta_h(z) * eta_w(z) * eta_w(z))
6687 * (-cell.q(z)).exp()
6688 * INV_TWO_PI
6689 });
6690 let numeric_bhww = simpson_integral(cell.left, cell.right, 5000, |z| {
6691 let eta = cell.eta(z);
6692 let h_z = eta_h(z);
6693 let w_z = eta_w(z);
6694 ((eta * eta - 1.0) * (eta_bh(z) * w_z * w_z + 2.0 * eta_bw(z) * h_z * w_z)
6695 + (-eta * eta * eta + 3.0 * eta) * eta_b(z) * h_z * w_z * w_z)
6696 * (-cell.q(z)).exp()
6697 * INV_TWO_PI
6698 });
6699 let numeric_hwww = simpson_integral(cell.left, cell.right, 5000, |z| {
6700 let eta = cell.eta(z);
6701 ((-eta * eta * eta + 3.0 * eta) * eta_h(z) * eta_w(z) * eta_w(z) * eta_w(z))
6702 * (-cell.q(z)).exp()
6703 * INV_TWO_PI
6704 });
6705
6706 assert!((exact_hw - numeric_hw).abs() < 1e-7);
6707 assert!((exact_ahw - numeric_ahw).abs() < 2e-6);
6708 assert!((exact_bhw - numeric_bhw).abs() < 2e-6);
6709 assert!((exact_hhw - numeric_hhw).abs() < 2e-6);
6710 assert!((exact_hww - numeric_hww).abs() < 2e-6);
6711 assert!((exact_aahw - numeric_aahw).abs() < 3e-6);
6712 assert!((exact_hhww - numeric_hhww).abs() < 3e-6);
6713 assert!((exact_hhhw - numeric_hhhw).abs() < 3e-6);
6714 assert!((exact_abhw - numeric_abhw).abs() < 3e-6);
6715 assert!((exact_ahww - numeric_ahww).abs() < 3e-6);
6716 assert!((exact_bhww - numeric_bhww).abs() < 3e-6);
6717 assert!((exact_hwww - numeric_hwww).abs() < 3e-6);
6718 }
6719
6720 #[test]
6721 fn cell_moment_scratch_reuses_buffers_under_margslope_like_pressure() {
6722 let cells = [
6723 DenestedCubicCell {
6724 left: -1.2,
6725 right: -0.35,
6726 c0: 0.18,
6727 c1: 0.72,
6728 c2: -0.045,
6729 c3: 0.018,
6730 },
6731 DenestedCubicCell {
6732 left: -0.35,
6733 right: 0.48,
6734 c0: -0.08,
6735 c1: 0.91,
6736 c2: 0.038,
6737 c3: -0.014,
6738 },
6739 DenestedCubicCell {
6740 left: 0.48,
6741 right: 1.4,
6742 c0: 0.11,
6743 c1: 0.83,
6744 c2: 0.022,
6745 c3: 0.012,
6746 },
6747 ];
6748 let mut scratch = CellMomentScratch::with_capacity(MAX_AFFINE_ANCHOR_DEGREE);
6749 for cell in cells {
6750 let baseline = evaluate_cell_moments(cell, 9).expect("baseline moments");
6751 let scratch_state =
6752 evaluate_cell_moments_with_scratch(cell, 9, &mut scratch).expect("scratch moments");
6753 assert_eq!(baseline.branch, scratch_state.branch);
6754 assert!((baseline.value - scratch_state.value).abs() <= 1e-10);
6755 assert_eq!(baseline.moments.len(), scratch_state.moments.len());
6756 for (lhs, rhs) in baseline.moments.iter().zip(scratch_state.moments.iter()) {
6757 assert!((lhs - rhs).abs() <= 1e-10, "{lhs} vs {rhs}");
6758 }
6759 }
6760
6761 reset_cell_moment_test_reallocs();
6762 let mut checksum = 0.0;
6763 for i in 0..5_000 {
6764 let cell = cells[i % cells.len()];
6765 let state = evaluate_cell_moments_with_scratch(cell, 9, &mut scratch)
6766 .expect("scratch moments under repeated pressure");
6767 checksum += state.value + state.moments[0] * 1e-12;
6768 }
6769 assert!(checksum.is_finite());
6770 assert_eq!(
6771 cell_moment_test_reallocs(),
6772 0,
6773 "scratch-backed inner cell-moment calls should not grow Vec buffers"
6774 );
6775 }
6776
6777 #[test]
6778 fn evaluate_cell_moments_matches_numeric_integrals() {
6779 let cell = DenestedCubicCell {
6780 left: -0.9,
6781 right: 0.8,
6782 c0: 0.15,
6783 c1: -0.35,
6784 c2: 0.11,
6785 c3: -0.07,
6786 };
6787 let state = evaluate_cell_moments(cell, 6).expect("cell moments");
6788 let value_numeric = simpson_integral(cell.left, cell.right, 4000, |z| {
6789 super::normal_cdf(cell.eta(z)) * normal_pdf(z)
6790 });
6791 assert!((state.value - value_numeric).abs() < 1e-9);
6792 for degree in 0..=6 {
6793 let target = simpson_integral(cell.left, cell.right, 4000, |z| {
6794 z.powi(degree as i32) * (-cell.q(z)).exp()
6795 });
6796 assert!((state.moments[degree] - target).abs() < 1e-9);
6797 }
6798 }
6799
6800 #[test]
6801 fn partition_builder_moves_link_preimages_with_intercept() {
6802 let score_breaks = [-2.0, -1.0, 0.0, 1.0, 2.0];
6803 let link_breaks = [-1.5, -0.5, 0.5, 1.5];
6804 let score_span = |z: f64| {
6805 let left = if z < -1.0 {
6806 -2.0
6807 } else if z < 0.0 {
6808 -1.0
6809 } else if z < 1.0 {
6810 0.0
6811 } else {
6812 1.0
6813 };
6814 Ok(LocalSpanCubic {
6815 left,
6816 right: left + 1.0,
6817 c0: 0.1,
6818 c1: 0.2,
6819 c2: 0.0,
6820 c3: 0.0,
6821 })
6822 };
6823 let link_span = |u: f64| {
6824 let left = if u < -0.5 {
6825 -1.5
6826 } else if u < 0.5 {
6827 -0.5
6828 } else {
6829 0.5
6830 };
6831 Ok(LocalSpanCubic {
6832 left,
6833 right: left + 1.0,
6834 c0: -0.05,
6835 c1: 0.1,
6836 c2: 0.0,
6837 c3: 0.0,
6838 })
6839 };
6840 let cells_a0 = build_denested_partition_cells(
6841 0.25,
6842 0.9,
6843 &score_breaks,
6844 &link_breaks,
6845 score_span,
6846 link_span,
6847 )
6848 .expect("cells a0");
6849 let cells_a1 = build_denested_partition_cells(
6850 0.55,
6851 0.9,
6852 &score_breaks,
6853 &link_breaks,
6854 score_span,
6855 link_span,
6856 )
6857 .expect("cells a1");
6858 assert!(cells_a0.len() >= score_breaks.len() - 1);
6859 assert!(
6860 cells_a0
6861 .windows(2)
6862 .all(|w| (w[0].cell.right - w[1].cell.left).abs() <= 1e-12)
6863 );
6864 assert!(
6865 cells_a0
6866 .iter()
6867 .zip(cells_a1.iter())
6868 .any(|(lhs, rhs)| (lhs.cell.left - rhs.cell.left).abs() > 1e-10)
6869 );
6870 assert!(cells_a0.first().unwrap().cell.left.is_infinite());
6871 assert!(cells_a0.last().unwrap().cell.right.is_infinite());
6872 }
6873
6874 #[test]
6875 fn partition_builder_without_breaks_returns_single_global_cell() {
6876 let cells = build_denested_partition_cells_with_tails(
6877 0.3,
6878 -0.4,
6879 &[],
6880 &[],
6881 |z| {
6882 if z.is_nan() {
6883 return Err("probe z is NaN".to_string());
6884 }
6885 Ok(LocalSpanCubic {
6886 left: 0.0,
6887 right: 1.0,
6888 c0: 0.0,
6889 c1: 0.0,
6890 c2: 0.0,
6891 c3: 0.0,
6892 })
6893 },
6894 |u| {
6895 if u.is_nan() {
6896 return Err("probe u is NaN".to_string());
6897 }
6898 Ok(LocalSpanCubic {
6899 left: 0.0,
6900 right: 1.0,
6901 c0: 0.0,
6902 c1: 0.0,
6903 c2: 0.0,
6904 c3: 0.0,
6905 })
6906 },
6907 )
6908 .expect("global cell");
6909 assert_eq!(cells.len(), 1);
6910 assert_eq!(cells[0].cell.left, f64::NEG_INFINITY);
6911 assert_eq!(cells[0].cell.right, f64::INFINITY);
6912 assert!(cells[0].cell.c2.abs() < 1e-12);
6913 assert!(cells[0].cell.c3.abs() < 1e-12);
6914 }
6915
6916 #[test]
6917 fn polynomial_integral_helper_matches_moment_sum() {
6918 let cell = DenestedCubicCell {
6919 left: -1.5,
6920 right: 1.25,
6921 c0: 0.2,
6922 c1: -0.4,
6923 c2: 0.15,
6924 c3: 0.03,
6925 };
6926 let state = evaluate_cell_moments(cell, 8).expect("cell moments");
6927 let coeffs = [1.5, -0.25, 0.75, 0.1];
6928 let expected = INV_TWO_PI
6929 * coeffs
6930 .iter()
6931 .enumerate()
6932 .map(|(idx, coeff)| coeff * state.moments[idx])
6933 .sum::<f64>();
6934 let got = cell_polynomial_integral_from_moments(&coeffs, &state.moments, "test poly")
6935 .expect("poly integral");
6936 assert!((got - expected).abs() < 1e-14);
6937 }
6938
6939 #[test]
6940 fn batched_cell_moment_max_degree_matches_direct_non_affine_grid() {
6941 let cells = [
6942 DenestedCubicCell {
6943 left: -2.0,
6944 right: -0.25,
6945 c0: -0.7,
6946 c1: 0.8,
6947 c2: 0.015,
6948 c3: -0.004,
6949 },
6950 DenestedCubicCell {
6951 left: -0.5,
6952 right: 0.75,
6953 c0: 0.2,
6954 c1: -0.35,
6955 c2: -0.025,
6956 c3: 0.0,
6957 },
6958 DenestedCubicCell {
6959 left: 0.1,
6960 right: 1.6,
6961 c0: 0.4,
6962 c1: 0.25,
6963 c2: 0.01,
6964 c3: 0.006,
6965 },
6966 DenestedCubicCell {
6967 left: -1.25,
6968 right: 2.25,
6969 c0: -0.1,
6970 c1: 0.55,
6971 c2: -0.012,
6972 c3: 0.003,
6973 },
6974 ];
6975 for cell in cells {
6976 let branch = branch_cell(cell).expect("branch");
6977 if branch == ExactCellBranch::Affine {
6978 continue;
6979 }
6980 let batched =
6981 evaluate_non_affine_cell_state(cell, branch, 21).expect("degree-21 state");
6982 for degree in [9usize, 15, 21] {
6983 let direct =
6984 evaluate_non_affine_cell_state(cell, branch, degree).expect("direct state");
6985 assert_eq!(batched.branch, direct.branch);
6986 let denom = direct.value.abs().max(1.0);
6987 assert!(((batched.value - direct.value).abs() / denom) < 1e-10);
6988 for k in 0..=degree {
6989 let denom = direct.moments[k].abs().max(1.0);
6990 let rel = (batched.moments[k] - direct.moments[k]).abs() / denom;
6991 assert!(
6992 rel < 1e-10,
6993 "cell={cell:?} degree={degree} moment={k} rel={rel:e}"
6994 );
6995 }
6996 }
6997 }
6998 }
6999
7000 #[test]
7001 fn derivative_moment_evaluator_matches_value_evaluator_moments() {
7002 let cells = [
7003 DenestedCubicCell {
7004 left: -2.0,
7005 right: -0.4,
7006 c0: 0.15,
7007 c1: -0.8,
7008 c2: 0.0,
7009 c3: 0.0,
7010 },
7011 DenestedCubicCell {
7012 left: -0.75,
7013 right: 1.4,
7014 c0: -0.25,
7015 c1: 0.6,
7016 c2: 0.12,
7017 c3: 0.0,
7018 },
7019 DenestedCubicCell {
7020 left: -1.1,
7021 right: 0.9,
7022 c0: 0.35,
7023 c1: -0.3,
7024 c2: 0.05,
7025 c3: -0.015,
7026 },
7027 ];
7028 for cell in cells {
7029 for degree in [4usize, 9, 15, 21] {
7030 let full = evaluate_cell_moments_uncached(cell, degree).expect("full moments");
7031 let derivative = evaluate_cell_derivative_moments_uncached(cell, degree)
7032 .expect("derivative moments");
7033 assert_eq!(full.branch, derivative.branch);
7034 assert_eq!(full.moments.len(), derivative.moments.len());
7035 for k in 0..full.moments.len() {
7036 assert_eq!(full.moments[k].to_bits(), derivative.moments[k].to_bits());
7037 }
7038 }
7039 }
7040 }
7041
7042 #[test]
7043 fn cell_moment_lru_matches_uncached_non_affine_grid() {
7044 let cache = CellMomentLruCache::new(16 * 1024 * 1024);
7045 let stats = CellMomentCacheStats::default();
7046 let c0s = [-0.75, 0.0, 0.5];
7047 let c1s = [-1.2, 0.25, 1.1];
7048 let c2s = [-0.18, 0.07];
7049 let c3s = [0.0, 0.025];
7050 let bounds = [(-2.0, -0.5), (-0.25, 1.5)];
7051 let degrees = [4usize, 9, 15, 21];
7052 for &c0 in &c0s {
7053 for &c1 in &c1s {
7054 for &c2 in &c2s {
7055 for &c3 in &c3s {
7056 for &(left, right) in &bounds {
7057 for &max_degree in °rees {
7058 let cell = DenestedCubicCell {
7059 left,
7060 right,
7061 c0,
7062 c1,
7063 c2,
7064 c3,
7065 };
7066 let branch = branch_cell(cell).expect("branch");
7067 if branch == ExactCellBranch::Affine {
7068 continue;
7069 }
7070 let expected =
7071 evaluate_non_affine_cell_state(cell, branch, max_degree)
7072 .expect("uncached non-affine moments");
7073 let got = evaluate_cell_moments_cached(
7074 cell,
7075 max_degree,
7076 &cache,
7077 Some(&stats),
7078 )
7079 .expect("cached moments");
7080 assert_eq!(got.branch, expected.branch);
7081 assert_eq!(got.moments.len(), max_degree + 1);
7082 let denom = expected.value.abs().max(1.0);
7083 assert!(
7084 ((got.value - expected.value).abs() / denom) < 1e-10,
7085 "value mismatch for {cell:?} degree {max_degree}: got {} expected {}",
7086 got.value,
7087 expected.value
7088 );
7089 for (idx, (&lhs, &rhs)) in
7090 got.moments.iter().zip(expected.moments.iter()).enumerate()
7091 {
7092 let denom = rhs.abs().max(1.0);
7093 assert!(
7094 ((lhs - rhs).abs() / denom) < 1e-10,
7095 "moment {idx} mismatch for {cell:?} degree {max_degree}: got {lhs} expected {rhs}"
7096 );
7097 }
7098 let warm = evaluate_cell_moments_cached(
7099 cell,
7100 max_degree,
7101 &cache,
7102 Some(&stats),
7103 )
7104 .expect("warm cached moments");
7105 assert_eq!(warm, got);
7106 }
7107 }
7108 }
7109 }
7110 }
7111 }
7112 let (hits, misses) = stats.snapshot();
7113 assert!(hits > 0, "expected warm LRU hits");
7114 assert!(misses > 0, "expected cold LRU misses");
7115 }
7116
7117 #[test]
7118 fn cell_moment_fingerprint_exact_cache_matches_current_evaluator() {
7119 let cells = [
7120 DenestedCubicCell {
7121 left: -1.75,
7122 right: -0.25,
7123 c0: 0.15,
7124 c1: -0.35,
7125 c2: 0.08,
7126 c3: -0.015,
7127 },
7128 DenestedCubicCell {
7129 left: -0.5,
7130 right: 0.8,
7131 c0: -0.2,
7132 c1: 0.45,
7133 c2: -0.12,
7134 c3: 0.025,
7135 },
7136 DenestedCubicCell {
7137 left: 0.1,
7138 right: 1.6,
7139 c0: 0.05,
7140 c1: 0.2,
7141 c2: 0.03,
7142 c3: 0.004,
7143 },
7144 ];
7145 let mut cache = std::collections::HashMap::new();
7146 for max_degree in [0usize, 3, 4, 9, 16] {
7147 for cell in cells {
7148 let baseline = evaluate_cell_moments(cell, max_degree).expect("baseline moments");
7149 let key = cell_moment_cache_key(cell, max_degree, 0.0);
7150 let cached = cache.entry(key).or_insert_with(|| {
7151 evaluate_cell_moments(cell, max_degree).expect("cached moments")
7152 });
7153 assert_eq!(baseline.branch, cached.branch);
7154 assert_eq!(baseline.value.to_bits(), cached.value.to_bits());
7155 assert_eq!(baseline.moments.len(), cached.moments.len());
7156 for (lhs, rhs) in baseline.moments.iter().zip(cached.moments.iter()) {
7157 assert_eq!(lhs.to_bits(), rhs.to_bits());
7158 }
7159 }
7160 }
7161 }
7162
7163 #[test]
7164 fn fuzzy_cell_moment_fingerprint_error_scales_with_epsilon() {
7165 for epsilon in [1e-8, 1e-6] {
7166 let base = DenestedCubicCell {
7167 left: -1.25,
7168 right: 1.1,
7169 c0: 0.1,
7170 c1: -0.25,
7171 c2: 0.04,
7172 c3: -0.006,
7173 };
7174 let perturbed = DenestedCubicCell {
7175 left: base.left + 0.001 * epsilon,
7176 right: base.right - 0.001 * epsilon,
7177 c0: base.c0 + 0.001 * epsilon,
7178 c1: base.c1 - 0.001 * epsilon,
7179 c2: base.c2 + 0.001 * epsilon,
7180 c3: base.c3 - 0.001 * epsilon,
7181 };
7182 assert_eq!(
7183 cell_moment_cache_key(base, 9, epsilon),
7184 cell_moment_cache_key(perturbed, 9, epsilon)
7185 );
7186 let lhs = evaluate_cell_moments(base, 9).expect("base moments");
7187 let rhs = evaluate_cell_moments(perturbed, 9).expect("perturbed moments");
7188 let max_rel = lhs
7189 .moments
7190 .iter()
7191 .zip(rhs.moments.iter())
7192 .map(|(a, b)| (a - b).abs() / a.abs().max(b.abs()).max(1.0))
7193 .fold(0.0_f64, f64::max);
7194 assert!(
7195 max_rel <= 10.0 * epsilon,
7196 "epsilon={epsilon:.1e} max_rel={max_rel:.3e}"
7197 );
7198 }
7199 }
7200
7201 #[test]
7209 fn non_affine_cell_state_matches_prefold_reference_to_1e_minus_13() {
7210 fn reference(
7214 cell: DenestedCubicCell,
7215 branch: ExactCellBranch,
7216 max_degree: usize,
7217 ) -> CellMomentState {
7218 let mut moments: CellMomentVec = smallvec![0.0_f64; max_degree + 1];
7219 let mut value_integral = 0.0_f64;
7220 let center = 0.5 * (cell.left + cell.right);
7221 let half_width = 0.5 * (cell.right - cell.left);
7222 for (&node, &weight) in GL_NODES.iter().zip(GL_WEIGHTS.iter()) {
7223 let z = center + half_width * node;
7224 let eta = cell.eta(z);
7225 let moment_weight = weight * (-cell.q(z)).exp();
7226 let mut z_pow = 1.0_f64;
7227 for moment in &mut moments {
7228 *moment = moment_weight.mul_add(z_pow, *moment);
7229 z_pow *= z;
7230 }
7231 value_integral += weight * (-0.5 * z * z).exp() * normal_cdf(eta);
7232 }
7233 for moment in &mut moments {
7234 *moment *= half_width;
7235 }
7236 CellMomentState {
7237 branch,
7238 value: value_integral * half_width / (std::f64::consts::TAU).sqrt(),
7239 moments,
7240 }
7241 }
7242
7243 let cells = [
7248 DenestedCubicCell {
7249 left: -1.25,
7250 right: -0.2,
7251 c0: -0.35,
7252 c1: 0.85,
7253 c2: 0.04,
7254 c3: -0.015,
7255 },
7256 DenestedCubicCell {
7257 left: -0.2,
7258 right: 0.55,
7259 c0: 0.12,
7260 c1: -0.65,
7261 c2: -0.025,
7262 c3: 0.02,
7263 },
7264 DenestedCubicCell {
7265 left: 0.55,
7266 right: 1.6,
7267 c0: 0.42,
7268 c1: 0.35,
7269 c2: 0.018,
7270 c3: 0.012,
7271 },
7272 DenestedCubicCell {
7273 left: -3.0,
7274 right: -1.0,
7275 c0: 1.7,
7276 c1: -0.4,
7277 c2: 0.11,
7278 c3: -0.07,
7279 },
7280 ];
7281 let degrees = [0_usize, 4, 9, 16, 24];
7282 for cell in cells {
7283 let branch = branch_cell(cell).expect("branch");
7284 assert_ne!(branch, ExactCellBranch::Affine);
7285 for max_degree in degrees {
7286 let actual = evaluate_non_affine_cell_state(cell, branch, max_degree)
7287 .expect("optimized non-affine");
7288 let expected = reference(cell, branch, max_degree);
7289 assert_eq!(actual.branch, expected.branch);
7290 assert_eq!(actual.moments.len(), expected.moments.len());
7291 let denom_v = expected.value.abs().max(1.0);
7292 let rel_v = (actual.value - expected.value).abs() / denom_v;
7293 let actual_v = actual.value;
7294 let expected_v = expected.value;
7295 assert!(
7296 rel_v <= 1e-13,
7297 "value rel mismatch for {cell:?} degree {max_degree}: \
7298 actual={actual_v:.17e} expected={expected_v:.17e} rel={rel_v:.3e}"
7299 );
7300 for (k, (lhs, rhs)) in actual
7301 .moments
7302 .iter()
7303 .zip(expected.moments.iter())
7304 .enumerate()
7305 {
7306 let denom = rhs.abs().max(1.0);
7307 let rel = (lhs - rhs).abs() / denom;
7308 assert!(
7309 rel <= 1e-13,
7310 "moment {k} rel mismatch for {cell:?} degree {max_degree}: \
7311 actual={lhs:.17e} expected={rhs:.17e} rel={rel:.3e}"
7312 );
7313 }
7314
7315 let actual_deriv =
7318 evaluate_non_affine_cell_derivative_state(cell, branch, max_degree)
7319 .expect("optimized derivative");
7320 for (k, (lhs, rhs)) in actual_deriv
7321 .moments
7322 .iter()
7323 .zip(expected.moments.iter())
7324 .enumerate()
7325 {
7326 let denom = rhs.abs().max(1.0);
7327 let rel = (lhs - rhs).abs() / denom;
7328 assert!(
7329 rel <= 1e-13,
7330 "deriv moment {k} rel mismatch for {cell:?} degree {max_degree}: \
7331 actual={lhs:.17e} expected={rhs:.17e} rel={rel:.3e}"
7332 );
7333 }
7334 }
7335 }
7336 }
7337
7338 #[test]
7344 fn third_derivative_kernel_matches_fd_of_second_with_eta_perturbation() {
7345 let base = DenestedCubicCell {
7347 left: -0.6,
7348 right: 0.9,
7349 c0: 0.30,
7350 c1: 0.45,
7351 c2: -0.20,
7352 c3: 0.12,
7353 };
7354 let eta_u = [0.11_f64, -0.07, 0.05, 0.02];
7357 let eta_v = [-0.09_f64, 0.13, -0.04, 0.03];
7358 let eta_t = [0.17_f64, 0.06, -0.10, 0.04]; let eta_uv = [0.02_f64, 0.01, -0.015, 0.005];
7361 let eta_ut = [-0.01_f64, 0.02, 0.007, -0.003];
7362 let eta_vt = [0.015_f64, -0.008, 0.01, 0.004];
7363 let eta_uvt = [0.003_f64, -0.002, 0.001, 0.0005];
7365
7366 let neg = |a: &[f64; 4]| a.map(|v| -v);
7367 let max_degree = 15usize;
7368
7369 let f_uv_at = |s: f64| -> f64 {
7376 let cell_s = DenestedCubicCell {
7377 c0: base.c0 + s * eta_t[0],
7378 c1: base.c1 + s * eta_t[1],
7379 c2: base.c2 + s * eta_t[2],
7380 c3: base.c3 + s * eta_t[3],
7381 ..base
7382 };
7383 let st = evaluate_cell_moments(cell_s, max_degree).unwrap();
7385 let neg_cell = DenestedCubicCell {
7386 c0: -cell_s.c0,
7387 c1: -cell_s.c1,
7388 c2: -cell_s.c2,
7389 c3: -cell_s.c3,
7390 ..cell_s
7391 };
7392 let u_s = [
7393 eta_u[0] + s * eta_ut[0],
7394 eta_u[1] + s * eta_ut[1],
7395 eta_u[2] + s * eta_ut[2],
7396 eta_u[3] + s * eta_ut[3],
7397 ];
7398 let v_s = [
7399 eta_v[0] + s * eta_vt[0],
7400 eta_v[1] + s * eta_vt[1],
7401 eta_v[2] + s * eta_vt[2],
7402 eta_v[3] + s * eta_vt[3],
7403 ];
7404 let uv_s = [
7405 eta_uv[0] + s * eta_uvt[0],
7406 eta_uv[1] + s * eta_uvt[1],
7407 eta_uv[2] + s * eta_uvt[2],
7408 eta_uv[3] + s * eta_uvt[3],
7409 ];
7410 cell_second_derivative_from_moments(
7411 neg_cell,
7412 &neg(&u_s),
7413 &neg(&v_s),
7414 &neg(&uv_s),
7415 &st.moments,
7416 )
7417 .unwrap()
7418 };
7419
7420 let h = 1e-5;
7421 let fd = (f_uv_at(h) - f_uv_at(-h)) / (2.0 * h);
7422
7423 let st0 = evaluate_cell_moments(base, max_degree).unwrap();
7426 let neg_cell0 = DenestedCubicCell {
7427 c0: -base.c0,
7428 c1: -base.c1,
7429 c2: -base.c2,
7430 c3: -base.c3,
7431 ..base
7432 };
7433 let analytic = cell_third_derivative_from_moments(
7434 neg_cell0,
7435 &neg(&eta_u),
7436 &neg(&eta_v),
7437 &neg(&eta_t),
7438 &neg(&eta_uv),
7439 &neg(&eta_ut),
7440 &neg(&eta_vt),
7441 &neg(&eta_uvt),
7442 &st0.moments,
7443 )
7444 .unwrap();
7445
7446 let denom = fd.abs().max(1e-3);
7447 let rel = (analytic - fd).abs() / denom;
7448 assert!(
7449 rel <= 1e-5,
7450 "third kernel vs FD-of-second mismatch: analytic={analytic:.12e} fd={fd:.12e} rel={rel:.3e}"
7451 );
7452 }
7453
7454 #[test]
7455 fn moving_shared_edge_second_integral_derivative_has_leibniz_jump_sign() {
7456 let edge0 = 0.2_f64;
7457 let edge_velocity = -0.37_f64;
7458
7459 let left_eta = [0.22_f64, -0.18, 0.09, 0.03];
7460 let right_eta = [-0.11_f64, 0.26, -0.04, 0.02];
7461 let left_r = [0.08_f64, -0.05, 0.03, 0.01];
7462 let left_s = [-0.06_f64, 0.04, 0.02, -0.015];
7463 let left_rs = [0.025_f64, -0.012, 0.006, 0.004];
7464 let right_r = [-0.03_f64, 0.07, -0.02, 0.012];
7465 let right_s = [0.05_f64, -0.025, 0.018, 0.007];
7466 let right_rs = [-0.018_f64, 0.014, -0.005, 0.003];
7467
7468 let integral_at = |shift: f64| -> f64 {
7469 let edge = edge0 + edge_velocity * shift;
7470 let left = DenestedCubicCell {
7471 left: -0.7,
7472 right: edge,
7473 c0: left_eta[0],
7474 c1: left_eta[1],
7475 c2: left_eta[2],
7476 c3: left_eta[3],
7477 };
7478 let right = DenestedCubicCell {
7479 left: edge,
7480 right: 1.1,
7481 c0: right_eta[0],
7482 c1: right_eta[1],
7483 c2: right_eta[2],
7484 c3: right_eta[3],
7485 };
7486 let left_state = evaluate_cell_moments(left, 12).expect("left moments");
7487 let right_state = evaluate_cell_moments(right, 12).expect("right moments");
7488 cell_second_derivative_from_moments(
7489 left,
7490 &left_r,
7491 &left_s,
7492 &left_rs,
7493 &left_state.moments,
7494 )
7495 .expect("left second")
7496 + cell_second_derivative_from_moments(
7497 right,
7498 &right_r,
7499 &right_s,
7500 &right_rs,
7501 &right_state.moments,
7502 )
7503 .expect("right second")
7504 };
7505
7506 let h = 1e-5;
7507 let fd = (integral_at(h) - integral_at(-h)) / (2.0 * h);
7508
7509 let left = DenestedCubicCell {
7510 left: -0.7,
7511 right: edge0,
7512 c0: left_eta[0],
7513 c1: left_eta[1],
7514 c2: left_eta[2],
7515 c3: left_eta[3],
7516 };
7517 let right = DenestedCubicCell {
7518 left: edge0,
7519 right: 1.1,
7520 c0: right_eta[0],
7521 c1: right_eta[1],
7522 c2: right_eta[2],
7523 c3: right_eta[3],
7524 };
7525 let f_left =
7526 cell_second_derivative_boundary_integrand(left, &left_r, &left_s, &left_rs, edge0);
7527 let f_right =
7528 cell_second_derivative_boundary_integrand(right, &right_r, &right_s, &right_rs, edge0);
7529 let analytic = edge_velocity * (f_left - f_right);
7530
7531 let denom = analytic.abs().max(1e-8);
7532 let rel = (fd - analytic).abs() / denom;
7533 assert!(
7534 rel <= 5e-8,
7535 "moving edge sign mismatch: fd={fd:.12e} analytic={analytic:.12e} rel={rel:.3e}"
7536 );
7537 }
7538
7539 #[test]
7540 fn moving_shared_edge_second_integral_mixed_derivative_has_full_leibniz_terms() {
7541 let edge0 = -0.15_f64;
7542 let edge_d1 = 0.31_f64;
7543 let edge_d2 = -0.27_f64;
7544 let edge_d12 = 0.19_f64;
7545
7546 let left_eta = [0.16_f64, -0.21, 0.07, -0.025];
7547 let right_eta = [-0.09_f64, 0.18, -0.055, 0.018];
7548 let left_r = [0.075_f64, -0.045, 0.018, 0.009];
7549 let left_s = [-0.052_f64, 0.033, 0.014, -0.011];
7550 let left_rs = [0.021_f64, -0.009, 0.005, 0.0025];
7551 let right_r = [-0.028_f64, 0.063, -0.017, 0.010];
7552 let right_s = [0.047_f64, -0.023, 0.016, 0.006];
7553 let right_rs = [-0.015_f64, 0.012, -0.004, 0.002];
7554
7555 let integral_at = |s1: f64, s2: f64| -> f64 {
7556 let edge = edge0 + edge_d1 * s1 + edge_d2 * s2 + edge_d12 * s1 * s2;
7557 let left = DenestedCubicCell {
7558 left: -0.8,
7559 right: edge,
7560 c0: left_eta[0],
7561 c1: left_eta[1],
7562 c2: left_eta[2],
7563 c3: left_eta[3],
7564 };
7565 let right = DenestedCubicCell {
7566 left: edge,
7567 right: 0.9,
7568 c0: right_eta[0],
7569 c1: right_eta[1],
7570 c2: right_eta[2],
7571 c3: right_eta[3],
7572 };
7573 let left_state = evaluate_cell_moments(left, 12).expect("left moments");
7574 let right_state = evaluate_cell_moments(right, 12).expect("right moments");
7575 cell_second_derivative_from_moments(
7576 left,
7577 &left_r,
7578 &left_s,
7579 &left_rs,
7580 &left_state.moments,
7581 )
7582 .expect("left second")
7583 + cell_second_derivative_from_moments(
7584 right,
7585 &right_r,
7586 &right_s,
7587 &right_rs,
7588 &right_state.moments,
7589 )
7590 .expect("right second")
7591 };
7592
7593 let h = 2e-4;
7594 let fd = (integral_at(h, h) - integral_at(h, -h) - integral_at(-h, h)
7595 + integral_at(-h, -h))
7596 / (4.0 * h * h);
7597
7598 let left = DenestedCubicCell {
7599 left: -0.8,
7600 right: edge0,
7601 c0: left_eta[0],
7602 c1: left_eta[1],
7603 c2: left_eta[2],
7604 c3: left_eta[3],
7605 };
7606 let right = DenestedCubicCell {
7607 left: edge0,
7608 right: 0.9,
7609 c0: right_eta[0],
7610 c1: right_eta[1],
7611 c2: right_eta[2],
7612 c3: right_eta[3],
7613 };
7614
7615 let boundary_z_derivative =
7616 |cell: DenestedCubicCell, r: &[f64], s: &[f64], rs: &[f64]| -> f64 {
7617 let eta = cell.eta(edge0);
7618 let eta_z = cell.c1 + 2.0 * cell.c2 * edge0 + 3.0 * cell.c3 * edge0 * edge0;
7619 let cr = poly_eval_at(r, edge0);
7620 let cs = poly_eval_at(s, edge0);
7621 let crs = poly_eval_at(rs, edge0);
7622 let cr_z = r.iter().enumerate().skip(1).fold(0.0, |acc, (k, val)| {
7623 acc + (k as f64) * val * edge0.powi(k as i32 - 1)
7624 });
7625 let cs_z = s.iter().enumerate().skip(1).fold(0.0, |acc, (k, val)| {
7626 acc + (k as f64) * val * edge0.powi(k as i32 - 1)
7627 });
7628 let crs_z = rs.iter().enumerate().skip(1).fold(0.0, |acc, (k, val)| {
7629 acc + (k as f64) * val * edge0.powi(k as i32 - 1)
7630 });
7631 let amp = crs - eta * cr * cs;
7632 let amp_z = crs_z - eta_z * cr * cs - eta * cr_z * cs - eta * cr * cs_z;
7633 let q_z = edge0 + eta * eta_z;
7634 (amp_z - amp * q_z) * (-cell.q(edge0)).exp() * INV_TWO_PI
7635 };
7636
7637 let f_left =
7638 cell_second_derivative_boundary_integrand(left, &left_r, &left_s, &left_rs, edge0);
7639 let f_right =
7640 cell_second_derivative_boundary_integrand(right, &right_r, &right_s, &right_rs, edge0);
7641 let fz_left = boundary_z_derivative(left, &left_r, &left_s, &left_rs);
7642 let fz_right = boundary_z_derivative(right, &right_r, &right_s, &right_rs);
7643 let analytic = edge_d12 * (f_left - f_right) + edge_d1 * edge_d2 * (fz_left - fz_right);
7644
7645 let denom = analytic.abs().max(1e-8);
7646 let rel = (fd - analytic).abs() / denom;
7647 assert!(
7648 rel <= 2e-7,
7649 "moving edge mixed term mismatch: fd={fd:.12e} analytic={analytic:.12e} rel={rel:.3e}"
7650 );
7651 }
7652
7653 #[test]
7678 fn third_order_self_flux_telescopes_but_third_integrand_jumps_at_c2_knot_1454() {
7679 let edge0 = 0.13_f64;
7680 let edge_velocity = -0.41_f64;
7681
7682 let left_eta = [0.18_f64, -0.12, 0.07, 0.04];
7686 let right_c3 = 0.04_f64 + 0.09; let l0 = left_eta[0];
7693 let l1 = left_eta[1];
7694 let l2 = left_eta[2];
7695 let l3 = left_eta[3];
7696 let e = edge0;
7697 let eta_val = l0 + l1 * e + l2 * e * e + l3 * e * e * e;
7698 let eta_d1 = l1 + 2.0 * l2 * e + 3.0 * l3 * e * e;
7699 let eta_d2 = 2.0 * l2 + 6.0 * l3 * e;
7700 let rc2 = (eta_d2 - 6.0 * right_c3 * e) / 2.0;
7701 let rc1 = eta_d1 - 2.0 * rc2 * e - 3.0 * right_c3 * e * e;
7702 let rc0 = eta_val - rc1 * e - rc2 * e * e - right_c3 * e * e * e;
7703 let right_eta = [rc0, rc1, rc2, right_c3];
7704
7705 let common_r = [0.06_f64, -0.04, 0.02, 0.0];
7711 let common_s = [-0.05_f64, 0.03, 0.015, 0.0];
7712 let common_t = [0.08_f64, 0.05, -0.03, 0.0];
7713 let common_rs = [0.02_f64, -0.01, 0.005, 0.0];
7714 let common_rt = [-0.012_f64, 0.008, 0.004, 0.0];
7715 let common_st = [0.015_f64, -0.006, 0.003, 0.0];
7716 let left_rst = [6.0 * l3, 0.0, 0.0, 0.0];
7718 let right_rst = [6.0 * right_c3, 0.0, 0.0, 0.0];
7719
7720 let max_degree = 15usize;
7721 let neg = |a: &[f64; 4]| a.map(|v| -v);
7722
7723 let integral_at = |shift: f64| -> f64 {
7728 let edge = edge0 + edge_velocity * shift;
7729 let left = DenestedCubicCell {
7730 left: -0.7,
7731 right: edge,
7732 c0: left_eta[0],
7733 c1: left_eta[1],
7734 c2: left_eta[2],
7735 c3: left_eta[3],
7736 };
7737 let right = DenestedCubicCell {
7738 left: edge,
7739 right: 1.0,
7740 c0: right_eta[0],
7741 c1: right_eta[1],
7742 c2: right_eta[2],
7743 c3: right_eta[3],
7744 };
7745 let lst = evaluate_cell_moments(left, max_degree).unwrap();
7746 let rst_m = evaluate_cell_moments(right, max_degree).unwrap();
7747 let neg_left = DenestedCubicCell {
7748 c0: -left.c0,
7749 c1: -left.c1,
7750 c2: -left.c2,
7751 c3: -left.c3,
7752 ..left
7753 };
7754 let neg_right = DenestedCubicCell {
7755 c0: -right.c0,
7756 c1: -right.c1,
7757 c2: -right.c2,
7758 c3: -right.c3,
7759 ..right
7760 };
7761 let li = cell_third_derivative_from_moments(
7762 neg_left,
7763 &neg(&common_r),
7764 &neg(&common_s),
7765 &neg(&common_t),
7766 &neg(&common_rs),
7767 &neg(&common_rt),
7768 &neg(&common_st),
7769 &neg(&left_rst),
7770 &lst.moments,
7771 )
7772 .unwrap();
7773 let ri = cell_third_derivative_from_moments(
7774 neg_right,
7775 &neg(&common_r),
7776 &neg(&common_s),
7777 &neg(&common_t),
7778 &neg(&common_rs),
7779 &neg(&common_rt),
7780 &neg(&common_st),
7781 &neg(&right_rst),
7782 &rst_m.moments,
7783 )
7784 .unwrap();
7785 li + ri
7786 };
7787
7788 let h = 1e-5;
7789 let fd = (integral_at(h) - integral_at(-h)) / (2.0 * h);
7790
7791 let neg_eta = |eta: &[f64; 4]| [-eta[0], -eta[1], -eta[2], -eta[3]];
7810 let left_eta_neg = neg_eta(&left_eta);
7811 let right_eta_neg = neg_eta(&right_eta);
7812 let left0 = DenestedCubicCell {
7813 left: -0.7,
7814 right: edge0,
7815 c0: left_eta_neg[0],
7816 c1: left_eta_neg[1],
7817 c2: left_eta_neg[2],
7818 c3: left_eta_neg[3],
7819 };
7820 let right0 = DenestedCubicCell {
7821 left: edge0,
7822 right: 1.0,
7823 c0: right_eta_neg[0],
7824 c1: right_eta_neg[1],
7825 c2: right_eta_neg[2],
7826 c3: right_eta_neg[3],
7827 };
7828 let f_left = cell_third_derivative_boundary_integrand(
7829 left0,
7830 &neg(&common_r),
7831 &neg(&common_s),
7832 &neg(&common_t),
7833 &neg(&common_rs),
7834 &neg(&common_rt),
7835 &neg(&common_st),
7836 &neg(&left_rst),
7837 edge0,
7838 );
7839 let f_right = cell_third_derivative_boundary_integrand(
7840 right0,
7841 &neg(&common_r),
7842 &neg(&common_s),
7843 &neg(&common_t),
7844 &neg(&common_rs),
7845 &neg(&common_rt),
7846 &neg(&common_st),
7847 &neg(&right_rst),
7848 edge0,
7849 );
7850
7851 let jump = f_left - f_right;
7855 assert!(
7856 jump.abs() > 1e-4,
7857 "third-derivative integrand must jump across the C² knot (α₃ discontinuity); \
7858 got jump={jump:.3e}"
7859 );
7860
7861 let analytic_flux = edge_velocity * jump;
7862 let denom = fd.abs().max(1e-6);
7863 let rel = (fd - analytic_flux).abs() / denom;
7864 assert!(
7865 rel <= 1e-5,
7866 "moving-edge third-derivative flux mismatch (#1454): fd={fd:.12e} \
7867 analytic_flux={analytic_flux:.12e} rel={rel:.3e}"
7868 );
7869
7870 let a_row = 0.21_f64;
7883 let b_row = 1.37_f64;
7884 let knot = a_row + b_row * edge0; let left_link = LocalSpanCubic {
7888 left: knot - 0.6,
7889 right: knot + 0.6,
7890 c0: 0.0,
7891 c1: 0.0,
7892 c2: 0.08,
7893 c3: -0.05,
7894 };
7895 let right_alpha3 = -0.05_f64 + 0.11; let right_left_coord = knot - 0.4;
7898 let lhs = 2.0 * left_link.c2 + 6.0 * left_link.c3 * (knot - left_link.left);
7899 let right_alpha2 = (lhs - 6.0 * right_alpha3 * (knot - right_left_coord)) / 2.0;
7900 let right_link = LocalSpanCubic {
7901 left: right_left_coord,
7902 right: right_left_coord + 0.8,
7903 c0: 0.0,
7904 c1: 0.0,
7905 c2: right_alpha2,
7906 c3: right_alpha3,
7907 };
7908 let (_, _, dc_dbb_left) = link_cubic_second_partials(left_link, a_row, b_row);
7909 let (_, _, dc_dbb_right) = link_cubic_second_partials(right_link, a_row, b_row);
7910 assert!(
7912 (dc_dbb_left[3] - dc_dbb_right[3]).abs() > 1e-3,
7913 "α₃ jump must make the raw dc_dbb coefficient arrays differ"
7914 );
7915 let c_bb_left = poly_eval_at(&dc_dbb_left, edge0);
7918 let c_bb_right = poly_eval_at(&dc_dbb_right, edge0);
7919 assert!(
7920 (c_bb_left - c_bb_right).abs() <= 1e-12,
7921 "second-derivative slope-slope integrand must be CONTINUOUS across the \
7922 C² knot (telescoping self-flux): left={c_bb_left:.15e} right={c_bb_right:.15e}"
7923 );
7924 }
7925}