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