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
2032pub fn cell_density_boundary_integrand(cell: DenestedCubicCell, g: &[f64], z: f64) -> f64 {
2046 poly_eval_at(g, z) * (-cell.q(z)).exp() * INV_TWO_PI
2047}
2048
2049#[inline]
2051fn poly_eval_at(coefficients: &[f64], z: f64) -> f64 {
2052 let mut acc = 0.0_f64;
2053 for &c in coefficients.iter().rev() {
2054 acc = acc.mul_add(z, c);
2055 }
2056 acc
2057}
2058
2059#[inline]
2060fn moment_dot_with_coefficients(
2061 coefficients: &[f64],
2062 moments: &[f64],
2063 label: &str,
2064) -> Result<f64, String> {
2065 if coefficients.len() > moments.len() {
2066 return Err(CubicCellKernelError::insufficient_moments(format!(
2067 "insufficient reduced moments for {label}: need {}, have {}",
2068 coefficients.len(),
2069 moments.len()
2070 ))
2071 .into());
2072 }
2073 Ok(moment_dot_with_coefficients_unchecked(
2074 coefficients,
2075 moments,
2076 ))
2077}
2078
2079#[inline]
2080fn moment_dot_with_coefficients_unchecked(coefficients: &[f64], moments: &[f64]) -> f64 {
2081 let mut acc = 0.0;
2082 for (idx, &coeff) in coefficients.iter().enumerate() {
2083 acc = coeff.mul_add(moments[idx], acc);
2084 }
2085 acc
2086}
2087
2088#[inline]
2098fn poly_conv_into(lhs: &[f64], rhs: &[f64], out: &mut [f64]) -> usize {
2099 if lhs.is_empty() || rhs.is_empty() {
2100 return 0;
2101 }
2102 let len = lhs.len() + rhs.len() - 1;
2103 assert!(out.len() >= len);
2104 for slot in out[..len].iter_mut() {
2105 *slot = 0.0;
2106 }
2107 for (i, &lv) in lhs.iter().enumerate() {
2108 for (j, &rv) in rhs.iter().enumerate() {
2109 out[i + j] = lv.mul_add(rv, out[i + j]);
2110 }
2111 }
2112 len
2113}
2114
2115#[inline]
2116fn require_moments_degree(
2117 required_degree: usize,
2118 moments: &[f64],
2119 label: &str,
2120) -> Result<(), String> {
2121 if required_degree >= moments.len() {
2122 return Err(CubicCellKernelError::insufficient_moments(format!(
2123 "insufficient reduced moments for {label}: need {}, have {}",
2124 required_degree + 1,
2125 moments.len()
2126 ))
2127 .into());
2128 }
2129 Ok::<(), _>(())
2130}
2131
2132#[inline]
2133fn require_scratch_capacity(
2134 required_len: usize,
2135 capacity: usize,
2136 label: &str,
2137) -> Result<(), String> {
2138 if required_len > capacity {
2139 return Err(CubicCellKernelError::insufficient_moments(format!(
2140 "{label} polynomial convolution scratch too small: need {required_len}, have {capacity}"
2141 ))
2142 .into());
2143 }
2144 Ok::<(), _>(())
2145}
2146
2147#[inline]
2148fn convolution_chain_len(lengths: &[usize]) -> usize {
2149 if lengths.is_empty() || lengths.contains(&0) {
2150 0
2151 } else {
2152 lengths.iter().sum::<usize>() - (lengths.len() - 1)
2153 }
2154}
2155
2156#[inline]
2157fn first_coefficients_degree(label: &str, coefficients: &[f64]) -> Result<usize, String> {
2158 coefficients
2159 .len()
2160 .checked_sub(1)
2161 .ok_or_else(|| format!("{label} first-derivative coefficients must be non-empty"))
2162}
2163
2164#[inline]
2165pub fn cell_third_derivative_from_moments(
2166 cell: DenestedCubicCell,
2167 first_coefficients_r: &[f64],
2168 first_coefficients_s: &[f64],
2169 first_coefficients_t: &[f64],
2170 second_coefficients_rs: &[f64],
2171 second_coefficients_rt: &[f64],
2172 second_coefficients_st: &[f64],
2173 third_coefficients_rst: &[f64],
2174 moments: &[f64],
2175) -> Result<f64, String> {
2176 let eta = [cell.c0, cell.c1, cell.c2, cell.c3];
2177 let r_degree = first_coefficients_degree("r", first_coefficients_r)?;
2178 let s_degree = first_coefficients_degree("s", first_coefficients_s)?;
2179 let t_degree = first_coefficients_degree("t", first_coefficients_t)?;
2180 let second_sum_degree = [
2181 second_coefficients_rs.len() + first_coefficients_t.len(),
2182 second_coefficients_rt.len() + first_coefficients_s.len(),
2183 second_coefficients_st.len() + first_coefficients_r.len(),
2184 ]
2185 .into_iter()
2186 .max()
2187 .unwrap_or(0)
2188 .saturating_sub(1);
2189 let triple_product_degree = r_degree + s_degree + t_degree;
2190 let needed = (third_coefficients_rst.len().saturating_sub(1))
2191 .max(3 + second_sum_degree)
2192 .max(6 + triple_product_degree);
2193 require_moments_degree(needed, moments, "third derivative")?;
2194
2195 let third_term = moment_dot_with_coefficients_unchecked(third_coefficients_rst, moments);
2196
2197 const SCRATCH: usize = 32;
2201 let max_linear_conv_len = [
2202 convolution_chain_len(&[
2203 eta.len(),
2204 second_coefficients_rs.len(),
2205 first_coefficients_t.len(),
2206 ]),
2207 convolution_chain_len(&[
2208 eta.len(),
2209 second_coefficients_rt.len(),
2210 first_coefficients_s.len(),
2211 ]),
2212 convolution_chain_len(&[
2213 eta.len(),
2214 second_coefficients_st.len(),
2215 first_coefficients_r.len(),
2216 ]),
2217 ]
2218 .into_iter()
2219 .max()
2220 .unwrap_or(0);
2221 let max_cubic_conv_len = convolution_chain_len(&[
2222 7,
2223 first_coefficients_r.len(),
2224 first_coefficients_s.len(),
2225 first_coefficients_t.len(),
2226 ]);
2227 require_scratch_capacity(
2228 max_linear_conv_len.max(max_cubic_conv_len),
2229 SCRATCH,
2230 "third derivative",
2231 )?;
2232 let mut buf_a = [0.0_f64; SCRATCH];
2233 let mut buf_b = [0.0_f64; SCRATCH];
2234
2235 let mut eta_second_term = 0.0;
2238 let conv_dot = |first: &[f64],
2239 second: &[f64],
2240 buf_a: &mut [f64; SCRATCH],
2241 buf_b: &mut [f64; SCRATCH]|
2242 -> f64 {
2243 let m = poly_conv_into(first, second, buf_a);
2244 let n = poly_conv_into(&eta, &buf_a[..m], buf_b);
2245 let mut acc = 0.0;
2246 for k in 0..n {
2247 acc = buf_b[k].mul_add(moments[k], acc);
2248 }
2249 acc
2250 };
2251 eta_second_term += conv_dot(
2252 second_coefficients_rs,
2253 first_coefficients_t,
2254 &mut buf_a,
2255 &mut buf_b,
2256 );
2257 eta_second_term += conv_dot(
2258 second_coefficients_rt,
2259 first_coefficients_s,
2260 &mut buf_a,
2261 &mut buf_b,
2262 );
2263 eta_second_term += conv_dot(
2264 second_coefficients_st,
2265 first_coefficients_r,
2266 &mut buf_a,
2267 &mut buf_b,
2268 );
2269
2270 let mut eta_sq_minus_one = [0.0_f64; 7];
2273 for (i, &eta_i) in eta.iter().enumerate() {
2274 for (j, &eta_j) in eta.iter().enumerate() {
2275 eta_sq_minus_one[i + j] = eta_i.mul_add(eta_j, eta_sq_minus_one[i + j]);
2276 }
2277 }
2278 eta_sq_minus_one[0] -= 1.0;
2279
2280 let rs_len = poly_conv_into(first_coefficients_r, first_coefficients_s, &mut buf_a);
2281 let rst_len = poly_conv_into(&buf_a[..rs_len], first_coefficients_t, &mut buf_b);
2282 let final_len = poly_conv_into(&eta_sq_minus_one, &buf_b[..rst_len], &mut buf_a);
2284 let mut cubic_coeff_term = 0.0;
2285 for k in 0..final_len {
2286 cubic_coeff_term = buf_a[k].mul_add(moments[k], cubic_coeff_term);
2287 }
2288
2289 Ok((third_term - eta_second_term + cubic_coeff_term) * INV_TWO_PI)
2290}
2291
2292#[inline]
2293pub fn cell_fourth_derivative_from_moments(
2294 cell: DenestedCubicCell,
2295 first_coefficients_r: &[f64],
2296 first_coefficients_s: &[f64],
2297 first_coefficients_t: &[f64],
2298 first_coefficients_u: &[f64],
2299 second_coefficients_rs: &[f64],
2300 second_coefficients_rt: &[f64],
2301 second_coefficients_ru: &[f64],
2302 second_coefficients_st: &[f64],
2303 second_coefficients_su: &[f64],
2304 second_coefficients_tu: &[f64],
2305 third_coefficients_rst: &[f64],
2306 third_coefficients_rsu: &[f64],
2307 third_coefficients_rtu: &[f64],
2308 third_coefficients_stu: &[f64],
2309 fourth_coefficients_rstu: &[f64],
2310 moments: &[f64],
2311) -> Result<f64, String> {
2312 let eta = [cell.c0, cell.c1, cell.c2, cell.c3];
2313 let r_degree = first_coefficients_degree("r", first_coefficients_r)?;
2314 let s_degree = first_coefficients_degree("s", first_coefficients_s)?;
2315 let t_degree = first_coefficients_degree("t", first_coefficients_t)?;
2316 let u_degree = first_coefficients_degree("u", first_coefficients_u)?;
2317 let linear_sum_degree = [
2318 third_coefficients_rst.len() + first_coefficients_u.len(),
2319 third_coefficients_rsu.len() + first_coefficients_t.len(),
2320 third_coefficients_rtu.len() + first_coefficients_s.len(),
2321 third_coefficients_stu.len() + first_coefficients_r.len(),
2322 second_coefficients_rs.len() + second_coefficients_tu.len(),
2323 second_coefficients_rt.len() + second_coefficients_su.len(),
2324 second_coefficients_ru.len() + second_coefficients_st.len(),
2325 ]
2326 .into_iter()
2327 .max()
2328 .unwrap_or(0)
2329 .saturating_sub(1);
2330 let quad_sum_degree = [
2331 second_coefficients_rs.len() + first_coefficients_t.len() + first_coefficients_u.len(),
2332 second_coefficients_rt.len() + first_coefficients_s.len() + first_coefficients_u.len(),
2333 second_coefficients_ru.len() + first_coefficients_s.len() + first_coefficients_t.len(),
2334 second_coefficients_st.len() + first_coefficients_r.len() + first_coefficients_u.len(),
2335 second_coefficients_su.len() + first_coefficients_r.len() + first_coefficients_t.len(),
2336 second_coefficients_tu.len() + first_coefficients_r.len() + first_coefficients_s.len(),
2337 ]
2338 .into_iter()
2339 .max()
2340 .unwrap_or(0)
2341 .saturating_sub(2);
2342 let quartic_product_degree = r_degree + s_degree + t_degree + u_degree;
2343 let needed = (fourth_coefficients_rstu.len().saturating_sub(1))
2344 .max(3 + linear_sum_degree)
2345 .max(6 + quad_sum_degree)
2346 .max(9 + quartic_product_degree);
2347 require_moments_degree(needed, moments, "fourth derivative")?;
2348
2349 let fourth_term = moment_dot_with_coefficients_unchecked(fourth_coefficients_rstu, moments);
2350
2351 const SCRATCH: usize = 32;
2355 let max_linear_conv_len = [
2356 convolution_chain_len(&[
2357 eta.len(),
2358 third_coefficients_rst.len(),
2359 first_coefficients_u.len(),
2360 ]),
2361 convolution_chain_len(&[
2362 eta.len(),
2363 third_coefficients_rsu.len(),
2364 first_coefficients_t.len(),
2365 ]),
2366 convolution_chain_len(&[
2367 eta.len(),
2368 third_coefficients_rtu.len(),
2369 first_coefficients_s.len(),
2370 ]),
2371 convolution_chain_len(&[
2372 eta.len(),
2373 third_coefficients_stu.len(),
2374 first_coefficients_r.len(),
2375 ]),
2376 convolution_chain_len(&[
2377 eta.len(),
2378 second_coefficients_rs.len(),
2379 second_coefficients_tu.len(),
2380 ]),
2381 convolution_chain_len(&[
2382 eta.len(),
2383 second_coefficients_rt.len(),
2384 second_coefficients_su.len(),
2385 ]),
2386 convolution_chain_len(&[
2387 eta.len(),
2388 second_coefficients_ru.len(),
2389 second_coefficients_st.len(),
2390 ]),
2391 ]
2392 .into_iter()
2393 .max()
2394 .unwrap_or(0);
2395 let max_quad_conv_len = [
2396 convolution_chain_len(&[
2397 7,
2398 second_coefficients_rs.len(),
2399 first_coefficients_t.len(),
2400 first_coefficients_u.len(),
2401 ]),
2402 convolution_chain_len(&[
2403 7,
2404 second_coefficients_rt.len(),
2405 first_coefficients_s.len(),
2406 first_coefficients_u.len(),
2407 ]),
2408 convolution_chain_len(&[
2409 7,
2410 second_coefficients_ru.len(),
2411 first_coefficients_s.len(),
2412 first_coefficients_t.len(),
2413 ]),
2414 convolution_chain_len(&[
2415 7,
2416 second_coefficients_st.len(),
2417 first_coefficients_r.len(),
2418 first_coefficients_u.len(),
2419 ]),
2420 convolution_chain_len(&[
2421 7,
2422 second_coefficients_su.len(),
2423 first_coefficients_r.len(),
2424 first_coefficients_t.len(),
2425 ]),
2426 convolution_chain_len(&[
2427 7,
2428 second_coefficients_tu.len(),
2429 first_coefficients_r.len(),
2430 first_coefficients_s.len(),
2431 ]),
2432 ]
2433 .into_iter()
2434 .max()
2435 .unwrap_or(0);
2436 let max_quartic_conv_len = convolution_chain_len(&[
2437 10,
2438 first_coefficients_r.len(),
2439 first_coefficients_s.len(),
2440 first_coefficients_t.len(),
2441 first_coefficients_u.len(),
2442 ]);
2443 require_scratch_capacity(
2444 max_linear_conv_len
2445 .max(max_quad_conv_len)
2446 .max(max_quartic_conv_len),
2447 SCRATCH,
2448 "fourth derivative",
2449 )?;
2450 let mut buf_a = [0.0_f64; SCRATCH];
2451 let mut buf_b = [0.0_f64; SCRATCH];
2452
2453 let conv_eta_dot = |first: &[f64],
2457 second: &[f64],
2458 buf_a: &mut [f64; SCRATCH],
2459 buf_b: &mut [f64; SCRATCH]|
2460 -> f64 {
2461 let m = poly_conv_into(first, second, buf_a);
2462 let n = poly_conv_into(&eta, &buf_a[..m], buf_b);
2463 let mut acc = 0.0;
2464 for k in 0..n {
2465 acc = buf_b[k].mul_add(moments[k], acc);
2466 }
2467 acc
2468 };
2469 let mut eta_linear_term = 0.0;
2470 eta_linear_term += conv_eta_dot(
2471 third_coefficients_rst,
2472 first_coefficients_u,
2473 &mut buf_a,
2474 &mut buf_b,
2475 );
2476 eta_linear_term += conv_eta_dot(
2477 third_coefficients_rsu,
2478 first_coefficients_t,
2479 &mut buf_a,
2480 &mut buf_b,
2481 );
2482 eta_linear_term += conv_eta_dot(
2483 third_coefficients_rtu,
2484 first_coefficients_s,
2485 &mut buf_a,
2486 &mut buf_b,
2487 );
2488 eta_linear_term += conv_eta_dot(
2489 third_coefficients_stu,
2490 first_coefficients_r,
2491 &mut buf_a,
2492 &mut buf_b,
2493 );
2494 eta_linear_term += conv_eta_dot(
2495 second_coefficients_rs,
2496 second_coefficients_tu,
2497 &mut buf_a,
2498 &mut buf_b,
2499 );
2500 eta_linear_term += conv_eta_dot(
2501 second_coefficients_rt,
2502 second_coefficients_su,
2503 &mut buf_a,
2504 &mut buf_b,
2505 );
2506 eta_linear_term += conv_eta_dot(
2507 second_coefficients_ru,
2508 second_coefficients_st,
2509 &mut buf_a,
2510 &mut buf_b,
2511 );
2512
2513 let mut eta_sq_minus_one = [0.0_f64; 7];
2514 for (i, &eta_i) in eta.iter().enumerate() {
2515 for (j, &eta_j) in eta.iter().enumerate() {
2516 eta_sq_minus_one[i + j] = eta_i.mul_add(eta_j, eta_sq_minus_one[i + j]);
2517 }
2518 }
2519 eta_sq_minus_one[0] -= 1.0;
2520
2521 let mut buf_c = [0.0_f64; SCRATCH];
2524 let conv_weighted_triple_dot = |weight: &[f64],
2525 a: &[f64],
2526 b: &[f64],
2527 c: &[f64],
2528 buf_a: &mut [f64; SCRATCH],
2529 buf_b: &mut [f64; SCRATCH],
2530 buf_c: &mut [f64; SCRATCH]|
2531 -> f64 {
2532 let ab_len = poly_conv_into(a, b, buf_a);
2533 let abc_len = poly_conv_into(&buf_a[..ab_len], c, buf_b);
2534 let final_len = poly_conv_into(weight, &buf_b[..abc_len], buf_c);
2535 let mut acc = 0.0;
2536 for k in 0..final_len {
2537 acc = buf_c[k].mul_add(moments[k], acc);
2538 }
2539 acc
2540 };
2541 let mut quad_coeff_term = 0.0;
2542 quad_coeff_term += conv_weighted_triple_dot(
2543 &eta_sq_minus_one,
2544 second_coefficients_rs,
2545 first_coefficients_t,
2546 first_coefficients_u,
2547 &mut buf_a,
2548 &mut buf_b,
2549 &mut buf_c,
2550 );
2551 quad_coeff_term += conv_weighted_triple_dot(
2552 &eta_sq_minus_one,
2553 second_coefficients_rt,
2554 first_coefficients_s,
2555 first_coefficients_u,
2556 &mut buf_a,
2557 &mut buf_b,
2558 &mut buf_c,
2559 );
2560 quad_coeff_term += conv_weighted_triple_dot(
2561 &eta_sq_minus_one,
2562 second_coefficients_ru,
2563 first_coefficients_s,
2564 first_coefficients_t,
2565 &mut buf_a,
2566 &mut buf_b,
2567 &mut buf_c,
2568 );
2569 quad_coeff_term += conv_weighted_triple_dot(
2570 &eta_sq_minus_one,
2571 second_coefficients_st,
2572 first_coefficients_r,
2573 first_coefficients_u,
2574 &mut buf_a,
2575 &mut buf_b,
2576 &mut buf_c,
2577 );
2578 quad_coeff_term += conv_weighted_triple_dot(
2579 &eta_sq_minus_one,
2580 second_coefficients_su,
2581 first_coefficients_r,
2582 first_coefficients_t,
2583 &mut buf_a,
2584 &mut buf_b,
2585 &mut buf_c,
2586 );
2587 quad_coeff_term += conv_weighted_triple_dot(
2588 &eta_sq_minus_one,
2589 second_coefficients_tu,
2590 first_coefficients_r,
2591 first_coefficients_s,
2592 &mut buf_a,
2593 &mut buf_b,
2594 &mut buf_c,
2595 );
2596
2597 let mut eta_sq = [0.0_f64; 7];
2600 for (i, &eta_i) in eta.iter().enumerate() {
2601 for (j, &eta_j) in eta.iter().enumerate() {
2602 eta_sq[i + j] = eta_i.mul_add(eta_j, eta_sq[i + j]);
2603 }
2604 }
2605 let mut cubic_weight = [0.0_f64; 10];
2606 for (i, &eta_sq_i) in eta_sq.iter().enumerate() {
2607 for (j, &eta_j) in eta.iter().enumerate() {
2608 cubic_weight[i + j] = (-eta_sq_i).mul_add(eta_j, cubic_weight[i + j]);
2609 }
2610 }
2611 for (idx, &eta_coeff) in eta.iter().enumerate() {
2612 cubic_weight[idx] += 3.0 * eta_coeff;
2613 }
2614
2615 let rs_len = poly_conv_into(first_coefficients_r, first_coefficients_s, &mut buf_a);
2620 let rst_len = poly_conv_into(&buf_a[..rs_len], first_coefficients_t, &mut buf_b);
2621 let rstu_len = poly_conv_into(&buf_b[..rst_len], first_coefficients_u, &mut buf_a);
2622 let final_len = poly_conv_into(&cubic_weight, &buf_a[..rstu_len], &mut buf_b);
2623 let mut quartic_coeff_term = 0.0;
2624 for k in 0..final_len {
2625 quartic_coeff_term = buf_b[k].mul_add(moments[k], quartic_coeff_term);
2626 }
2627
2628 Ok((fourth_term - eta_linear_term + quad_coeff_term + quartic_coeff_term) * INV_TWO_PI)
2629}
2630
2631#[inline]
2632pub fn global_cubic_from_local(span: LocalSpanCubic) -> (f64, f64, f64, f64) {
2633 let left = span.left;
2634 let q0 = span.c0 - span.c1 * left + span.c2 * left * left - span.c3 * left * left * left;
2635 let q1 = span.c1 - 2.0 * span.c2 * left + 3.0 * span.c3 * left * left;
2636 let q2 = span.c2 - 3.0 * span.c3 * left;
2637 let q3 = span.c3;
2638 (q0, q1, q2, q3)
2639}
2640
2641#[inline]
2665pub fn transformed_link_cubic(link_span: LocalSpanCubic, a: f64, b: f64) -> (f64, f64, f64, f64) {
2666 let shift = a - link_span.left;
2667 let d0 = link_span.c0
2668 + link_span.c1 * shift
2669 + link_span.c2 * shift * shift
2670 + link_span.c3 * shift * shift * shift;
2671 let d1 = b * (link_span.c1 + 2.0 * link_span.c2 * shift + 3.0 * link_span.c3 * shift * shift);
2672 let d2 = b * b * (link_span.c2 + 3.0 * link_span.c3 * shift);
2673 let d3 = link_span.c3 * b * b * b;
2674 (d0, d1, d2, d3)
2675}
2676
2677#[inline]
2678pub fn denested_cell_coefficients(
2679 score_span: LocalSpanCubic,
2680 link_span: LocalSpanCubic,
2681 a: f64,
2682 b: f64,
2683) -> [f64; 4] {
2684 let (h0, h1, h2, h3) = global_cubic_from_local(score_span);
2685 let (d0, d1, d2, d3) = transformed_link_cubic(link_span, a, b);
2686 [a + b * h0 + d0, b + b * h1 + d1, b * h2 + d2, b * h3 + d3]
2687}
2688
2689#[inline]
2690pub fn denested_cell_coefficient_partials(
2691 score_span: LocalSpanCubic,
2692 link_span: LocalSpanCubic,
2693 a: f64,
2694 b: f64,
2695) -> ([f64; 4], [f64; 4]) {
2696 let (h0, h1, h2, h3) = global_cubic_from_local(score_span);
2697 let shift = a - link_span.left;
2698 let alpha1 = link_span.c1;
2699 let alpha2 = link_span.c2;
2700 let alpha3 = link_span.c3;
2701 let dc_da = [
2702 1.0 + alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2703 b * (2.0 * alpha2 + 6.0 * alpha3 * shift),
2704 3.0 * alpha3 * b * b,
2705 0.0,
2706 ];
2707 let dc_db = [
2708 h0,
2709 1.0 + h1 + alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2710 h2 + 2.0 * b * (alpha2 + 3.0 * alpha3 * shift),
2711 h3 + 3.0 * alpha3 * b * b,
2712 ];
2713 (dc_da, dc_db)
2714}
2715
2716#[inline]
2717fn link_cubic_second_partials(
2718 link_span: LocalSpanCubic,
2719 a: f64,
2720 b: f64,
2721) -> ([f64; 4], [f64; 4], [f64; 4]) {
2722 let shift = a - link_span.left;
2723 let alpha2 = link_span.c2;
2724 let alpha3 = link_span.c3;
2725 let dc_daa = [
2726 2.0 * alpha2 + 6.0 * alpha3 * shift,
2727 6.0 * alpha3 * b,
2728 0.0,
2729 0.0,
2730 ];
2731 let dc_dab = [
2732 0.0,
2733 2.0 * alpha2 + 6.0 * alpha3 * shift,
2734 6.0 * alpha3 * b,
2735 0.0,
2736 ];
2737 let dc_dbb = [
2738 0.0,
2739 0.0,
2740 2.0 * (alpha2 + 3.0 * alpha3 * shift),
2741 6.0 * alpha3 * b,
2742 ];
2743 (dc_daa, dc_dab, dc_dbb)
2744}
2745
2746#[inline]
2747pub fn denested_cell_second_partials(
2748 score_span: LocalSpanCubic,
2749 link_span: LocalSpanCubic,
2750 a: f64,
2751 b: f64,
2752) -> ([f64; 4], [f64; 4], [f64; 4]) {
2753 let score_left = score_span.left;
2754 if !score_left.is_finite() {
2755 return ([f64::NAN; 4], [f64::NAN; 4], [f64::NAN; 4]);
2756 }
2757 link_cubic_second_partials(link_span, a, b)
2758}
2759
2760#[inline]
2761fn link_cubic_third_partials(
2762 link_span: LocalSpanCubic,
2763) -> ([f64; 4], [f64; 4], [f64; 4], [f64; 4]) {
2764 let alpha3 = link_span.c3;
2765 (
2766 [6.0 * alpha3, 0.0, 0.0, 0.0],
2767 [0.0, 6.0 * alpha3, 0.0, 0.0],
2768 [0.0, 0.0, 6.0 * alpha3, 0.0],
2769 [0.0, 0.0, 0.0, 6.0 * alpha3],
2770 )
2771}
2772
2773#[inline]
2774pub fn denested_cell_third_partials(
2775 link_span: LocalSpanCubic,
2776) -> ([f64; 4], [f64; 4], [f64; 4], [f64; 4]) {
2777 link_cubic_third_partials(link_span)
2778}
2779
2780#[inline]
2781pub fn score_basis_cell_coefficients(score_basis_span: LocalSpanCubic, b: f64) -> [f64; 4] {
2782 let (h0, h1, h2, h3) = global_cubic_from_local(score_basis_span);
2783 [b * h0, b * h1, b * h2, b * h3]
2784}
2785
2786#[inline]
2787pub fn link_basis_cell_coefficients(link_basis_span: LocalSpanCubic, a: f64, b: f64) -> [f64; 4] {
2788 let (d0, d1, d2, d3) = transformed_link_cubic(link_basis_span, a, b);
2789 [d0, d1, d2, d3]
2790}
2791
2792#[inline]
2793pub fn link_basis_cell_coefficient_partials(
2794 link_basis_span: LocalSpanCubic,
2795 a: f64,
2796 b: f64,
2797) -> ([f64; 4], [f64; 4]) {
2798 let shift = a - link_basis_span.left;
2799 let alpha1 = link_basis_span.c1;
2800 let alpha2 = link_basis_span.c2;
2801 let alpha3 = link_basis_span.c3;
2802 let dc_da = [
2803 alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2804 b * (2.0 * alpha2 + 6.0 * alpha3 * shift),
2805 3.0 * alpha3 * b * b,
2806 0.0,
2807 ];
2808 let dc_db = [
2809 0.0,
2810 alpha1 + 2.0 * alpha2 * shift + 3.0 * alpha3 * shift * shift,
2811 2.0 * b * (alpha2 + 3.0 * alpha3 * shift),
2812 3.0 * alpha3 * b * b,
2813 ];
2814 (dc_da, dc_db)
2815}
2816
2817#[inline]
2818pub fn link_basis_cell_second_partials(
2819 link_basis_span: LocalSpanCubic,
2820 a: f64,
2821 b: f64,
2822) -> ([f64; 4], [f64; 4], [f64; 4]) {
2823 link_cubic_second_partials(link_basis_span, a, b)
2824}
2825
2826#[inline]
2827pub fn link_basis_cell_third_partials(
2828 link_basis_span: LocalSpanCubic,
2829) -> ([f64; 4], [f64; 4], [f64; 4], [f64; 4]) {
2830 link_cubic_third_partials(link_basis_span)
2831}
2832
2833pub fn build_denested_partition_cells<FS, FL>(
2834 a: f64,
2835 b: f64,
2836 score_breaks: &[f64],
2837 link_breaks: &[f64],
2838 score_span_at: FS,
2839 link_span_at: FL,
2840) -> Result<Vec<DenestedPartitionCell>, String>
2841where
2842 FS: FnMut(f64) -> Result<LocalSpanCubic, String>,
2843 FL: FnMut(f64) -> Result<LocalSpanCubic, String>,
2844{
2845 build_denested_partition_cells_with_tails(
2846 a,
2847 b,
2848 score_breaks,
2849 link_breaks,
2850 score_span_at,
2851 link_span_at,
2852 )
2853}
2854
2855pub fn build_denested_partition_cells_with_tails<FS, FL>(
2864 a: f64,
2865 b: f64,
2866 score_breaks: &[f64],
2867 link_breaks: &[f64],
2868 mut score_span_at: FS,
2869 mut link_span_at: FL,
2870) -> Result<Vec<DenestedPartitionCell>, String>
2871where
2872 FS: FnMut(f64) -> Result<LocalSpanCubic, String>,
2873 FL: FnMut(f64) -> Result<LocalSpanCubic, String>,
2874{
2875 let mut split_points: Vec<(f64, PartitionEdge)> = score_breaks
2880 .iter()
2881 .map(|&sigma| (sigma, PartitionEdge::Fixed(sigma)))
2882 .collect();
2883 if b.abs() > 1e-12 {
2884 for &tau in link_breaks {
2885 let z = (tau - a) / b;
2886 if z.is_finite() {
2887 split_points.push((z, PartitionEdge::Crossing { tau }));
2888 }
2889 }
2890 }
2891 dedup_sorted_tagged_breakpoints(&mut split_points);
2892
2893 let mut out = Vec::new();
2894
2895 if split_points.is_empty() {
2896 let score_span = score_span_at(0.0)?;
2897 let link_span = link_span_at(a)?;
2898 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
2899 return Ok(vec![DenestedPartitionCell {
2900 cell: DenestedCubicCell {
2901 left: f64::NEG_INFINITY,
2902 right: f64::INFINITY,
2903 c0: coeffs[0],
2904 c1: coeffs[1],
2905 c2: 0.0,
2906 c3: 0.0,
2907 },
2908 score_span,
2909 link_span,
2910 left_edge: PartitionEdge::Fixed(f64::NEG_INFINITY),
2911 right_edge: PartitionEdge::Fixed(f64::INFINITY),
2912 }]);
2913 }
2914
2915 let (leftmost, leftmost_edge) = split_points[0];
2917 let left_probe = interval_probe_point(f64::NEG_INFINITY, leftmost)?;
2920 let left_score_span = score_span_at(left_probe)?;
2921 let left_link_span = link_span_at(a + b * left_probe)?;
2922 let left_coeffs = denested_cell_coefficients(left_score_span, left_link_span, a, b);
2923 if left_coeffs[2] != 0.0 || left_coeffs[3] != 0.0 {
2924 return Err(CubicCellKernelError::invalid_cell_shape(format!(
2925 "left tail cell must be affine (deviations constant outside support), \
2926 got c2={:.3e}, c3={:.3e}",
2927 left_coeffs[2], left_coeffs[3]
2928 ))
2929 .into());
2930 }
2931 out.push(DenestedPartitionCell {
2932 cell: DenestedCubicCell {
2933 left: f64::NEG_INFINITY,
2934 right: leftmost,
2935 c0: left_coeffs[0],
2936 c1: left_coeffs[1],
2937 c2: 0.0,
2938 c3: 0.0,
2939 },
2940 score_span: left_score_span,
2941 link_span: left_link_span,
2942 left_edge: PartitionEdge::Fixed(f64::NEG_INFINITY),
2943 right_edge: leftmost_edge,
2944 });
2945
2946 for window in split_points.windows(2) {
2948 let (left, left_edge) = window[0];
2949 let (right, right_edge) = window[1];
2950 if !left.is_finite() || !right.is_finite() || right - left <= 1e-12 {
2951 continue;
2952 }
2953 let mid = interval_probe_point(left, right)?;
2954 let score_span = score_span_at(mid)?;
2955 let link_span = link_span_at(a + b * mid)?;
2956 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
2957 out.push(DenestedPartitionCell {
2958 cell: DenestedCubicCell {
2959 left,
2960 right,
2961 c0: coeffs[0],
2962 c1: coeffs[1],
2963 c2: coeffs[2],
2964 c3: coeffs[3],
2965 },
2966 score_span,
2967 link_span,
2968 left_edge,
2969 right_edge,
2970 });
2971 }
2972
2973 let (rightmost, rightmost_edge) = *split_points.last().unwrap();
2975 let right_probe = interval_probe_point(rightmost, f64::INFINITY)?;
2976 let right_score_span = score_span_at(right_probe)?;
2977 let right_link_span = link_span_at(a + b * right_probe)?;
2978 let right_coeffs = denested_cell_coefficients(right_score_span, right_link_span, a, b);
2979 if right_coeffs[2] != 0.0 || right_coeffs[3] != 0.0 {
2980 return Err(CubicCellKernelError::invalid_cell_shape(format!(
2981 "right tail cell must be affine (deviations constant outside support), \
2982 got c2={:.3e}, c3={:.3e}",
2983 right_coeffs[2], right_coeffs[3]
2984 ))
2985 .into());
2986 }
2987 out.push(DenestedPartitionCell {
2988 cell: DenestedCubicCell {
2989 left: rightmost,
2990 right: f64::INFINITY,
2991 c0: right_coeffs[0],
2992 c1: right_coeffs[1],
2993 c2: 0.0,
2994 c3: 0.0,
2995 },
2996 score_span: right_score_span,
2997 link_span: right_link_span,
2998 left_edge: rightmost_edge,
2999 right_edge: PartitionEdge::Fixed(f64::INFINITY),
3000 });
3001
3002 Ok(out)
3003}
3004
3005#[inline]
3006pub fn branch_cell(cell: DenestedCubicCell) -> Result<ExactCellBranch, String> {
3007 validate_cell_inputs(cell)?;
3008 if !cell.left.is_finite() || !cell.right.is_finite() {
3009 if cell.c2 == 0.0 && cell.c3 == 0.0 {
3010 return Ok(ExactCellBranch::Affine);
3011 }
3012 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3013 "non-affine cells require finite bounds, got [{}, {}] with c2={:.6e}, c3={:.6e}",
3014 cell.left, cell.right, cell.c2, cell.c3
3015 ))
3016 .into());
3017 }
3018 if cell.right <= cell.left {
3019 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3020 "finite cell must have left < right, got [{}, {}]",
3021 cell.left, cell.right
3022 ))
3023 .into());
3024 }
3025 if cell.c2 == 0.0 && cell.c3 == 0.0 {
3028 Ok(ExactCellBranch::Affine)
3029 } else if cell.c3 == 0.0 {
3030 Ok(ExactCellBranch::Quartic)
3031 } else {
3032 Ok(ExactCellBranch::Sextic)
3033 }
3034}
3035
3036#[inline]
3037fn validate_bvn_args(h: f64, k: f64, rho: f64) -> Result<(), String> {
3038 if !h.is_finite() && !h.is_infinite() {
3039 return Err(CubicCellKernelError::bivariate_normal_domain(
3040 "bivariate normal cdf requires finite or infinite h",
3041 )
3042 .into());
3043 }
3044 if !k.is_finite() && !k.is_infinite() {
3045 return Err(CubicCellKernelError::bivariate_normal_domain(
3046 "bivariate normal cdf requires finite or infinite k",
3047 )
3048 .into());
3049 }
3050 if !rho.is_finite() {
3051 return Err(CubicCellKernelError::bivariate_normal_domain(format!(
3052 "bivariate normal cdf requires finite correlation, got {rho}"
3053 ))
3054 .into());
3055 }
3056 Ok::<(), _>(())
3057}
3058
3059#[inline]
3060fn bvn_gl_sum(h: f64, k: f64, rho_clamped: f64, asr: f64) -> f64 {
3061 if rho_clamped == 0.0 {
3068 return 0.0;
3069 }
3070 let hs = 0.5 * (h * h + k * k);
3071 let hk = h * k;
3072 let half_asr = 0.5 * asr;
3073 let (sin_mid, cos_mid) = half_asr.sin_cos();
3074 let mut sum = 0.0;
3075 for i in 0..10 {
3076 let node = GL20_NODES[i].abs();
3077 let weight = GL20_WEIGHTS[i];
3078 let (sin_delta, cos_delta) = (half_asr * node).sin_cos();
3079
3080 let sn_lo = sin_mid * cos_delta - cos_mid * sin_delta;
3081 let one_minus_lo = 1.0 - sn_lo * sn_lo;
3082 let expo_lo = ((sn_lo * hk) - hs) / one_minus_lo;
3083
3084 let sn_hi = sin_mid * cos_delta + cos_mid * sin_delta;
3085 let one_minus_hi = 1.0 - sn_hi * sn_hi;
3086 let expo_hi = ((sn_hi * hk) - hs) / one_minus_hi;
3087
3088 sum += weight * (expo_lo.exp() + expo_hi.exp());
3089 }
3090 sum
3091}
3092
3093pub fn bivariate_normal_cdf(h: f64, k: f64, rho: f64) -> Result<f64, String> {
3094 validate_bvn_args(h, k, rho)?;
3095 if h == f64::NEG_INFINITY || k == f64::NEG_INFINITY {
3096 return Ok(0.0);
3097 }
3098 if h == f64::INFINITY {
3099 return Ok(normal_cdf(k));
3100 }
3101 if k == f64::INFINITY {
3102 return Ok(normal_cdf(h));
3103 }
3104
3105 let rho_clamped = rho.clamp(-1.0, 1.0);
3106 if rho_clamped >= 1.0 - 1e-12 {
3107 return Ok(normal_cdf(h.min(k)));
3108 }
3109 if rho_clamped <= -1.0 + 1e-12 {
3110 return Ok((normal_cdf(h) - normal_cdf(-k)).clamp(0.0, 1.0));
3111 }
3112 if rho_clamped == 0.0 {
3113 return Ok((normal_cdf(h) * normal_cdf(k)).clamp(0.0, 1.0));
3114 }
3115 if h == 0.0 && k == 0.0 {
3116 return Ok((0.25 + rho_clamped.asin() / std::f64::consts::TAU).clamp(0.0, 1.0));
3117 }
3118
3119 let asr = rho_clamped.asin();
3120 let sum = bvn_gl_sum(h, k, rho_clamped, asr);
3121 Ok((normal_cdf(h) * normal_cdf(k) + asr * sum / (4.0 * std::f64::consts::PI)).clamp(0.0, 1.0))
3122}
3123
3124#[inline]
3125fn bvn_gl_sum_interval(h: f64, left: f64, right: f64, rho_clamped: f64, asr: f64) -> f64 {
3126 if rho_clamped == 0.0 {
3127 return 0.0;
3128 }
3129 let h2 = h * h;
3130 let right_hs = 0.5 * (h2 + right * right);
3131 let left_hs = 0.5 * (h2 + left * left);
3132 let half_asr = 0.5 * asr;
3133 let (sin_mid, cos_mid) = half_asr.sin_cos();
3134 let mut sum = 0.0;
3135 for i in 0..10 {
3136 let node = GL20_NODES[i].abs();
3137 let weight = GL20_WEIGHTS[i];
3138 let (sin_delta, cos_delta) = (half_asr * node).sin_cos();
3139
3140 let sn_lo = sin_mid * cos_delta - cos_mid * sin_delta;
3141 let one_minus_lo = 1.0 - sn_lo * sn_lo;
3142 let lo_right = (((sn_lo * h * right) - right_hs) / one_minus_lo).exp();
3143 let lo_left = (((sn_lo * h * left) - left_hs) / one_minus_lo).exp();
3144
3145 let sn_hi = sin_mid * cos_delta + cos_mid * sin_delta;
3146 let one_minus_hi = 1.0 - sn_hi * sn_hi;
3147 let hi_right = (((sn_hi * h * right) - right_hs) / one_minus_hi).exp();
3148 let hi_left = (((sn_hi * h * left) - left_hs) / one_minus_hi).exp();
3149
3150 sum += weight * ((lo_right - lo_left) + (hi_right - hi_left));
3151 }
3152 sum
3153}
3154
3155fn bivariate_normal_cdf_interval(h: f64, left: f64, right: f64, rho: f64) -> Result<f64, String> {
3156 if right <= left {
3157 return Ok(0.0);
3158 }
3159 if left == f64::NEG_INFINITY && right == f64::INFINITY {
3160 return Ok(normal_cdf(h));
3161 }
3162 if !left.is_finite() || !right.is_finite() {
3163 let upper = bivariate_normal_cdf(h, right, rho)?;
3164 let lower = bivariate_normal_cdf(h, left, rho)?;
3165 return Ok((upper - lower).clamp(0.0, 1.0));
3166 }
3167 validate_bvn_args(h, left, rho)?;
3168 validate_bvn_args(h, right, rho)?;
3169 if h == f64::NEG_INFINITY {
3170 return Ok(0.0);
3171 }
3172 if h == f64::INFINITY {
3173 return Ok((normal_cdf(right) - normal_cdf(left)).clamp(0.0, 1.0));
3174 }
3175
3176 let rho_clamped = rho.clamp(-1.0, 1.0);
3177 if rho_clamped >= 1.0 - 1e-12 || rho_clamped <= -1.0 + 1e-12 {
3178 let upper = bivariate_normal_cdf(h, right, rho_clamped)?;
3179 let lower = bivariate_normal_cdf(h, left, rho_clamped)?;
3180 return Ok((upper - lower).clamp(0.0, 1.0));
3181 }
3182
3183 let cdf_h = normal_cdf(h);
3184 let normal_part = cdf_h * (normal_cdf(right) - normal_cdf(left));
3185 if rho_clamped == 0.0 {
3186 return Ok(normal_part.clamp(0.0, 1.0));
3187 }
3188 let asr = rho_clamped.asin();
3189 let sum = bvn_gl_sum_interval(h, left, right, rho_clamped, asr);
3190 Ok((normal_part + asr * sum / (4.0 * std::f64::consts::PI)).clamp(0.0, 1.0))
3191}
3192
3193fn exp_neg_half_square(x: f64) -> f64 {
3194 if x.is_infinite() {
3195 0.0
3196 } else {
3197 (-0.5 * x * x).exp()
3198 }
3199}
3200
3201fn truncated_gaussian_zeroth_moment(a: f64, b: f64) -> f64 {
3245 let inv_sqrt2 = 1.0 / std::f64::consts::SQRT_2;
3246 let za = a * inv_sqrt2;
3247 let zb = b * inv_sqrt2;
3248 let erf_diff = if za >= 0.0 {
3249 libm::erfc(za) - libm::erfc(zb)
3250 } else if zb <= 0.0 {
3251 libm::erfc(-zb) - libm::erfc(-za)
3252 } else if zb <= 0.5 && -za <= 0.5 {
3253 libm::erf(zb) + libm::erf(-za)
3258 } else {
3259 2.0 - libm::erfc(zb) - libm::erfc(-za)
3260 };
3261 (std::f64::consts::PI / 2.0).sqrt() * erf_diff
3263}
3264
3265fn fill_truncated_gaussian_moments(a: f64, b: f64, out: &mut [f64]) {
3287 if out.is_empty() {
3288 return;
3289 }
3290 out[0] = truncated_gaussian_zeroth_moment(a, b);
3291 if out.len() == 1 {
3292 return;
3293 }
3294 let ea = exp_neg_half_square(a);
3295 let eb = exp_neg_half_square(b);
3296 out[1] = ea - eb;
3297 if out.len() == 2 {
3298 return;
3299 }
3300 let a_finite = a.is_finite();
3301 let b_finite = b.is_finite();
3302 let mut a_pow_n_minus_1 = a; let mut b_pow_n_minus_1 = b;
3310 for n in 2..out.len() {
3311 let left = if a_finite { a_pow_n_minus_1 * ea } else { 0.0 };
3312 let right = if b_finite { b_pow_n_minus_1 * eb } else { 0.0 };
3313 out[n] = left - right + (n as f64 - 1.0) * out[n - 2];
3314 a_pow_n_minus_1 *= a;
3315 b_pow_n_minus_1 *= b;
3316 }
3317}
3318
3319const MAX_AFFINE_ANCHOR_DEGREE: usize = 64;
3324
3325pub fn affine_anchor_moment_vector(
3326 alpha: f64,
3327 beta: f64,
3328 left: f64,
3329 right: f64,
3330 max_degree: usize,
3331) -> Vec<f64> {
3332 let mut out = vec![0.0; max_degree + 1];
3333 affine_anchor_moment_vector_into(alpha, beta, left, right, max_degree, &mut out);
3334 out
3335}
3336
3337fn affine_anchor_moment_vector_into(
3338 alpha: f64,
3339 beta: f64,
3340 left: f64,
3341 right: f64,
3342 max_degree: usize,
3343 out: &mut [f64],
3344) {
3345 assert_eq!(out.len(), max_degree + 1);
3346 let s = (1.0 + beta * beta).sqrt();
3347 let mu = -alpha * beta / (1.0 + beta * beta);
3348 let y_left = if left.is_infinite() {
3349 if left.is_sign_positive() {
3350 f64::INFINITY
3351 } else {
3352 f64::NEG_INFINITY
3353 }
3354 } else {
3355 s * (left - mu)
3356 };
3357 let y_right = if right.is_infinite() {
3358 if right.is_sign_positive() {
3359 f64::INFINITY
3360 } else {
3361 f64::NEG_INFINITY
3362 }
3363 } else {
3364 s * (right - mu)
3365 };
3366 let anchor = (-alpha * alpha / (2.0 * s * s)).exp() / s;
3367 assert!(
3368 max_degree <= MAX_AFFINE_ANCHOR_DEGREE,
3369 "affine_anchor_moment_vector max_degree {} exceeds compile-time bound {}",
3370 max_degree,
3371 MAX_AFFINE_ANCHOR_DEGREE
3372 );
3373 let mut t = [0.0_f64; MAX_AFFINE_ANCHOR_DEGREE + 1];
3374 fill_truncated_gaussian_moments(y_left, y_right, &mut t[..=max_degree]);
3375 let mut mu_pow = [1.0_f64; MAX_AFFINE_ANCHOR_DEGREE + 1];
3381 for k in 1..=max_degree {
3382 mu_pow[k] = mu_pow[k - 1] * mu;
3383 }
3384 let inv_s = 1.0 / s;
3385 let mut inv_s_pow = [1.0_f64; MAX_AFFINE_ANCHOR_DEGREE + 1];
3386 for k in 1..=max_degree {
3387 inv_s_pow[k] = inv_s_pow[k - 1] * inv_s;
3388 }
3389 out.fill(0.0);
3390 for n in 0..=max_degree {
3391 let mut acc = 0.0;
3392 let mut binom = 1.0;
3394 for k in 0..=n {
3395 let term = binom * mu_pow[n - k] * inv_s_pow[k];
3396 acc = term.mul_add(t[k], acc);
3397 if k < n {
3398 binom = binom * (n - k) as f64 / (k + 1) as f64;
3399 }
3400 }
3401 out[n] = anchor * acc;
3402 }
3403}
3404
3405fn affine_value_from_moment_primitive(
3406 alpha: f64,
3407 beta: f64,
3408 left: f64,
3409 right: f64,
3410) -> Result<f64, String> {
3411 let s = (1.0 + beta * beta).sqrt();
3423 let h = alpha / s;
3424 let rho = -beta / s;
3425 bivariate_normal_cdf_interval(h, left, right, rho)
3426}
3427
3428fn validate_cell_inputs(cell: DenestedCubicCell) -> Result<(), String> {
3429 for (name, value) in [
3430 ("c0", cell.c0),
3431 ("c1", cell.c1),
3432 ("c2", cell.c2),
3433 ("c3", cell.c3),
3434 ] {
3435 if !value.is_finite() {
3436 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3437 "cell coefficient {name} must be finite, got {value}"
3438 ))
3439 .into());
3440 }
3441 }
3442 if cell.left.is_nan() || cell.right.is_nan() || cell.left >= cell.right {
3443 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3444 "cell bounds must satisfy left < right without NaN, got [{}, {}]",
3445 cell.left, cell.right
3446 ))
3447 .into());
3448 }
3449 Ok(())
3450}
3451
3452fn validate_affine_cell_inputs(cell: DenestedCubicCell, max_degree: usize) -> Result<(), String> {
3453 validate_cell_inputs(cell)?;
3454 if cell.c2 != 0.0 || cell.c3 != 0.0 {
3455 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3456 "affine cell requires c2=c3=0 exactly, got c2={:.6e}, c3={:.6e}",
3457 cell.c2, cell.c3
3458 ))
3459 .into());
3460 }
3461 if max_degree > MAX_AFFINE_ANCHOR_DEGREE {
3462 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3463 "affine cell moment degree {max_degree} exceeds supported maximum {MAX_AFFINE_ANCHOR_DEGREE}"
3464 ))
3465 .into());
3466 }
3467 Ok(())
3468}
3469
3470pub fn evaluate_affine_cell_state(
3477 cell: DenestedCubicCell,
3478 max_degree: usize,
3479) -> Result<CellMomentState, String> {
3480 validate_affine_cell_inputs(cell, max_degree)?;
3481 let alpha = cell.c0;
3482 let beta = cell.c1;
3483 let value = affine_value_from_moment_primitive(alpha, beta, cell.left, cell.right)?;
3484 let moments = affine_anchor_moment_vector(alpha, beta, cell.left, cell.right, max_degree);
3485 Ok(CellMomentState {
3486 branch: ExactCellBranch::Affine,
3487 value,
3488 moments: moments.into(),
3489 })
3490}
3491
3492fn evaluate_affine_cell_derivative_state(
3493 cell: DenestedCubicCell,
3494 max_degree: usize,
3495) -> Result<CellDerivativeMomentState, String> {
3496 validate_affine_cell_inputs(cell, max_degree)?;
3497 let alpha = cell.c0;
3498 let beta = cell.c1;
3499 let moments = affine_anchor_moment_vector(alpha, beta, cell.left, cell.right, max_degree);
3500 Ok(CellDerivativeMomentState {
3501 branch: ExactCellBranch::Affine,
3502 moments: moments.into(),
3503 })
3504}
3505
3506#[inline]
3513fn accumulate_moments_unrolled4(moments: &mut [f64], mw: f64, z: f64) {
3514 let mut z_pow = 1.0_f64;
3515 for slot in moments.iter_mut() {
3516 *slot = mw.mul_add(z_pow, *slot);
3517 z_pow *= z;
3518 }
3519}
3520
3521#[inline(always)]
3564fn evaluate_non_affine_cell_with_rule<const COMPUTE_VALUE: bool>(
3565 cell: DenestedCubicCell,
3566 max_degree: usize,
3567 gl_nodes: &[f64],
3568 gl_weights: &[f64],
3569) -> (CellMomentVec, f64) {
3570 let mut moments: CellMomentVec = smallvec![0.0_f64; max_degree + 1];
3571 let mut value_integral = 0.0_f64;
3572 let center = 0.5 * (cell.left + cell.right);
3573 let half_width = 0.5 * (cell.right - cell.left);
3574 let c0 = cell.c0;
3575 let c1 = cell.c1;
3576 let c2 = cell.c2;
3577 let c3 = cell.c3;
3578 let moments_slice: &mut [f64] = &mut moments;
3579 assert_eq!(gl_nodes.len(), gl_weights.len());
3580 use wide::f64x4;
3581 let center_v = f64x4::splat(center);
3582 let half_width_v = f64x4::splat(half_width);
3583 let c0_v = f64x4::splat(c0);
3584 let c1_v = f64x4::splat(c1);
3585 let c2_v = f64x4::splat(c2);
3586 let c3_v = f64x4::splat(c3);
3587 let neg_half_v = f64x4::splat(-0.5);
3588 let n_total = gl_nodes.len();
3589 let n_simd = n_total - (n_total % 4);
3590 let mut i = 0;
3591 while i < n_simd {
3592 let node_v = f64x4::from([
3593 gl_nodes[i],
3594 gl_nodes[i + 1],
3595 gl_nodes[i + 2],
3596 gl_nodes[i + 3],
3597 ]);
3598 let weight_v = f64x4::from([
3599 gl_weights[i],
3600 gl_weights[i + 1],
3601 gl_weights[i + 2],
3602 gl_weights[i + 3],
3603 ]);
3604 let z_v = half_width_v.mul_add(node_v, center_v);
3605 let eta_v = c3_v
3607 .mul_add(z_v, c2_v)
3608 .mul_add(z_v, c1_v)
3609 .mul_add(z_v, c0_v);
3610 let z2_v = z_v * z_v;
3611 let neg_q_v = neg_half_v * (z2_v + eta_v * eta_v);
3612 let exp_negq_v = neg_q_v.exp();
3613 let moment_weight_v = weight_v * exp_negq_v;
3614 let z_arr = z_v.to_array();
3615 let mw_arr = moment_weight_v.to_array();
3616 if COMPUTE_VALUE {
3617 for lane in 0..4 {
3618 let z = z_arr[lane];
3619 let mw = mw_arr[lane];
3620 accumulate_moments_unrolled4(moments_slice, mw, z);
3621 let node = gl_nodes[i + lane];
3634 let weight = gl_weights[i + lane];
3635 let z_ref = center + half_width * node;
3636 let eta_ref = c0 + c1 * z_ref + c2 * z_ref * z_ref + c3 * z_ref * z_ref * z_ref;
3637 value_integral += weight * (-0.5 * z_ref * z_ref).exp() * normal_cdf(eta_ref);
3638 }
3639 } else {
3640 for lane in 0..4 {
3641 let z = z_arr[lane];
3642 let mw = mw_arr[lane];
3643 accumulate_moments_unrolled4(moments_slice, mw, z);
3644 }
3645 }
3646 i += 4;
3647 }
3648 while i < n_total {
3649 let node = gl_nodes[i];
3650 let weight = gl_weights[i];
3651 let z = center + half_width * node;
3652 let eta = c3.mul_add(z, c2).mul_add(z, c1).mul_add(z, c0);
3653 let q = 0.5 * (z * z + eta * eta);
3654 let moment_weight = weight * (-q).exp();
3655 accumulate_moments_unrolled4(moments_slice, moment_weight, z);
3656 if COMPUTE_VALUE {
3657 let eta_ref = c0 + c1 * z + c2 * z * z + c3 * z * z * z;
3662 value_integral += weight * (-0.5 * z * z).exp() * normal_cdf(eta_ref);
3663 }
3664 i += 1;
3665 }
3666 for moment in moments_slice.iter_mut() {
3670 *moment *= half_width;
3671 }
3672 let value = if COMPUTE_VALUE {
3673 value_integral * half_width
3674 } else {
3675 value_integral
3676 };
3677 (moments, value)
3678}
3679
3680const NON_AFFINE_LADDER_RTOL: f64 = 1e-15;
3706
3707const NON_AFFINE_LADDER_RUNGS: [usize; 5] = [12, 24, 48, 96, 192];
3710
3711fn non_affine_ladder_rules() -> &'static [(Vec<f64>, Vec<f64>)] {
3718 static RULES: std::sync::OnceLock<Vec<(Vec<f64>, Vec<f64>)>> = std::sync::OnceLock::new();
3719 RULES.get_or_init(|| {
3720 NON_AFFINE_LADDER_RUNGS
3721 .iter()
3722 .map(|&n| gauss_legendre_rule(n))
3723 .collect()
3724 })
3725}
3726
3727use gam_math::special::gauss_legendre as gauss_legendre_rule;
3731
3732fn non_affine_ladder_converged(coarse: &CellMomentVec, fine: &CellMomentVec) -> bool {
3747 let mut scale = 0.0_f64;
3748 let mut err = 0.0_f64;
3749 for (&c, &f) in coarse.iter().zip(fine.iter()) {
3750 scale = scale.max(f.abs());
3751 err = err.max((c - f).abs());
3752 }
3753 if !(scale.is_finite() && err.is_finite()) {
3754 return false;
3755 }
3756 err <= NON_AFFINE_LADDER_RTOL * scale
3757}
3758
3759pub(crate) static NON_AFFINE_LADDER_CERT_COUNTS: [AtomicU64; NON_AFFINE_LADDER_RUNGS.len() + 1] = [
3767 AtomicU64::new(0),
3768 AtomicU64::new(0),
3769 AtomicU64::new(0),
3770 AtomicU64::new(0),
3771 AtomicU64::new(0),
3772 AtomicU64::new(0),
3773];
3774
3775pub fn non_affine_ladder_cert_histogram() -> (Vec<(usize, u64)>, u64) {
3778 let per_rung = NON_AFFINE_LADDER_RUNGS
3779 .iter()
3780 .enumerate()
3781 .map(|(i, &n)| (n, NON_AFFINE_LADDER_CERT_COUNTS[i].load(Ordering::Relaxed)))
3782 .collect();
3783 let terminal =
3784 NON_AFFINE_LADDER_CERT_COUNTS[NON_AFFINE_LADDER_RUNGS.len()].load(Ordering::Relaxed);
3785 (per_rung, terminal)
3786}
3787
3788#[inline]
3793fn evaluate_non_affine_cell_simd<const COMPUTE_VALUE: bool>(
3794 cell: DenestedCubicCell,
3795 max_degree: usize,
3796) -> (CellMomentVec, f64) {
3797 let mut prev: Option<(CellMomentVec, f64)> = None;
3798 for (i, (nodes, weights)) in non_affine_ladder_rules().iter().enumerate() {
3799 let cur =
3800 evaluate_non_affine_cell_with_rule::<COMPUTE_VALUE>(cell, max_degree, nodes, weights);
3801 if let Some(prev) = prev.as_ref()
3802 && non_affine_ladder_converged(&prev.0, &cur.0)
3803 {
3804 NON_AFFINE_LADDER_CERT_COUNTS[i].fetch_add(1, Ordering::Relaxed);
3805 return cur;
3806 }
3807 prev = Some(cur);
3808 }
3809 NON_AFFINE_LADDER_CERT_COUNTS[NON_AFFINE_LADDER_RUNGS.len()].fetch_add(1, Ordering::Relaxed);
3810 evaluate_non_affine_cell_with_rule::<COMPUTE_VALUE>(cell, max_degree, &GL_NODES, &GL_WEIGHTS)
3811}
3812
3813fn evaluate_non_affine_cell_value_terminal(cell: DenestedCubicCell) -> f64 {
3833 let center = 0.5 * (cell.left + cell.right);
3834 let half_width = 0.5 * (cell.right - cell.left);
3835 let c0 = cell.c0;
3836 let c1 = cell.c1;
3837 let c2 = cell.c2;
3838 let c3 = cell.c3;
3839 let mut value_integral = 0.0_f64;
3840 for (&node, &weight) in GL_NODES.iter().zip(GL_WEIGHTS.iter()) {
3841 let z = center + half_width * node;
3842 let eta = c0 + c1 * z + c2 * z * z + c3 * z * z * z;
3843 value_integral += weight * (-0.5 * z * z).exp() * normal_cdf(eta);
3844 }
3845 value_integral * half_width
3846}
3847
3848fn evaluate_non_affine_cell_state(
3849 cell: DenestedCubicCell,
3850 branch: ExactCellBranch,
3851 max_degree: usize,
3852) -> Result<CellMomentState, String> {
3853 let (moments, _) = evaluate_non_affine_cell_simd::<false>(cell, max_degree);
3854 let value_integral = evaluate_non_affine_cell_value_terminal(cell);
3855 Ok(CellMomentState {
3860 branch,
3861 value: value_integral / (std::f64::consts::TAU).sqrt(),
3862 moments,
3863 })
3864}
3865
3866fn evaluate_non_affine_cell_derivative_state(
3867 cell: DenestedCubicCell,
3868 branch: ExactCellBranch,
3869 max_degree: usize,
3870) -> Result<CellDerivativeMomentState, String> {
3871 let (moments, _) = evaluate_non_affine_cell_simd::<false>(cell, max_degree);
3872 Ok(CellDerivativeMomentState { branch, moments })
3873}
3874
3875pub fn evaluate_cell_moments(
3881 cell: DenestedCubicCell,
3882 max_degree: usize,
3883) -> Result<CellMomentState, String> {
3884 if !TAIL_CELL_MOMENT_CACHE_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
3885 return evaluate_cell_moments_uncached(cell, max_degree);
3886 }
3887 tail_cell_moment_cache().evaluate(cell, max_degree)
3888}
3889
3890pub fn evaluate_cell_moments_uncached(
3895 cell: DenestedCubicCell,
3896 max_degree: usize,
3897) -> Result<CellMomentState, String> {
3898 evaluate_cell_state_dispatched(
3899 cell,
3900 max_degree,
3901 evaluate_affine_cell_state,
3902 evaluate_non_affine_cell_state,
3903 )
3904}
3905
3906pub fn evaluate_cell_derivative_moments_uncached(
3913 cell: DenestedCubicCell,
3914 max_degree: usize,
3915) -> Result<CellDerivativeMomentState, String> {
3916 evaluate_cell_state_dispatched(
3917 cell,
3918 max_degree,
3919 evaluate_affine_cell_derivative_state,
3920 evaluate_non_affine_cell_derivative_state,
3921 )
3922}
3923
3924fn evaluate_cell_state_dispatched<S>(
3933 cell: DenestedCubicCell,
3934 max_degree: usize,
3935 affine: fn(DenestedCubicCell, usize) -> Result<S, String>,
3936 non_affine: fn(DenestedCubicCell, ExactCellBranch, usize) -> Result<S, String>,
3937) -> Result<S, String> {
3938 validate_cell_inputs(cell)?;
3939 let left_inf = !cell.left.is_finite();
3940 let right_inf = !cell.right.is_finite();
3941 if left_inf || right_inf {
3942 if cell.c2 != 0.0 || cell.c3 != 0.0 {
3946 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3947 "semi-infinite cell [{}, {}] must be affine (c2=c3=0), got c2={:.3e}, c3={:.3e}",
3948 cell.left, cell.right, cell.c2, cell.c3
3949 ))
3950 .into());
3951 }
3952 return affine(cell, max_degree);
3953 }
3954 if cell.right <= cell.left {
3955 return Err(CubicCellKernelError::invalid_cell_shape(format!(
3956 "finite cell must have left < right, got [{}, {}]",
3957 cell.left, cell.right
3958 ))
3959 .into());
3960 }
3961 let branch = branch_cell(cell)?;
3962 if branch == ExactCellBranch::Affine {
3963 return affine(cell, max_degree);
3964 }
3965 non_affine(cell, branch, max_degree)
3966}
3967
3968pub fn evaluate_cell_moments_cached(
3975 cell: DenestedCubicCell,
3976 max_degree: usize,
3977 cache: &CellMomentLruCache,
3978 stats: Option<&CellMomentCacheStats>,
3979) -> Result<CellMomentState, String> {
3980 if matches!(branch_cell(cell), Ok(ExactCellBranch::Affine)) {
3989 if let Some(stats) = stats {
3990 stats.misses.fetch_add(1, Ordering::Relaxed);
3991 }
3992 return evaluate_cell_moments_uncached(cell, max_degree);
3993 }
3994 let key = CellFingerprint::new(cell);
3995 let existing_derivative = match cache.get(&key) {
3996 Some(cached) => {
3997 if let Some(state) = cached.state_for_degree(max_degree) {
3998 if let Some(stats) = stats {
3999 stats.hits.fetch_add(1, Ordering::Relaxed);
4000 }
4001 return Ok(state);
4002 }
4003 cached.derivative_state.clone()
4007 }
4008 None => None,
4009 };
4010 if let Some(stats) = stats {
4011 stats.misses.fetch_add(1, Ordering::Relaxed);
4012 }
4013 let state = evaluate_cell_moments(cell, max_degree)?;
4014 let shared = Arc::new(state);
4019 let mut entry = CachedCellMoments::new(Arc::clone(&shared));
4020 if let Some(derivative) = existing_derivative {
4021 entry = entry.with_derivative(derivative);
4022 }
4023 cache.insert(key, entry);
4024 Ok(Arc::try_unwrap(shared).unwrap_or_else(|a| (*a).clone()))
4025}
4026
4027pub fn evaluate_cell_derivative_moments_cached(
4033 cell: DenestedCubicCell,
4034 max_degree: usize,
4035 cache: &CellMomentLruCache,
4036 stats: Option<&CellMomentCacheStats>,
4037) -> Result<CellDerivativeMomentState, String> {
4038 if matches!(branch_cell(cell), Ok(ExactCellBranch::Affine)) {
4042 if let Some(stats) = stats {
4043 stats.misses.fetch_add(1, Ordering::Relaxed);
4044 }
4045 return evaluate_cell_derivative_moments_uncached(cell, max_degree);
4046 }
4047 let key = CellFingerprint::new(cell);
4048 let existing_value = match cache.get(&key) {
4049 Some(cached) => {
4050 if let Some(state) = cached.derivative_state_for_degree(max_degree) {
4051 if let Some(stats) = stats {
4052 stats.hits.fetch_add(1, Ordering::Relaxed);
4053 }
4054 return Ok(state);
4055 }
4056 cached.state.clone()
4060 }
4061 None => None,
4062 };
4063 if let Some(stats) = stats {
4064 stats.misses.fetch_add(1, Ordering::Relaxed);
4065 }
4066 let state = evaluate_cell_derivative_moments_uncached(cell, max_degree)?;
4067 let shared = Arc::new(state);
4072 let mut entry = CachedCellMoments::new_derivative(Arc::clone(&shared));
4073 if let Some(value) = existing_value {
4074 entry = entry.with_value(value);
4075 }
4076 cache.insert(key, entry);
4077 Ok(Arc::try_unwrap(shared).unwrap_or_else(|a| (*a).clone()))
4078}
4079
4080pub fn evaluate_cell_moments_with_scratch<'a>(
4087 cell: DenestedCubicCell,
4088 max_degree: usize,
4089 scratch: &'a mut CellMomentScratch,
4090) -> Result<CellMomentStateRef<'a>, String> {
4091 let state = evaluate_cell_moments(cell, max_degree)?;
4092 let out = scratch.prepare_moments(max_degree + 1);
4093 out.copy_from_slice(&state.moments);
4094 Ok(CellMomentStateRef {
4095 branch: state.branch,
4096 value: state.value,
4097 moments: out,
4098 })
4099}
4100
4101#[cfg(test)]
4102mod tests {
4103 use super::*;
4104 use gam_math::probability::normal_pdf;
4105
4106 #[inline]
4107 pub(super) fn polynomial_value(coefficients: &[f64], z: f64) -> f64 {
4108 coefficients
4109 .iter()
4110 .rev()
4111 .fold(0.0, |acc, &coeff| acc * z + coeff)
4112 }
4113
4114 fn reset_cell_moment_test_reallocs() {
4115 super::CELL_MOMENT_REALLOCS.store(0, std::sync::atomic::Ordering::Relaxed);
4116 }
4117
4118 fn cell_moment_test_reallocs() -> usize {
4119 super::CELL_MOMENT_REALLOCS.load(std::sync::atomic::Ordering::Relaxed)
4120 }
4121
4122 fn assert_close_rel(label: &str, actual: f64, expected: f64, tol: f64) {
4123 let denom = expected.abs().max(1.0);
4124 let rel = (actual - expected).abs() / denom;
4125 assert!(
4126 rel <= tol,
4127 "{label}: actual={actual:.17e} expected={expected:.17e} rel={rel:.3e} tol={tol:.3e}"
4128 );
4129 }
4130
4131 #[test]
4146 fn link_basis_cell_fourth_ab_partials_vanish_third_are_nonzero() {
4147 let span = LocalSpanCubic {
4148 left: -0.4,
4149 right: 1.6,
4150 c0: 0.37,
4151 c1: -0.81,
4152 c2: 0.53,
4153 c3: -0.29,
4154 };
4155 let a0 = 0.23_f64;
4156 let b0 = 0.61_f64;
4157 let h = 1e-2_f64;
4158
4159 let stencil = |order: usize| -> &'static [(i64, f64)] {
4161 match order {
4162 0 => &[(0, 1.0)],
4163 1 => &[(-1, -0.5), (1, 0.5)],
4164 2 => &[(-1, 1.0), (0, -2.0), (1, 1.0)],
4165 3 => &[(-2, -0.5), (-1, 1.0), (1, -1.0), (2, 0.5)],
4166 4 => &[(-2, 1.0), (-1, -4.0), (0, 6.0), (1, -4.0), (2, 1.0)],
4167 _ => &[(0, 1.0)],
4168 }
4169 };
4170 let fd = |k: usize, na: usize, nb: usize| -> f64 {
4172 let mut acc = 0.0;
4173 for &(ia, wa) in stencil(na) {
4174 for &(ib, wb) in stencil(nb) {
4175 let a = a0 + (ia as f64) * h;
4176 let b = b0 + (ib as f64) * h;
4177 acc += wa * wb * link_basis_cell_coefficients(span, a, b)[k];
4178 }
4179 }
4180 acc / h.powi((na + nb) as i32)
4181 };
4182
4183 let (p3_aaa, p3_aab, p3_abb, p3_bbb) = link_basis_cell_third_partials(span);
4184
4185 let mut max_third = 0.0_f64;
4189 for k in 0..4 {
4190 for (label, (na, nb), analytic) in [
4191 ("aaa", (3usize, 0usize), p3_aaa[k]),
4192 ("aab", (2, 1), p3_aab[k]),
4193 ("abb", (1, 2), p3_abb[k]),
4194 ("bbb", (0, 3), p3_bbb[k]),
4195 ] {
4196 let got = fd(k, na, nb);
4197 assert!(
4198 (got - analytic).abs() <= 1e-4 + 1e-3 * analytic.abs(),
4199 "3rd partial {label}[{k}] analytic {analytic:+.6e} vs FD {got:+.6e}"
4200 );
4201 max_third = max_third.max(analytic.abs());
4202 }
4203 }
4204 assert!(
4205 max_third > 1e-1,
4206 "expected an appreciable nonzero 3rd (a,b)-partial; max |analytic| = {max_third:.3e}"
4207 );
4208
4209 for k in 0..4 {
4213 for (na, nb) in [(4usize, 0usize), (3, 1), (2, 2), (1, 3), (0, 4)] {
4214 let got = fd(k, na, nb);
4215 assert!(
4216 got.abs() <= 1e-2,
4217 "4th (a,b)-partial ∂^{na}_a∂^{nb}_b of cell coeff[{k}] must vanish, FD = {got:+.6e}"
4218 );
4219 }
4220 }
4221 }
4222
4223 #[test]
4224 fn non_affine_cell_state_grid_matches_public_cell_moments_reference() {
4225 let cells = [
4226 DenestedCubicCell {
4227 left: -1.25,
4228 right: -0.2,
4229 c0: -0.35,
4230 c1: 0.85,
4231 c2: 0.04,
4232 c3: -0.015,
4233 },
4234 DenestedCubicCell {
4235 left: -0.2,
4236 right: 0.55,
4237 c0: 0.12,
4238 c1: -0.65,
4239 c2: -0.025,
4240 c3: 0.02,
4241 },
4242 DenestedCubicCell {
4243 left: 0.55,
4244 right: 1.6,
4245 c0: 0.42,
4246 c1: 0.35,
4247 c2: 0.018,
4248 c3: 0.012,
4249 },
4250 ];
4251 for cell in cells {
4252 let branch = branch_cell(cell).expect("branch");
4253 assert_ne!(branch, ExactCellBranch::Affine);
4254 for max_degree in [0usize, 2, 4, 9, 16] {
4255 let direct = evaluate_non_affine_cell_state(cell, branch, max_degree)
4256 .expect("direct non-affine transport");
4257 let public = evaluate_cell_moments(cell, max_degree).expect("public evaluator");
4258 assert_eq!(direct.branch, public.branch);
4259 assert_eq!(direct.moments.len(), public.moments.len());
4260 let value_scale = direct.value.abs().max(public.value.abs()).max(1.0);
4261 assert!(
4262 (direct.value - public.value).abs() <= 1e-10 * value_scale,
4263 "value mismatch for {cell:?} degree {max_degree}: direct={} public={}",
4264 direct.value,
4265 public.value
4266 );
4267 for (degree, (lhs, rhs)) in
4268 direct.moments.iter().zip(public.moments.iter()).enumerate()
4269 {
4270 let scale = lhs.abs().max(rhs.abs()).max(1.0);
4271 assert!(
4272 (lhs - rhs).abs() <= 1e-10 * scale,
4273 "moment {degree} mismatch for {cell:?} degree {max_degree}: {lhs} vs {rhs}"
4274 );
4275 }
4276 }
4277 }
4278 }
4279
4280 #[test]
4281 fn affine_tail_cell_memo_matches_uncached_grid_and_records_hits() {
4282 let cache = TailCellMomentCache::new();
4288 let c0s = [-2.0, -0.25, 0.0, 1.5];
4289 let c1s = [-1.2, -0.05, 0.0, 0.8];
4290 let endpoints = [-4.0, -1.0, 0.0, 2.5, 6.0];
4291 let degrees = [0_usize, 4, 9, 16, 24];
4292
4293 for &c0 in &c0s {
4294 for &c1 in &c1s {
4295 for &endpoint in &endpoints {
4296 for &max_degree in °rees {
4297 for &(left, right) in
4298 &[(f64::NEG_INFINITY, endpoint), (endpoint, f64::INFINITY)]
4299 {
4300 let cell = DenestedCubicCell {
4301 left,
4302 right,
4303 c0,
4304 c1,
4305 c2: 0.0,
4306 c3: 0.0,
4307 };
4308 let expected = evaluate_cell_moments_uncached(cell, max_degree)
4309 .expect("uncached affine tail moments");
4310 let actual = cache
4311 .evaluate(cell, max_degree)
4312 .expect("cached affine tail moments miss");
4313 let repeat = cache
4314 .evaluate(cell, max_degree)
4315 .expect("cached affine tail moments hit");
4316 assert_eq!(actual.branch, expected.branch);
4317 assert_eq!(repeat.branch, expected.branch);
4318 assert_close_rel(
4319 "tail value miss",
4320 actual.value,
4321 expected.value,
4322 1e-14,
4323 );
4324 assert_close_rel("tail value hit", repeat.value, expected.value, 1e-14);
4325 assert_eq!(actual.moments.len(), expected.moments.len());
4326 assert_eq!(repeat.moments.len(), expected.moments.len());
4327 for (idx, ((a, r), e)) in actual
4328 .moments
4329 .iter()
4330 .zip(repeat.moments.iter())
4331 .zip(expected.moments.iter())
4332 .enumerate()
4333 {
4334 assert_close_rel(
4335 &format!("tail moment miss[{idx}]"),
4336 *a,
4337 *e,
4338 1e-14,
4339 );
4340 assert_close_rel(&format!("tail moment hit[{idx}]"), *r, *e, 1e-14);
4341 }
4342 }
4343 }
4344 }
4345 }
4346 }
4347
4348 let stats = cache.stats();
4349 assert_eq!(stats.misses, stats.entries);
4350 assert!(
4351 stats.hits >= stats.misses,
4352 "expected repeat hits: {stats:?}"
4353 );
4354 assert!(
4355 stats.hit_rate() >= 0.5,
4356 "unexpected low hit rate: {stats:?}"
4357 );
4358 }
4359
4360 fn reference_bivariate_normal_cdf_20(h: f64, k: f64, rho: f64) -> f64 {
4361 if h == f64::NEG_INFINITY || k == f64::NEG_INFINITY {
4362 return 0.0;
4363 }
4364 if h == f64::INFINITY {
4365 return normal_cdf(k);
4366 }
4367 if k == f64::INFINITY {
4368 return normal_cdf(h);
4369 }
4370 let rho_clamped = rho.clamp(-1.0, 1.0);
4371 if rho_clamped >= 1.0 - 1e-12 {
4372 return normal_cdf(h.min(k));
4373 }
4374 if rho_clamped <= -1.0 + 1e-12 {
4375 return (normal_cdf(h) - normal_cdf(-k)).clamp(0.0, 1.0);
4376 }
4377
4378 let hs = 0.5 * (h * h + k * k);
4379 let asr = rho_clamped.asin();
4380 let mut sum = 0.0;
4381 for (&node, &weight) in GL20_NODES.iter().zip(GL20_WEIGHTS.iter()) {
4382 let sn = (0.5 * asr * (node + 1.0)).sin();
4383 let one_minus = 1.0 - sn * sn;
4384 let expo = ((sn * h * k) - hs) / one_minus;
4385 sum += weight * expo.exp();
4386 }
4387 (normal_cdf(h) * normal_cdf(k) + asr * sum / (4.0 * std::f64::consts::PI)).clamp(0.0, 1.0)
4388 }
4389
4390 #[test]
4391 fn non_affine_cell_state_reference_grid_matches_public_moments() {
4392 let c0s = [-0.4, 0.0, 0.35];
4393 let c1s = [-0.8, 0.25, 1.1];
4394 let c2s = [-0.12, 0.08];
4395 let c3s = [-0.04, 0.03];
4396 let intervals = [(-1.25, -0.2), (-0.5, 0.75), (0.1, 1.4)];
4397 let degrees = [3usize, 6, 9, 12];
4398
4399 for &c0 in &c0s {
4400 for &c1 in &c1s {
4401 for &c2 in &c2s {
4402 for &c3 in &c3s {
4403 for &(left, right) in &intervals {
4404 let cell = DenestedCubicCell {
4405 left,
4406 right,
4407 c0,
4408 c1,
4409 c2,
4410 c3,
4411 };
4412 let branch = branch_cell(cell).expect("branch");
4413 assert_ne!(branch, ExactCellBranch::Affine);
4414 for °ree in °rees {
4415 let direct = evaluate_non_affine_cell_state(cell, branch, degree)
4416 .expect("direct non-affine state");
4417 let public = evaluate_cell_moments(cell, degree)
4418 .expect("public non-affine state");
4419 assert_eq!(direct.branch, public.branch);
4420 let value_scale =
4421 direct.value.abs().max(public.value.abs()).max(1.0);
4422 assert!(
4423 (direct.value - public.value).abs() / value_scale <= 1.0e-15,
4424 "value mismatch for {cell:?}, degree {degree}: direct={:.17e}, public={:.17e}",
4425 direct.value,
4426 public.value
4427 );
4428 assert_eq!(direct.moments.len(), public.moments.len());
4429 for (idx, (&a, &b)) in
4430 direct.moments.iter().zip(public.moments.iter()).enumerate()
4431 {
4432 let scale = a.abs().max(b.abs()).max(1.0);
4433 assert!(
4434 (a - b).abs() / scale <= 1.0e-15,
4435 "moment {idx} mismatch for {cell:?}, degree {degree}: direct={a:.17e}, public={b:.17e}"
4436 );
4437 }
4438 }
4439 }
4440 }
4441 }
4442 }
4443 }
4444 }
4445
4446 #[test]
4447 fn bivariate_normal_cdf_matches_reference_grid_to_1e_minus_10() {
4448 let hs = [-8.0, -5.0, -3.0, -1.5, -0.5, 0.0, 0.25, 1.0, 2.5, 5.0, 8.0];
4449 let ks = [-8.0, -4.0, -2.0, -0.75, 0.0, 0.4, 1.25, 3.0, 6.0, 8.0];
4450 let rhos = [
4451 -0.999_999_999_999,
4452 -0.999,
4453 -0.95,
4454 -0.7,
4455 -0.3,
4456 -1.0e-12,
4457 0.0,
4458 1.0e-12,
4459 0.3,
4460 0.7,
4461 0.95,
4462 0.999,
4463 0.999_999_999_999,
4464 ];
4465 for &h in &hs {
4466 for &k in &ks {
4467 for &rho in &rhos {
4468 let actual = bivariate_normal_cdf(h, k, rho).expect("bvn");
4469 let expected = reference_bivariate_normal_cdf_20(h, k, rho);
4470 let scale = expected.abs().max(1.0e-300);
4471 let rel = (actual - expected).abs() / scale;
4472 assert!(
4473 rel < 1.0e-10 || (actual - expected).abs() < 1.0e-14,
4474 "h={h} k={k} rho={rho} actual={actual:.17e} expected={expected:.17e} rel={rel:.3e}"
4475 );
4476 }
4477 }
4478 }
4479 }
4480
4481 #[test]
4482 fn bivariate_normal_cdf_matches_reference_lcg_property_samples() {
4483 let mut seed = 0x5eed_cafe_f00d_u64;
4484 let mut next_unit = || {
4485 seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
4486 ((seed >> 11) as f64) * (1.0 / ((1_u64 << 53) as f64))
4487 };
4488 for _ in 0..4096 {
4489 let h = -8.0 + 16.0 * next_unit();
4490 let k = -8.0 + 16.0 * next_unit();
4491 let rho = -0.999 + 1.998 * next_unit();
4492 let actual = bivariate_normal_cdf(h, k, rho).expect("bvn");
4493 let expected = reference_bivariate_normal_cdf_20(h, k, rho);
4494 let scale = expected.abs().max(1.0e-300);
4495 let rel = (actual - expected).abs() / scale;
4496 assert!(
4497 rel < 1.0e-10 || (actual - expected).abs() < 1.0e-14,
4498 "h={h} k={k} rho={rho} actual={actual:.17e} expected={expected:.17e} rel={rel:.3e}"
4499 );
4500 }
4501 }
4502
4503 #[test]
4504 fn affine_bvn_interval_primitive_matches_two_cdf_difference() {
4505 let hs = [-6.0, -2.0, -0.25, 0.0, 0.8, 3.0, 6.0];
4506 let bounds = [
4507 (-5.0, -2.0),
4508 (-3.0, -0.1),
4509 (-1.0, 0.0),
4510 (-0.25, 0.75),
4511 (0.2, 3.5),
4512 (2.0, 7.0),
4513 ];
4514 let rhos = [-0.98, -0.8, -0.25, 0.0, 0.25, 0.8, 0.98];
4515 for &h in &hs {
4516 for &(left, right) in &bounds {
4517 for &rho in &rhos {
4518 let actual =
4519 bivariate_normal_cdf_interval(h, left, right, rho).expect("interval");
4520 let expected = (reference_bivariate_normal_cdf_20(h, right, rho)
4521 - reference_bivariate_normal_cdf_20(h, left, rho))
4522 .clamp(0.0, 1.0);
4523 let scale = expected.abs().max(1.0e-300);
4524 let rel = (actual - expected).abs() / scale;
4525 assert!(
4526 rel < 1.0e-10 || (actual - expected).abs() < 1.0e-12,
4527 "h={h} left={left} right={right} rho={rho} actual={actual:.17e} expected={expected:.17e} rel={rel:.3e}"
4528 );
4529 }
4530 }
4531 }
4532 }
4533
4534 fn simpson_integral<F>(left: f64, right: f64, steps: usize, f: F) -> f64
4535 where
4536 F: Fn(f64) -> f64,
4537 {
4538 let n = if steps.is_multiple_of(2) {
4539 steps
4540 } else {
4541 steps + 1
4542 };
4543 let h = (right - left) / n as f64;
4544 let mut acc = f(left) + f(right);
4545 for k in 1..n {
4546 let x = left + h * k as f64;
4547 let w = if k % 2 == 0 { 2.0 } else { 4.0 };
4548 acc += w * f(x);
4549 }
4550 acc * h / 3.0
4551 }
4552
4553 #[test]
4554 fn global_transform_preserves_local_span_polynomial() {
4555 let span = LocalSpanCubic {
4556 left: -1.2,
4557 right: 0.8,
4558 c0: 0.3,
4559 c1: -0.25,
4560 c2: 0.11,
4561 c3: -0.04,
4562 };
4563 let (g0, g1, g2, g3) = global_cubic_from_local(span);
4564 for &x in &[-1.2, -0.7, -0.1, 0.4, 0.8] {
4565 let local = span.evaluate(x);
4566 let global = g0 + g1 * x + g2 * x * x + g3 * x * x * x;
4567 assert!((local - global).abs() < 1e-12);
4568 }
4569 }
4570
4571 #[test]
4572 fn bivariate_normal_cdf_independent_factorizes() {
4573 let h = -0.35;
4574 let k = 0.8;
4575 let out = bivariate_normal_cdf(h, k, 0.0).expect("bvn");
4576 let target = normal_cdf(h) * normal_cdf(k);
4577 assert!((out - target).abs() < 1e-12);
4578 }
4579
4580 #[test]
4581 fn evaluate_affine_cell_state_matches_numeric_integrals() {
4582 let cell = DenestedCubicCell {
4583 left: -0.9,
4584 right: 0.8,
4585 c0: 0.15,
4586 c1: -0.35,
4587 c2: 0.0,
4588 c3: 0.0,
4589 };
4590 let state = evaluate_affine_cell_state(cell, 6).expect("affine cell");
4591 let value_numeric = simpson_integral(cell.left, cell.right, 4000, |z| {
4592 super::normal_cdf(cell.eta(z)) * normal_pdf(z)
4593 });
4594 assert_eq!(state.branch, ExactCellBranch::Affine);
4595 assert!((state.value - value_numeric).abs() < 1e-9);
4596 for degree in 0..=6 {
4597 let target = simpson_integral(cell.left, cell.right, 4000, |z| {
4598 z.powi(degree as i32) * (-cell.q(z)).exp()
4599 });
4600 assert!((state.moments[degree] - target).abs() < 1e-9);
4601 }
4602 }
4603
4604 #[test]
4611 fn affine_value_primitive_propagates_bvn_errors_2293() {
4612 for (case, result) in [
4613 (
4614 "non-finite standardized threshold",
4615 affine_value_from_moment_primitive(f64::NAN, -0.35, -0.9, 0.8),
4616 ),
4617 (
4618 "non-finite integration bound",
4619 affine_value_from_moment_primitive(0.15, -0.35, f64::NAN, 0.8),
4620 ),
4621 ] {
4622 let error = result.expect_err(case);
4623 assert!(!error.is_empty(), "{case} must retain its BVN diagnostic");
4624 }
4625 }
4626
4627 #[test]
4632 fn affine_cell_errors_are_never_substituted_with_probability_zero_2293() {
4633 let base = DenestedCubicCell {
4634 left: -0.9,
4635 right: 0.8,
4636 c0: 0.15,
4637 c1: -0.35,
4638 c2: 0.0,
4639 c3: 0.0,
4640 };
4641 for (field, invalid) in [
4642 ("c0", f64::NAN),
4643 ("c0", f64::INFINITY),
4644 ("c1", f64::NEG_INFINITY),
4645 ("c2", f64::NAN),
4646 ("c3", f64::INFINITY),
4647 ] {
4648 let cell = match field {
4649 "c0" => DenestedCubicCell {
4650 c0: invalid,
4651 ..base
4652 },
4653 "c1" => DenestedCubicCell {
4654 c1: invalid,
4655 ..base
4656 },
4657 "c2" => DenestedCubicCell {
4658 c2: invalid,
4659 ..base
4660 },
4661 "c3" => DenestedCubicCell {
4662 c3: invalid,
4663 ..base
4664 },
4665 _ => unreachable!(),
4666 };
4667 assert!(evaluate_affine_cell_state(cell, 3).is_err());
4668 assert!(evaluate_cell_moments_uncached(cell, 3).is_err());
4669 }
4670 for cell in [
4671 DenestedCubicCell {
4672 left: f64::NAN,
4673 ..base
4674 },
4675 DenestedCubicCell {
4676 right: f64::NAN,
4677 ..base
4678 },
4679 DenestedCubicCell {
4680 left: 1.0,
4681 right: 0.0,
4682 ..base
4683 },
4684 ] {
4685 assert!(evaluate_affine_cell_state(cell, 3).is_err());
4686 assert!(evaluate_cell_moments_uncached(cell, 3).is_err());
4687 }
4688 }
4689
4690 #[test]
4691 fn semi_infinite_cells_require_structurally_affine_coefficients_2293() {
4692 let tiny_curvature = 5.0e-11;
4693 for cell in [
4694 DenestedCubicCell {
4695 left: f64::NEG_INFINITY,
4696 right: 0.5,
4697 c0: 0.2,
4698 c1: -0.1,
4699 c2: tiny_curvature,
4700 c3: 0.0,
4701 },
4702 DenestedCubicCell {
4703 left: -0.5,
4704 right: f64::INFINITY,
4705 c0: 0.2,
4706 c1: -0.1,
4707 c2: 0.0,
4708 c3: -tiny_curvature,
4709 },
4710 ] {
4711 assert!(branch_cell(cell).is_err());
4712 assert!(evaluate_cell_moments_uncached(cell, 3).is_err());
4713 assert!(tail_cell_cache_key(cell, 3).is_none());
4714 }
4715 }
4716
4717 #[test]
4718 fn large_affine_anchor_cannot_hide_finite_cell_curvature_2321() {
4719 let cell = DenestedCubicCell {
4720 left: -1.0,
4721 right: 1.0,
4722 c0: 1.0e8,
4723 c1: -2.0e7,
4724 c2: -7.895_512e-3,
4725 c3: -2.973_499e-3,
4726 };
4727
4728 assert_eq!(branch_cell(cell).unwrap(), ExactCellBranch::Sextic);
4729 assert_ne!(
4730 evaluate_cell_moments_uncached(cell, 9).unwrap().branch,
4731 ExactCellBranch::Affine
4732 );
4733 }
4734
4735 #[test]
4736 fn affine_cell_value_matches_zero_moment_derivative() {
4737 let cell = DenestedCubicCell {
4738 left: -1.1,
4739 right: 0.7,
4740 c0: 0.23,
4741 c1: -0.41,
4742 c2: 0.0,
4743 c3: 0.0,
4744 };
4745 let h = 1e-6;
4746 let plus = evaluate_affine_cell_state(
4747 DenestedCubicCell {
4748 c0: cell.c0 + h,
4749 ..cell
4750 },
4751 0,
4752 )
4753 .expect("affine plus");
4754 let minus = evaluate_affine_cell_state(
4755 DenestedCubicCell {
4756 c0: cell.c0 - h,
4757 ..cell
4758 },
4759 0,
4760 )
4761 .expect("affine minus");
4762 let center = evaluate_affine_cell_state(cell, 0).expect("affine center");
4763 let d_value = (plus.value - minus.value) / (2.0 * h);
4764 let target = INV_TWO_PI * center.moments[0];
4765 assert!((d_value - target).abs() < 1e-8);
4766 }
4767
4768 #[test]
4769 fn coefficient_partials_match_exact_span_derivatives() {
4770 let score_span = LocalSpanCubic {
4771 left: -0.75,
4772 right: 0.25,
4773 c0: 0.08,
4774 c1: -0.03,
4775 c2: 0.02,
4776 c3: -0.01,
4777 };
4778 let link_span = LocalSpanCubic {
4779 left: -0.6,
4780 right: 0.9,
4781 c0: -0.05,
4782 c1: 0.04,
4783 c2: -0.02,
4784 c3: 0.015,
4785 };
4786 let a = 0.3;
4787 let b = -0.7;
4788 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
4789 for &z in &[-0.75, -0.4, -0.1, 0.2] {
4790 let u = a + b * z;
4791 let eta_a = 1.0 + link_span.first_derivative(u);
4792 let eta_b = z + score_span.evaluate(z) + z * link_span.first_derivative(u);
4793 assert!((polynomial_value(&dc_da, z) - eta_a).abs() < 1e-12);
4794 assert!((polynomial_value(&dc_db, z) - eta_b).abs() < 1e-12);
4795 }
4796 }
4797
4798 #[test]
4799 fn second_coefficient_partials_match_exact_span_derivatives() {
4800 let score_span = LocalSpanCubic {
4801 left: -0.75,
4802 right: 0.25,
4803 c0: 0.08,
4804 c1: -0.03,
4805 c2: 0.02,
4806 c3: -0.01,
4807 };
4808 let link_span = LocalSpanCubic {
4809 left: -0.6,
4810 right: 0.9,
4811 c0: -0.05,
4812 c1: 0.04,
4813 c2: -0.02,
4814 c3: 0.015,
4815 };
4816 let a = 0.3;
4817 let b = -0.7;
4818 let second_partials = denested_cell_second_partials(score_span, link_span, a, b);
4819 let dc_daa = second_partials.0;
4820 let dc_dab = second_partials.1;
4821 let dc_dbb = second_partials.2;
4822 for &z in &[-0.75, -0.4, -0.1, 0.2] {
4823 let u = a + b * z;
4824 let eta_aa = link_span.second_derivative(u);
4825 let eta_ab = z * link_span.second_derivative(u);
4826 let eta_bb = z * z * link_span.second_derivative(u);
4827 assert!((polynomial_value(&dc_daa, z) - eta_aa).abs() < 1e-12);
4828 assert!((polynomial_value(&dc_dab, z) - eta_ab).abs() < 1e-12);
4829 assert!((polynomial_value(&dc_dbb, z) - eta_bb).abs() < 1e-12);
4830 }
4831 }
4832
4833 #[test]
4834 fn higher_derivative_moment_helpers_reject_empty_first_coefficients() {
4835 let cell = DenestedCubicCell {
4836 left: -1.0,
4837 right: 1.0,
4838 c0: 0.0,
4839 c1: 1.0,
4840 c2: 0.0,
4841 c3: 0.0,
4842 };
4843 let moments = [1.0; 16];
4844
4845 let third_err = cell_third_derivative_from_moments(
4846 cell,
4847 &[],
4848 &[1.0],
4849 &[1.0],
4850 &[],
4851 &[],
4852 &[],
4853 &[],
4854 &moments,
4855 )
4856 .expect_err("empty first coefficients should be rejected");
4857 assert!(third_err.contains("r first-derivative coefficients must be non-empty"));
4858
4859 let fourth_err = cell_fourth_derivative_from_moments(
4860 cell,
4861 &[1.0],
4862 &[],
4863 &[1.0],
4864 &[1.0],
4865 &[],
4866 &[],
4867 &[],
4868 &[],
4869 &[],
4870 &[],
4871 &[],
4872 &[],
4873 &[],
4874 &[],
4875 &[],
4876 &moments,
4877 )
4878 .expect_err("empty first coefficients should be rejected");
4879 assert!(fourth_err.contains("s first-derivative coefficients must be non-empty"));
4880 }
4881
4882 #[test]
4883 fn fourth_derivative_rejects_overlong_scratch_convolutions() {
4884 let cell = DenestedCubicCell {
4885 left: -1.0,
4886 right: 1.0,
4887 c0: 0.0,
4888 c1: 1.0,
4889 c2: 0.0,
4890 c3: 0.0,
4891 };
4892 let long_first = [1.0; 10];
4893 let zero = [0.0; 1];
4894 let moments = [1.0; 64];
4895
4896 let err = cell_fourth_derivative_from_moments(
4897 cell,
4898 &long_first,
4899 &long_first,
4900 &long_first,
4901 &long_first,
4902 &zero,
4903 &zero,
4904 &zero,
4905 &zero,
4906 &zero,
4907 &zero,
4908 &zero,
4909 &zero,
4910 &zero,
4911 &zero,
4912 &zero,
4913 &moments,
4914 )
4915 .expect_err("oversized convolution should be rejected before writing scratch");
4916 assert!(err.contains("fourth derivative polynomial convolution scratch too small"));
4917 }
4918
4919 #[test]
4920 fn score_and_link_basis_cell_coefficients_match_direct_construction() {
4921 let score_basis_span = LocalSpanCubic {
4922 left: -0.7,
4923 right: 0.4,
4924 c0: 0.2,
4925 c1: -0.04,
4926 c2: 0.03,
4927 c3: -0.01,
4928 };
4929 let link_basis_span = LocalSpanCubic {
4930 left: -0.5,
4931 right: 1.1,
4932 c0: -0.03,
4933 c1: 0.05,
4934 c2: -0.02,
4935 c3: 0.01,
4936 };
4937 let a = 0.25;
4938 let b = -0.8;
4939 let score_coeffs = score_basis_cell_coefficients(score_basis_span, b);
4940 let link_coeffs = link_basis_cell_coefficients(link_basis_span, a, b);
4941 for &z in &[-0.7, -0.1, 0.2, 0.4] {
4942 let score_poly = polynomial_value(&score_coeffs, z);
4943 let link_poly = polynomial_value(&link_coeffs, z);
4944 assert!((score_poly - b * score_basis_span.evaluate(z)).abs() < 1e-12);
4945 assert!((link_poly - link_basis_span.evaluate(a + b * z)).abs() < 1e-12);
4946 }
4947 }
4948
4949 #[test]
4950 fn link_basis_partials_match_exact_span_derivatives() {
4951 let link_basis_span = LocalSpanCubic {
4952 left: -0.5,
4953 right: 1.1,
4954 c0: -0.03,
4955 c1: 0.05,
4956 c2: -0.02,
4957 c3: 0.01,
4958 };
4959 let a = 0.25;
4960 let b = -0.8;
4961 let (dc_da, dc_db) = link_basis_cell_coefficient_partials(link_basis_span, a, b);
4962 let (dc_daa, dc_dab, dc_dbb) = link_basis_cell_second_partials(link_basis_span, a, b);
4963 for &z in &[-0.6, -0.2, 0.15, 0.5] {
4964 let u = a + b * z;
4965 let eta_a = link_basis_span.first_derivative(u);
4966 let eta_b = z * link_basis_span.first_derivative(u);
4967 let eta_aa = link_basis_span.second_derivative(u);
4968 let eta_ab = z * link_basis_span.second_derivative(u);
4969 let eta_bb = z * z * link_basis_span.second_derivative(u);
4970 assert!((polynomial_value(&dc_da, z) - eta_a).abs() < 1e-12);
4971 assert!((polynomial_value(&dc_db, z) - eta_b).abs() < 1e-12);
4972 assert!((polynomial_value(&dc_daa, z) - eta_aa).abs() < 1e-12);
4973 assert!((polynomial_value(&dc_dab, z) - eta_ab).abs() < 1e-12);
4974 assert!((polynomial_value(&dc_dbb, z) - eta_bb).abs() < 1e-12);
4975 }
4976 }
4977
4978 #[test]
4979 fn denested_third_partials_match_exact_span_derivatives() {
4980 let link_span = LocalSpanCubic {
4981 left: -0.6,
4982 right: 0.9,
4983 c0: -0.05,
4984 c1: 0.04,
4985 c2: -0.02,
4986 c3: 0.015,
4987 };
4988 let (dc_daaa, dc_daab, dc_dabb, dc_dbbb) = denested_cell_third_partials(link_span);
4989 let link_third = 6.0 * link_span.c3;
4990 for &z in &[-0.75, -0.4, -0.1, 0.2] {
4991 let eta_aaa = link_third;
4992 let eta_aab = z * link_third;
4993 let eta_abb = z * z * link_third;
4994 let eta_bbb = z * z * z * link_third;
4995 assert!((polynomial_value(&dc_daaa, z) - eta_aaa).abs() < 1e-12);
4996 assert!((polynomial_value(&dc_daab, z) - eta_aab).abs() < 1e-12);
4997 assert!((polynomial_value(&dc_dabb, z) - eta_abb).abs() < 1e-12);
4998 assert!((polynomial_value(&dc_dbbb, z) - eta_bbb).abs() < 1e-12);
4999 }
5000 }
5001
5002 #[test]
5003 fn link_basis_third_partials_match_exact_span_derivatives() {
5004 let link_basis_span = LocalSpanCubic {
5005 left: -0.5,
5006 right: 1.1,
5007 c0: -0.03,
5008 c1: 0.05,
5009 c2: -0.02,
5010 c3: 0.01,
5011 };
5012 let (dc_daaa, dc_daab, dc_dabb, dc_dbbb) = link_basis_cell_third_partials(link_basis_span);
5013 let link_third = 6.0 * link_basis_span.c3;
5014 for &z in &[-0.6, -0.2, 0.15, 0.5] {
5015 let eta_aaa = link_third;
5016 let eta_aab = z * link_third;
5017 let eta_abb = z * z * link_third;
5018 let eta_bbb = z * z * z * link_third;
5019 assert!((polynomial_value(&dc_daaa, z) - eta_aaa).abs() < 1e-12);
5020 assert!((polynomial_value(&dc_daab, z) - eta_aab).abs() < 1e-12);
5021 assert!((polynomial_value(&dc_dabb, z) - eta_abb).abs() < 1e-12);
5022 assert!((polynomial_value(&dc_dbbb, z) - eta_bbb).abs() < 1e-12);
5023 }
5024 }
5025
5026 #[test]
5027 fn branch_selection_uses_exact_polynomial_degree() {
5028 let affine = DenestedCubicCell {
5029 left: -1.0,
5030 right: 1.0,
5031 c0: 0.1,
5032 c1: -0.4,
5033 c2: 0.0,
5034 c3: 0.0,
5035 };
5036 let quartic = DenestedCubicCell {
5037 c2: 2e-4,
5038 c3: 0.0,
5039 ..affine
5040 };
5041 let sextic = DenestedCubicCell {
5042 c2: 2e-4,
5043 c3: -1e-13,
5044 ..affine
5045 };
5046 assert_eq!(branch_cell(affine).unwrap(), ExactCellBranch::Affine);
5047 assert_eq!(branch_cell(quartic).unwrap(), ExactCellBranch::Quartic);
5048 assert_eq!(branch_cell(sextic).unwrap(), ExactCellBranch::Sextic);
5049 }
5050
5051 #[test]
5052 fn affine_anchor_moments_match_whole_line_closed_forms() {
5053 let out = affine_anchor_moment_vector(0.0, 0.0, f64::NEG_INFINITY, f64::INFINITY, 4);
5054 let sqrt_2pi = (2.0 * std::f64::consts::PI).sqrt();
5062 assert!((out[0] - sqrt_2pi).abs() < 1e-12);
5063 assert!(out[1].abs() < 1e-12);
5064 assert!((out[2] - sqrt_2pi).abs() < 1e-12);
5065 }
5066
5067 #[test]
5068 fn affine_anchor_moments_match_shifted_gaussian_whole_line() {
5069 let alpha = 0.7;
5070 let beta = -0.4;
5071 let out = affine_anchor_moment_vector(alpha, beta, f64::NEG_INFINITY, f64::INFINITY, 4);
5072 let s = (1.0 + beta * beta).sqrt();
5073 let mu = -alpha * beta / (1.0 + beta * beta);
5074 let scale = (-alpha * alpha / (2.0 * s * s)).exp() / s;
5081 let sqrt_2pi = (2.0 * std::f64::consts::PI).sqrt();
5082 assert!((out[0] - scale * sqrt_2pi).abs() < 1e-12);
5083 assert!((out[1] - scale * sqrt_2pi * mu).abs() < 1e-12);
5084 assert!((out[2] - scale * sqrt_2pi * (mu * mu + 1.0 / (s * s))).abs() < 1e-10);
5085 }
5086
5087 #[test]
5088 fn quartic_recurrence_reduces_higher_moments() {
5089 let cell = DenestedCubicCell {
5090 left: -1.0,
5091 right: 0.9,
5092 c0: 0.2,
5093 c1: -0.3,
5094 c2: 0.18,
5095 c3: 0.0,
5096 };
5097 let exact = |k: usize| {
5098 simpson_integral(cell.left, cell.right, 2000, |z| {
5099 z.powi(k as i32) * (-cell.q(z)).exp()
5100 })
5101 };
5102 let reduced = reduce_quartic_moments(cell, [exact(0), exact(1), exact(2)], 6)
5103 .expect("quartic reduction");
5104 for k in 0..=6 {
5105 let target = exact(k);
5106 assert!(
5107 (reduced[k] - target).abs() < 1e-7,
5108 "quartic reduced moment M{k} mismatch: {} vs {}",
5109 reduced[k],
5110 target
5111 );
5112 }
5113 }
5114
5115 #[test]
5116 fn sextic_recurrence_reduces_higher_moments() {
5117 let cell = DenestedCubicCell {
5118 left: -0.8,
5119 right: 0.7,
5120 c0: -0.1,
5121 c1: 0.25,
5122 c2: -0.14,
5123 c3: 0.22,
5124 };
5125 let exact = |k: usize| {
5126 simpson_integral(cell.left, cell.right, 3000, |z| {
5127 z.powi(k as i32) * (-cell.q(z)).exp()
5128 })
5129 };
5130 let reduced =
5131 reduce_sextic_moments(cell, [exact(0), exact(1), exact(2), exact(3), exact(4)], 9)
5132 .expect("sextic reduction");
5133 for k in 0..=9 {
5134 let target = exact(k);
5135 assert!(
5136 (reduced[k] - target).abs() < 1e-7,
5137 "sextic reduced moment M{k} mismatch: {} vs {}",
5138 reduced[k],
5139 target
5140 );
5141 }
5142 }
5143
5144 #[test]
5145 fn ill_conditioned_sextic_recurrence_preserves_the_exact_cubic() {
5146 let cell = DenestedCubicCell {
5147 left: -1.0,
5148 right: 1.0,
5149 c0: 0.0,
5150 c1: 0.0,
5151 c2: 0.1,
5152 c3: 2.0e-10,
5153 };
5154 assert_eq!(branch_cell(cell).unwrap(), ExactCellBranch::Sextic);
5155
5156 let state = evaluate_cell_moments(cell, 9).expect("degenerate sextic cell");
5157 let reduced = reduce_sextic_moments(cell, [0.0; 5], 9)
5158 .expect("ill-conditioned recurrence must use exact transport");
5159 assert_eq!(reduced.as_slice(), state.moments.as_slice());
5160 let affine = evaluate_affine_cell_state(
5161 DenestedCubicCell {
5162 c2: 0.0,
5163 c3: 0.0,
5164 ..cell
5165 },
5166 9,
5167 )
5168 .expect("affine cell");
5169
5170 assert_eq!(state.branch, ExactCellBranch::Sextic);
5171 assert!(
5172 (state.moments[0] - affine.moments[0]).abs() > 1e-4,
5173 "degenerate sextic handling must not drop the nonzero c2 term"
5174 );
5175 }
5176
5177 #[test]
5178 fn moment_reduced_first_and_second_derivatives_match_numeric_integrals() {
5179 let cell = DenestedCubicCell {
5180 left: -0.9,
5181 right: 0.6,
5182 c0: 0.15,
5183 c1: -0.2,
5184 c2: 0.08,
5185 c3: 0.17,
5186 };
5187 let moments = reduce_sextic_moments(
5188 cell,
5189 [
5190 simpson_integral(cell.left, cell.right, 3000, |z| (-cell.q(z)).exp()),
5191 simpson_integral(cell.left, cell.right, 3000, |z| z * (-cell.q(z)).exp()),
5192 simpson_integral(cell.left, cell.right, 3000, |z| z * z * (-cell.q(z)).exp()),
5193 simpson_integral(cell.left, cell.right, 3000, |z| {
5194 z.powi(3) * (-cell.q(z)).exp()
5195 }),
5196 simpson_integral(cell.left, cell.right, 3000, |z| {
5197 z.powi(4) * (-cell.q(z)).exp()
5198 }),
5199 ],
5200 9,
5201 )
5202 .expect("reduced moments");
5203
5204 let r = [0.7, -0.1, 0.3];
5205 let s = [0.2, 0.5];
5206 let second = [0.4, -0.2, 0.1];
5207 let exact_first = cell_first_derivative_from_moments(&r, &moments).expect("first");
5208 let exact_second =
5209 cell_second_derivative_from_moments(cell, &r, &s, &second, &moments).expect("second");
5210
5211 let numeric_first = simpson_integral(cell.left, cell.right, 3000, |z| {
5212 polynomial_value(&r, z) * (-cell.q(z)).exp() / (2.0 * std::f64::consts::PI)
5213 });
5214 let numeric_second = simpson_integral(cell.left, cell.right, 3000, |z| {
5215 let eta = cell.eta(z);
5216 (polynomial_value(&second, z) - eta * polynomial_value(&r, z) * polynomial_value(&s, z))
5217 * (-cell.q(z)).exp()
5218 / (2.0 * std::f64::consts::PI)
5219 });
5220
5221 assert!((exact_first - numeric_first).abs() < 1e-7);
5222 assert!((exact_second - numeric_second).abs() < 1e-7);
5223 }
5224
5225 #[test]
5226 fn moment_reduced_third_derivative_matches_numeric_integral() {
5227 let cell = DenestedCubicCell {
5228 left: -0.85,
5229 right: 0.7,
5230 c0: -0.12,
5231 c1: 0.18,
5232 c2: 0.09,
5233 c3: -0.11,
5234 };
5235 let moments = evaluate_cell_moments(cell, 12).expect("cell moments");
5236 let r = [0.35, -0.12, 0.08];
5237 let s = [0.17, 0.09];
5238 let t = [-0.21, 0.14, -0.04];
5239 let rs = [0.11, -0.07, 0.05];
5240 let rt = [-0.06, 0.03];
5241 let st = [0.08, -0.02, 0.01];
5242 let rst = [0.04, -0.05, 0.02];
5243
5244 let exact_third = cell_third_derivative_from_moments(
5245 cell,
5246 &r,
5247 &s,
5248 &t,
5249 &rs,
5250 &rt,
5251 &st,
5252 &rst,
5253 &moments.moments,
5254 )
5255 .expect("third derivative");
5256 let numeric_third = simpson_integral(cell.left, cell.right, 4000, |z| {
5257 let eta = cell.eta(z);
5258 let rz = polynomial_value(&r, z);
5259 let sz = polynomial_value(&s, z);
5260 let tz = polynomial_value(&t, z);
5261 let rsz = polynomial_value(&rs, z);
5262 let rtz = polynomial_value(&rt, z);
5263 let stz = polynomial_value(&st, z);
5264 let rstz = polynomial_value(&rst, z);
5265 (rstz - eta * (rsz * tz + rtz * sz + stz * rz) + (eta * eta - 1.0) * rz * sz * tz)
5266 * (-cell.q(z)).exp()
5267 / (2.0 * std::f64::consts::PI)
5268 });
5269
5270 assert!((exact_third - numeric_third).abs() < 1e-7);
5271 }
5272
5273 #[test]
5274 fn moment_reduced_fourth_derivative_matches_numeric_integral() {
5275 let cell = DenestedCubicCell {
5276 left: -0.8,
5277 right: 0.65,
5278 c0: 0.11,
5279 c1: -0.22,
5280 c2: 0.07,
5281 c3: 0.13,
5282 };
5283 let moments = evaluate_cell_moments(cell, 16).expect("cell moments");
5284 let r = [0.21, -0.13, 0.06];
5285 let s = [-0.18, 0.04];
5286 let t = [0.09, 0.07, -0.03];
5287 let u = [-0.14, 0.05];
5288 let rs = [0.08, -0.03, 0.02];
5289 let rt = [-0.05, 0.01];
5290 let ru = [0.04, -0.02, 0.01];
5291 let st = [0.03, 0.02];
5292 let su = [-0.02, 0.05, -0.01];
5293 let tu = [0.07, -0.04];
5294 let rst = [0.03, -0.01, 0.02];
5295 let rsu = [-0.02, 0.04];
5296 let rtu = [0.01, 0.02, -0.01];
5297 let stu = [-0.03, 0.02];
5298 let rstu = [0.02, -0.01, 0.01];
5299
5300 let exact_fourth = cell_fourth_derivative_from_moments(
5301 cell,
5302 &r,
5303 &s,
5304 &t,
5305 &u,
5306 &rs,
5307 &rt,
5308 &ru,
5309 &st,
5310 &su,
5311 &tu,
5312 &rst,
5313 &rsu,
5314 &rtu,
5315 &stu,
5316 &rstu,
5317 &moments.moments,
5318 )
5319 .expect("fourth derivative");
5320 let numeric_fourth = simpson_integral(cell.left, cell.right, 5000, |z| {
5321 let eta = cell.eta(z);
5322 let rz = polynomial_value(&r, z);
5323 let sz = polynomial_value(&s, z);
5324 let tz = polynomial_value(&t, z);
5325 let uz = polynomial_value(&u, z);
5326 let rsz = polynomial_value(&rs, z);
5327 let rtz = polynomial_value(&rt, z);
5328 let ruz = polynomial_value(&ru, z);
5329 let stz = polynomial_value(&st, z);
5330 let suz = polynomial_value(&su, z);
5331 let tuz = polynomial_value(&tu, z);
5332 let rstz = polynomial_value(&rst, z);
5333 let rsuz = polynomial_value(&rsu, z);
5334 let rtuz = polynomial_value(&rtu, z);
5335 let stuz = polynomial_value(&stu, z);
5336 let rstuz = polynomial_value(&rstu, z);
5337 let linear =
5338 rstz * uz + rsuz * tz + rtuz * sz + stuz * rz + rsz * tuz + rtz * suz + ruz * stz;
5339 let quadratic = rsz * tz * uz
5340 + rtz * sz * uz
5341 + ruz * sz * tz
5342 + stz * rz * uz
5343 + suz * rz * tz
5344 + tuz * rz * sz;
5345 let quartic = rz * sz * tz * uz;
5346 (rstuz - eta * linear
5347 + (eta * eta - 1.0) * quadratic
5348 + (-eta * eta * eta + 3.0 * eta) * quartic)
5349 * (-cell.q(z)).exp()
5350 / (2.0 * std::f64::consts::PI)
5351 });
5352
5353 assert!((exact_fourth - numeric_fourth).abs() < 2e-7);
5354 }
5355
5356 #[test]
5357 fn denested_cell_parameter_derivatives_match_exact_integrands() {
5358 let score_span = LocalSpanCubic {
5359 left: -0.75,
5360 right: 0.25,
5361 c0: 0.08,
5362 c1: -0.03,
5363 c2: 0.02,
5364 c3: -0.01,
5365 };
5366 let link_span = LocalSpanCubic {
5367 left: -0.6,
5368 right: 0.9,
5369 c0: -0.05,
5370 c1: 0.04,
5371 c2: -0.02,
5372 c3: 0.015,
5373 };
5374 let a = 0.3;
5375 let b = -0.7;
5376 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
5377 let cell = DenestedCubicCell {
5378 left: score_span.left,
5379 right: score_span.right,
5380 c0: coeffs[0],
5381 c1: coeffs[1],
5382 c2: coeffs[2],
5383 c3: coeffs[3],
5384 };
5385 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
5386 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
5387 let (dc_daa, dc_dab, dc_dbb) = denested_cell_second_partials(score_span, link_span, a, b);
5388 let (dc_daaa, dc_daab, dc_dabb, dc_dbbb) = denested_cell_third_partials(link_span);
5389 let zero = [0.0; 4];
5390 let link_third = 6.0 * link_span.c3;
5391
5392 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
5393 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
5394 let eta_aa = |z: f64| link_span.second_derivative(a + b * z);
5395 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
5396 let eta_bb = |z: f64| z * z * link_span.second_derivative(a + b * z);
5397 let eta_aaa = |z: f64| link_third + 0.0 * z;
5398 let eta_aab = |z: f64| z * link_third;
5399 let eta_abb = |z: f64| z * z * link_third;
5400 let eta_bbb = |z: f64| z * z * z * link_third;
5401
5402 let exact_a = cell_first_derivative_from_moments(&dc_da, &state.moments).expect("a");
5403 let exact_b = cell_first_derivative_from_moments(&dc_db, &state.moments).expect("b");
5404 let exact_aa =
5405 cell_second_derivative_from_moments(cell, &dc_da, &dc_da, &dc_daa, &state.moments)
5406 .expect("aa");
5407 let exact_ab =
5408 cell_second_derivative_from_moments(cell, &dc_da, &dc_db, &dc_dab, &state.moments)
5409 .expect("ab");
5410 let exact_bb =
5411 cell_second_derivative_from_moments(cell, &dc_db, &dc_db, &dc_dbb, &state.moments)
5412 .expect("bb");
5413 let exact_aaa = cell_third_derivative_from_moments(
5414 cell,
5415 &dc_da,
5416 &dc_da,
5417 &dc_da,
5418 &dc_daa,
5419 &dc_daa,
5420 &dc_daa,
5421 &dc_daaa,
5422 &state.moments,
5423 )
5424 .expect("aaa");
5425 let exact_aab = cell_third_derivative_from_moments(
5426 cell,
5427 &dc_da,
5428 &dc_da,
5429 &dc_db,
5430 &dc_daa,
5431 &dc_dab,
5432 &dc_dab,
5433 &dc_daab,
5434 &state.moments,
5435 )
5436 .expect("aab");
5437 let exact_abb = cell_third_derivative_from_moments(
5438 cell,
5439 &dc_da,
5440 &dc_db,
5441 &dc_db,
5442 &dc_dab,
5443 &dc_dab,
5444 &dc_dbb,
5445 &dc_dabb,
5446 &state.moments,
5447 )
5448 .expect("abb");
5449 let exact_bbb = cell_third_derivative_from_moments(
5450 cell,
5451 &dc_db,
5452 &dc_db,
5453 &dc_db,
5454 &dc_dbb,
5455 &dc_dbb,
5456 &dc_dbb,
5457 &dc_dbbb,
5458 &state.moments,
5459 )
5460 .expect("bbb");
5461 let exact_aaaa = cell_fourth_derivative_from_moments(
5462 cell,
5463 &dc_da,
5464 &dc_da,
5465 &dc_da,
5466 &dc_da,
5467 &dc_daa,
5468 &dc_daa,
5469 &dc_daa,
5470 &dc_daa,
5471 &dc_daa,
5472 &dc_daa,
5473 &dc_daaa,
5474 &dc_daaa,
5475 &dc_daaa,
5476 &dc_daaa,
5477 &zero,
5478 &state.moments,
5479 )
5480 .expect("aaaa");
5481 let exact_aaab = cell_fourth_derivative_from_moments(
5482 cell,
5483 &dc_da,
5484 &dc_da,
5485 &dc_da,
5486 &dc_db,
5487 &dc_daa,
5488 &dc_daa,
5489 &dc_dab,
5490 &dc_daa,
5491 &dc_dab,
5492 &dc_dab,
5493 &dc_daaa,
5494 &dc_daab,
5495 &dc_daab,
5496 &dc_daab,
5497 &zero,
5498 &state.moments,
5499 )
5500 .expect("aaab");
5501 let exact_aabb = cell_fourth_derivative_from_moments(
5502 cell,
5503 &dc_da,
5504 &dc_da,
5505 &dc_db,
5506 &dc_db,
5507 &dc_daa,
5508 &dc_dab,
5509 &dc_dab,
5510 &dc_dab,
5511 &dc_dab,
5512 &dc_dbb,
5513 &dc_daab,
5514 &dc_daab,
5515 &dc_dabb,
5516 &dc_dabb,
5517 &zero,
5518 &state.moments,
5519 )
5520 .expect("aabb");
5521 let exact_abbb = cell_fourth_derivative_from_moments(
5522 cell,
5523 &dc_da,
5524 &dc_db,
5525 &dc_db,
5526 &dc_db,
5527 &dc_dab,
5528 &dc_dab,
5529 &dc_dab,
5530 &dc_dbb,
5531 &dc_dbb,
5532 &dc_dbb,
5533 &dc_dabb,
5534 &dc_dabb,
5535 &dc_dabb,
5536 &dc_dbbb,
5537 &zero,
5538 &state.moments,
5539 )
5540 .expect("abbb");
5541 let exact_bbbb = cell_fourth_derivative_from_moments(
5542 cell,
5543 &dc_db,
5544 &dc_db,
5545 &dc_db,
5546 &dc_db,
5547 &dc_dbb,
5548 &dc_dbb,
5549 &dc_dbb,
5550 &dc_dbb,
5551 &dc_dbb,
5552 &dc_dbb,
5553 &dc_dbbb,
5554 &dc_dbbb,
5555 &dc_dbbb,
5556 &dc_dbbb,
5557 &zero,
5558 &state.moments,
5559 )
5560 .expect("bbbb");
5561
5562 let numeric_a = simpson_integral(cell.left, cell.right, 5000, |z| {
5563 eta_a(z) * (-cell.q(z)).exp() * INV_TWO_PI
5564 });
5565 let numeric_b = simpson_integral(cell.left, cell.right, 5000, |z| {
5566 eta_b(z) * (-cell.q(z)).exp() * INV_TWO_PI
5567 });
5568 let numeric_aa = simpson_integral(cell.left, cell.right, 5000, |z| {
5569 (eta_aa(z) - cell.eta(z) * eta_a(z) * eta_a(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5570 });
5571 let numeric_ab = simpson_integral(cell.left, cell.right, 5000, |z| {
5572 (eta_ab(z) - cell.eta(z) * eta_a(z) * eta_b(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5573 });
5574 let numeric_bb = simpson_integral(cell.left, cell.right, 5000, |z| {
5575 (eta_bb(z) - cell.eta(z) * eta_b(z) * eta_b(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5576 });
5577 let numeric_aaa = simpson_integral(cell.left, cell.right, 5000, |z| {
5578 let eta = cell.eta(z);
5579 (eta_aaa(z) - 3.0 * eta * eta_aa(z) * eta_a(z) + (eta * eta - 1.0) * eta_a(z).powi(3))
5580 * (-cell.q(z)).exp()
5581 * INV_TWO_PI
5582 });
5583 let numeric_aab = simpson_integral(cell.left, cell.right, 5000, |z| {
5584 let eta = cell.eta(z);
5585 let a_z = eta_a(z);
5586 let b_z = eta_b(z);
5587 (eta_aab(z) - eta * (eta_aa(z) * b_z + 2.0 * eta_ab(z) * a_z)
5588 + (eta * eta - 1.0) * a_z * a_z * b_z)
5589 * (-cell.q(z)).exp()
5590 * INV_TWO_PI
5591 });
5592 let numeric_abb = simpson_integral(cell.left, cell.right, 5000, |z| {
5593 let eta = cell.eta(z);
5594 let a_z = eta_a(z);
5595 let b_z = eta_b(z);
5596 (eta_abb(z) - eta * (2.0 * eta_ab(z) * b_z + eta_bb(z) * a_z)
5597 + (eta * eta - 1.0) * a_z * b_z * b_z)
5598 * (-cell.q(z)).exp()
5599 * INV_TWO_PI
5600 });
5601 let numeric_bbb = simpson_integral(cell.left, cell.right, 5000, |z| {
5602 let eta = cell.eta(z);
5603 (eta_bbb(z) - 3.0 * eta * eta_bb(z) * eta_b(z) + (eta * eta - 1.0) * eta_b(z).powi(3))
5604 * (-cell.q(z)).exp()
5605 * INV_TWO_PI
5606 });
5607 let numeric_aaaa = simpson_integral(cell.left, cell.right, 5000, |z| {
5608 let eta = cell.eta(z);
5609 let eta_a_z = eta_a(z);
5610 let eta_aa_z = eta_aa(z);
5611 let eta_aaa_z = eta_aaa(z);
5612 (-eta * (4.0 * eta_aaa_z * eta_a_z + 3.0 * eta_aa_z * eta_aa_z)
5613 + (eta * eta - 1.0) * (6.0 * eta_aa_z * eta_a_z * eta_a_z)
5614 + (-eta * eta * eta + 3.0 * eta) * eta_a_z.powi(4))
5615 * (-cell.q(z)).exp()
5616 * INV_TWO_PI
5617 });
5618 let numeric_aaab = 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 aaa_z = eta_aaa(z);
5625 let aab_z = eta_aab(z);
5626 (-eta * (aaa_z * b_z + 3.0 * aab_z * a_z + 3.0 * aa_z * ab_z)
5627 + (eta * eta - 1.0) * (3.0 * aa_z * a_z * b_z + 3.0 * ab_z * a_z * a_z)
5628 + (-eta * eta * eta + 3.0 * eta) * a_z.powi(3) * b_z)
5629 * (-cell.q(z)).exp()
5630 * INV_TWO_PI
5631 });
5632 let numeric_aabb = simpson_integral(cell.left, cell.right, 5000, |z| {
5633 let eta = cell.eta(z);
5634 let a_z = eta_a(z);
5635 let b_z = eta_b(z);
5636 let aa_z = eta_aa(z);
5637 let ab_z = eta_ab(z);
5638 let bb_z = eta_bb(z);
5639 let aab_z = eta_aab(z);
5640 let abb_z = eta_abb(z);
5641 (-eta * (2.0 * aab_z * b_z + 2.0 * abb_z * a_z + aa_z * bb_z + 2.0 * ab_z * ab_z)
5642 + (eta * eta - 1.0)
5643 * (aa_z * b_z * b_z + 4.0 * ab_z * a_z * b_z + bb_z * a_z * a_z)
5644 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * b_z * b_z)
5645 * (-cell.q(z)).exp()
5646 * INV_TWO_PI
5647 });
5648 let numeric_abbb = simpson_integral(cell.left, cell.right, 5000, |z| {
5649 let eta = cell.eta(z);
5650 let a_z = eta_a(z);
5651 let b_z = eta_b(z);
5652 let ab_z = eta_ab(z);
5653 let bb_z = eta_bb(z);
5654 let abb_z = eta_abb(z);
5655 let bbb_z = eta_bbb(z);
5656 (-eta * (3.0 * abb_z * b_z + bbb_z * a_z + 3.0 * ab_z * bb_z)
5657 + (eta * eta - 1.0) * (3.0 * ab_z * b_z * b_z + 3.0 * bb_z * a_z * b_z)
5658 + (-eta * eta * eta + 3.0 * eta) * a_z * b_z.powi(3))
5659 * (-cell.q(z)).exp()
5660 * INV_TWO_PI
5661 });
5662 let numeric_bbbb = simpson_integral(cell.left, cell.right, 5000, |z| {
5663 let eta = cell.eta(z);
5664 let eta_b_z = eta_b(z);
5665 let eta_bb_z = eta_bb(z);
5666 let eta_bbb_z = eta_bbb(z);
5667 (-eta * (4.0 * eta_bbb_z * eta_b_z + 3.0 * eta_bb_z * eta_bb_z)
5668 + (eta * eta - 1.0) * (6.0 * eta_bb_z * eta_b_z * eta_b_z)
5669 + (-eta * eta * eta + 3.0 * eta) * eta_b_z.powi(4))
5670 * (-cell.q(z)).exp()
5671 * INV_TWO_PI
5672 });
5673
5674 assert!((exact_a - numeric_a).abs() < 1e-8);
5675 assert!((exact_b - numeric_b).abs() < 1e-8);
5676 assert!((exact_aa - numeric_aa).abs() < 1e-8);
5677 assert!((exact_ab - numeric_ab).abs() < 1e-8);
5678 assert!((exact_bb - numeric_bb).abs() < 1e-8);
5679 assert!((exact_aaa - numeric_aaa).abs() < 2e-7);
5680 assert!((exact_aab - numeric_aab).abs() < 2e-7);
5681 assert!((exact_abb - numeric_abb).abs() < 2e-7);
5682 assert!((exact_bbb - numeric_bbb).abs() < 2e-7);
5683 assert!((exact_aaaa - numeric_aaaa).abs() < 2e-6);
5684 assert!((exact_aaab - numeric_aaab).abs() < 2e-6);
5685 assert!((exact_aabb - numeric_aabb).abs() < 2e-6);
5686 assert!((exact_abbb - numeric_abbb).abs() < 2e-6);
5687 assert!((exact_bbbb - numeric_bbbb).abs() < 2e-6);
5688 }
5689
5690 #[test]
5691 fn link_basis_cell_derivatives_match_exact_integrands() {
5692 let score_span = LocalSpanCubic {
5693 left: -0.75,
5694 right: 0.25,
5695 c0: 0.08,
5696 c1: -0.03,
5697 c2: 0.02,
5698 c3: -0.01,
5699 };
5700 let link_span = LocalSpanCubic {
5701 left: -0.6,
5702 right: 0.9,
5703 c0: -0.05,
5704 c1: 0.04,
5705 c2: -0.02,
5706 c3: 0.015,
5707 };
5708 let link_basis_span = LocalSpanCubic {
5709 left: -0.6,
5710 right: 0.9,
5711 c0: 0.02,
5712 c1: -0.01,
5713 c2: 0.03,
5714 c3: -0.02,
5715 };
5716 let a = 0.3;
5717 let b = -0.7;
5718 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
5719 let cell = DenestedCubicCell {
5720 left: score_span.left,
5721 right: score_span.right,
5722 c0: coeffs[0],
5723 c1: coeffs[1],
5724 c2: coeffs[2],
5725 c3: coeffs[3],
5726 };
5727 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
5728 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
5729 let second_partials = denested_cell_second_partials(score_span, link_span, a, b);
5730 let dc_daa = second_partials.0;
5731 let dc_dab = second_partials.1;
5732 let dc_dbb = second_partials.2;
5733 let denested_third = denested_cell_third_partials(link_span);
5734 let dc_daaa = denested_third.0;
5735 let dc_dbbb = denested_third.3;
5736
5737 let coeff_w = link_basis_cell_coefficients(link_basis_span, a, b);
5738 let (coeff_aw, coeff_bw) = link_basis_cell_coefficient_partials(link_basis_span, a, b);
5739 let (coeff_aaw, coeff_abw, coeff_bbw) =
5740 link_basis_cell_second_partials(link_basis_span, a, b);
5741 let link_basis_third = link_basis_cell_third_partials(link_basis_span);
5742 let coeff_aaaw = link_basis_third.0;
5743 let coeff_bbbw = link_basis_third.3;
5744 let zero = [0.0; 4];
5745 let basis_third = 6.0 * link_basis_span.c3;
5746
5747 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
5748 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
5749 let eta_aa = |z: f64| link_span.second_derivative(a + b * z);
5750 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
5751 let eta_bb = |z: f64| z * z * link_span.second_derivative(a + b * z);
5752 let eta_w = |z: f64| link_basis_span.evaluate(a + b * z);
5753 let eta_aw = |z: f64| link_basis_span.first_derivative(a + b * z);
5754 let eta_bw = |z: f64| z * link_basis_span.first_derivative(a + b * z);
5755 let eta_aaw = |z: f64| link_basis_span.second_derivative(a + b * z);
5756 let eta_abw = |z: f64| z * link_basis_span.second_derivative(a + b * z);
5757 let eta_bbw = |z: f64| z * z * link_basis_span.second_derivative(a + b * z);
5758 let eta_aaaw = |z: f64| basis_third + 0.0 * z;
5759 let eta_bbbw = |z: f64| z * z * z * basis_third;
5760
5761 let exact_w = cell_first_derivative_from_moments(&coeff_w, &state.moments).expect("w");
5762 let exact_aw =
5763 cell_second_derivative_from_moments(cell, &dc_da, &coeff_w, &coeff_aw, &state.moments)
5764 .expect("aw");
5765 let exact_bw =
5766 cell_second_derivative_from_moments(cell, &dc_db, &coeff_w, &coeff_bw, &state.moments)
5767 .expect("bw");
5768 let exact_ww =
5769 cell_second_derivative_from_moments(cell, &coeff_w, &coeff_w, &zero, &state.moments)
5770 .expect("ww");
5771 let exact_aaw = cell_third_derivative_from_moments(
5772 cell,
5773 &dc_da,
5774 &dc_da,
5775 &coeff_w,
5776 &dc_daa,
5777 &coeff_aw,
5778 &coeff_aw,
5779 &coeff_aaw,
5780 &state.moments,
5781 )
5782 .expect("aaw");
5783 let exact_abw = cell_third_derivative_from_moments(
5784 cell,
5785 &dc_da,
5786 &dc_db,
5787 &coeff_w,
5788 &dc_dab,
5789 &coeff_aw,
5790 &coeff_bw,
5791 &coeff_abw,
5792 &state.moments,
5793 )
5794 .expect("abw");
5795 let exact_bbw = cell_third_derivative_from_moments(
5796 cell,
5797 &dc_db,
5798 &dc_db,
5799 &coeff_w,
5800 &dc_dbb,
5801 &coeff_bw,
5802 &coeff_bw,
5803 &coeff_bbw,
5804 &state.moments,
5805 )
5806 .expect("bbw");
5807 let exact_www = cell_third_derivative_from_moments(
5808 cell,
5809 &coeff_w,
5810 &coeff_w,
5811 &coeff_w,
5812 &zero,
5813 &zero,
5814 &zero,
5815 &zero,
5816 &state.moments,
5817 )
5818 .expect("www");
5819 let exact_aaaw = cell_fourth_derivative_from_moments(
5820 cell,
5821 &dc_da,
5822 &dc_da,
5823 &dc_da,
5824 &coeff_w,
5825 &dc_daa,
5826 &dc_daa,
5827 &coeff_aw,
5828 &dc_daa,
5829 &coeff_aw,
5830 &coeff_aw,
5831 &dc_daaa,
5832 &coeff_aaw,
5833 &coeff_aaw,
5834 &coeff_aaw,
5835 &coeff_aaaw,
5836 &state.moments,
5837 )
5838 .expect("aaaw");
5839 let exact_aaww = cell_fourth_derivative_from_moments(
5840 cell,
5841 &dc_da,
5842 &dc_da,
5843 &coeff_w,
5844 &coeff_w,
5845 &dc_daa,
5846 &coeff_aw,
5847 &coeff_aw,
5848 &coeff_aw,
5849 &coeff_aw,
5850 &zero,
5851 &coeff_aaw,
5852 &coeff_aaw,
5853 &zero,
5854 &zero,
5855 &zero,
5856 &state.moments,
5857 )
5858 .expect("aaww");
5859 let exact_abww = cell_fourth_derivative_from_moments(
5860 cell,
5861 &dc_da,
5862 &dc_db,
5863 &coeff_w,
5864 &coeff_w,
5865 &dc_dab,
5866 &coeff_aw,
5867 &coeff_aw,
5868 &coeff_bw,
5869 &coeff_bw,
5870 &zero,
5871 &coeff_abw,
5872 &coeff_abw,
5873 &zero,
5874 &zero,
5875 &zero,
5876 &state.moments,
5877 )
5878 .expect("abww");
5879 let exact_bbww = cell_fourth_derivative_from_moments(
5880 cell,
5881 &dc_db,
5882 &dc_db,
5883 &coeff_w,
5884 &coeff_w,
5885 &dc_dbb,
5886 &coeff_bw,
5887 &coeff_bw,
5888 &coeff_bw,
5889 &coeff_bw,
5890 &zero,
5891 &coeff_bbw,
5892 &coeff_bbw,
5893 &zero,
5894 &zero,
5895 &zero,
5896 &state.moments,
5897 )
5898 .expect("bbww");
5899 let exact_bbbw = cell_fourth_derivative_from_moments(
5900 cell,
5901 &dc_db,
5902 &dc_db,
5903 &dc_db,
5904 &coeff_w,
5905 &dc_dbb,
5906 &dc_dbb,
5907 &coeff_bw,
5908 &dc_dbb,
5909 &coeff_bw,
5910 &coeff_bw,
5911 &dc_dbbb,
5912 &coeff_bbw,
5913 &coeff_bbw,
5914 &coeff_bbw,
5915 &coeff_bbbw,
5916 &state.moments,
5917 )
5918 .expect("bbbw");
5919 let exact_wwww = cell_fourth_derivative_from_moments(
5920 cell,
5921 &coeff_w,
5922 &coeff_w,
5923 &coeff_w,
5924 &coeff_w,
5925 &zero,
5926 &zero,
5927 &zero,
5928 &zero,
5929 &zero,
5930 &zero,
5931 &zero,
5932 &zero,
5933 &zero,
5934 &zero,
5935 &zero,
5936 &state.moments,
5937 )
5938 .expect("wwww");
5939
5940 let numeric_w = simpson_integral(cell.left, cell.right, 5000, |z| {
5941 eta_w(z) * (-cell.q(z)).exp() * INV_TWO_PI
5942 });
5943 let numeric_aw = simpson_integral(cell.left, cell.right, 5000, |z| {
5944 (eta_aw(z) - cell.eta(z) * eta_a(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5945 });
5946 let numeric_bw = simpson_integral(cell.left, cell.right, 5000, |z| {
5947 (eta_bw(z) - cell.eta(z) * eta_b(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5948 });
5949 let numeric_ww = simpson_integral(cell.left, cell.right, 5000, |z| {
5950 (-cell.eta(z) * eta_w(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
5951 });
5952 let numeric_aaw = simpson_integral(cell.left, cell.right, 5000, |z| {
5953 let eta = cell.eta(z);
5954 let w_z = eta_w(z);
5955 let a_z = eta_a(z);
5956 (eta_aaw(z) - eta * (eta_aa(z) * w_z + 2.0 * eta_aw(z) * a_z)
5957 + (eta * eta - 1.0) * a_z * a_z * w_z)
5958 * (-cell.q(z)).exp()
5959 * INV_TWO_PI
5960 });
5961 let numeric_abw = simpson_integral(cell.left, cell.right, 5000, |z| {
5962 let eta = cell.eta(z);
5963 let w_z = eta_w(z);
5964 let a_z = eta_a(z);
5965 let b_z = eta_b(z);
5966 (eta_abw(z) - eta * (eta_ab(z) * w_z + eta_aw(z) * b_z + eta_bw(z) * a_z)
5967 + (eta * eta - 1.0) * a_z * b_z * w_z)
5968 * (-cell.q(z)).exp()
5969 * INV_TWO_PI
5970 });
5971 let numeric_bbw = simpson_integral(cell.left, cell.right, 5000, |z| {
5972 let eta = cell.eta(z);
5973 let w_z = eta_w(z);
5974 let b_z = eta_b(z);
5975 (eta_bbw(z) - eta * (eta_bb(z) * w_z + 2.0 * eta_bw(z) * b_z)
5976 + (eta * eta - 1.0) * b_z * b_z * w_z)
5977 * (-cell.q(z)).exp()
5978 * INV_TWO_PI
5979 });
5980 let numeric_www = simpson_integral(cell.left, cell.right, 5000, |z| {
5981 let eta = cell.eta(z);
5982 let w_z = eta_w(z);
5983 ((eta * eta - 1.0) * w_z * w_z * w_z) * (-cell.q(z)).exp() * INV_TWO_PI
5984 });
5985 let numeric_aaaw = simpson_integral(cell.left, cell.right, 5000, |z| {
5986 let eta = cell.eta(z);
5987 let a_z = eta_a(z);
5988 let w_z = eta_w(z);
5989 let aa_z = eta_aa(z);
5990 let aw_z = eta_aw(z);
5991 (eta_aaaw(z)
5992 - eta * ((dc_daaa[0] + 0.0 * z) * w_z + 3.0 * eta_aaw(z) * a_z + 3.0 * aa_z * aw_z)
5993 + (eta * eta - 1.0) * (3.0 * aa_z * a_z * w_z + 3.0 * aw_z * a_z * a_z)
5994 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * a_z * w_z)
5995 * (-cell.q(z)).exp()
5996 * INV_TWO_PI
5997 });
5998 let numeric_aaww = simpson_integral(cell.left, cell.right, 5000, |z| {
5999 let eta = cell.eta(z);
6000 let a_z = eta_a(z);
6001 let w_z = eta_w(z);
6002 let aw_z = eta_aw(z);
6003 (-(2.0 * eta * (eta_aaw(z) * w_z + aw_z * aw_z))
6004 + (eta * eta - 1.0) * (eta_aa(z) * w_z * w_z + 4.0 * aw_z * a_z * w_z)
6005 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * w_z * w_z)
6006 * (-cell.q(z)).exp()
6007 * INV_TWO_PI
6008 });
6009 let numeric_abww = simpson_integral(cell.left, cell.right, 5000, |z| {
6010 let eta = cell.eta(z);
6011 let a_z = eta_a(z);
6012 let b_z = eta_b(z);
6013 let w_z = eta_w(z);
6014 let aw_z = eta_aw(z);
6015 let bw_z = eta_bw(z);
6016 (-(2.0 * eta * (eta_abw(z) * w_z + aw_z * bw_z))
6017 + (eta * eta - 1.0)
6018 * (eta_ab(z) * w_z * w_z + 2.0 * aw_z * b_z * w_z + 2.0 * bw_z * a_z * w_z)
6019 + (-eta * eta * eta + 3.0 * eta) * a_z * b_z * w_z * w_z)
6020 * (-cell.q(z)).exp()
6021 * INV_TWO_PI
6022 });
6023 let numeric_bbww = simpson_integral(cell.left, cell.right, 5000, |z| {
6024 let eta = cell.eta(z);
6025 let b_z = eta_b(z);
6026 let w_z = eta_w(z);
6027 let bw_z = eta_bw(z);
6028 (-(2.0 * eta * (eta_bbw(z) * w_z + bw_z * bw_z))
6029 + (eta * eta - 1.0) * (eta_bb(z) * w_z * w_z + 4.0 * bw_z * b_z * w_z)
6030 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * w_z * w_z)
6031 * (-cell.q(z)).exp()
6032 * INV_TWO_PI
6033 });
6034 let numeric_bbbw = simpson_integral(cell.left, cell.right, 5000, |z| {
6035 let eta = cell.eta(z);
6036 let b_z = eta_b(z);
6037 let w_z = eta_w(z);
6038 let bb_z = eta_bb(z);
6039 let bw_z = eta_bw(z);
6040 (eta_bbbw(z)
6041 - eta
6042 * ((dc_dbbb[3] * z * z * z) * w_z + 3.0 * eta_bbw(z) * b_z + 3.0 * bb_z * bw_z)
6043 + (eta * eta - 1.0) * (3.0 * bb_z * b_z * w_z + 3.0 * bw_z * b_z * b_z)
6044 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * b_z * w_z)
6045 * (-cell.q(z)).exp()
6046 * INV_TWO_PI
6047 });
6048 let numeric_wwww = simpson_integral(cell.left, cell.right, 5000, |z| {
6049 let eta = cell.eta(z);
6050 let w_z = eta_w(z);
6051 ((-eta * eta * eta + 3.0 * eta) * w_z * w_z * w_z * w_z)
6052 * (-cell.q(z)).exp()
6053 * INV_TWO_PI
6054 });
6055
6056 assert!((exact_w - numeric_w).abs() < 1e-8);
6057 assert!((exact_aw - numeric_aw).abs() < 1e-7);
6058 assert!((exact_bw - numeric_bw).abs() < 1e-7);
6059 assert!((exact_ww - numeric_ww).abs() < 1e-7);
6060 assert!((exact_aaw - numeric_aaw).abs() < 2e-6);
6061 assert!((exact_abw - numeric_abw).abs() < 2e-6);
6062 assert!((exact_bbw - numeric_bbw).abs() < 2e-6);
6063 assert!((exact_www - numeric_www).abs() < 2e-6);
6064 assert!((exact_aaaw - numeric_aaaw).abs() < 3e-6);
6065 assert!((exact_aaww - numeric_aaww).abs() < 3e-6);
6066 assert!((exact_abww - numeric_abww).abs() < 3e-6);
6067 assert!((exact_bbww - numeric_bbww).abs() < 3e-6);
6068 assert!((exact_bbbw - numeric_bbbw).abs() < 3e-6);
6069 assert!((exact_wwww - numeric_wwww).abs() < 3e-6);
6070 }
6071
6072 #[test]
6073 fn score_basis_cell_derivatives_match_exact_integrands() {
6074 let score_span = LocalSpanCubic {
6075 left: -0.75,
6076 right: 0.25,
6077 c0: 0.08,
6078 c1: -0.03,
6079 c2: 0.02,
6080 c3: -0.01,
6081 };
6082 let score_basis_span = LocalSpanCubic {
6083 left: -0.75,
6084 right: 0.25,
6085 c0: -0.04,
6086 c1: 0.06,
6087 c2: -0.01,
6088 c3: 0.02,
6089 };
6090 let link_span = LocalSpanCubic {
6091 left: -0.6,
6092 right: 0.9,
6093 c0: -0.05,
6094 c1: 0.04,
6095 c2: -0.02,
6096 c3: 0.015,
6097 };
6098 let a = 0.3;
6099 let b = -0.7;
6100 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
6101 let cell = DenestedCubicCell {
6102 left: score_span.left,
6103 right: score_span.right,
6104 c0: coeffs[0],
6105 c1: coeffs[1],
6106 c2: coeffs[2],
6107 c3: coeffs[3],
6108 };
6109 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
6110 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
6111 let second_partials = denested_cell_second_partials(score_span, link_span, a, b);
6112 let dc_daa = second_partials.0;
6113 let dc_dab = second_partials.1;
6114 let dc_dbb = second_partials.2;
6115 let denested_third = denested_cell_third_partials(link_span);
6116 let dc_dbbb = denested_third.3;
6117
6118 let coeff_h = score_basis_cell_coefficients(score_basis_span, b);
6119 let coeff_bh = score_basis_cell_coefficients(score_basis_span, 1.0);
6120 let zero = [0.0; 4];
6121
6122 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
6123 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
6124 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
6125 let eta_bb = |z: f64| z * z * link_span.second_derivative(a + b * z);
6126 let eta_h = |z: f64| b * score_basis_span.evaluate(z);
6127 let eta_bh = |z: f64| score_basis_span.evaluate(z);
6128
6129 let exact_h = cell_first_derivative_from_moments(&coeff_h, &state.moments).expect("h");
6130 let exact_ah =
6131 cell_second_derivative_from_moments(cell, &dc_da, &coeff_h, &zero, &state.moments)
6132 .expect("ah");
6133 let exact_bh =
6134 cell_second_derivative_from_moments(cell, &dc_db, &coeff_h, &coeff_bh, &state.moments)
6135 .expect("bh");
6136 let exact_hh =
6137 cell_second_derivative_from_moments(cell, &coeff_h, &coeff_h, &zero, &state.moments)
6138 .expect("hh");
6139 let exact_abh = cell_third_derivative_from_moments(
6140 cell,
6141 &dc_da,
6142 &dc_db,
6143 &coeff_h,
6144 &dc_dab,
6145 &zero,
6146 &coeff_bh,
6147 &zero,
6148 &state.moments,
6149 )
6150 .expect("abh");
6151 let exact_bbh = cell_third_derivative_from_moments(
6152 cell,
6153 &dc_db,
6154 &dc_db,
6155 &coeff_h,
6156 &dc_dbb,
6157 &coeff_bh,
6158 &coeff_bh,
6159 &zero,
6160 &state.moments,
6161 )
6162 .expect("bbh");
6163 let exact_bhh = cell_third_derivative_from_moments(
6164 cell,
6165 &dc_db,
6166 &coeff_h,
6167 &coeff_h,
6168 &coeff_bh,
6169 &coeff_bh,
6170 &zero,
6171 &zero,
6172 &state.moments,
6173 )
6174 .expect("bhh");
6175 let exact_hhh = cell_third_derivative_from_moments(
6176 cell,
6177 &coeff_h,
6178 &coeff_h,
6179 &coeff_h,
6180 &zero,
6181 &zero,
6182 &zero,
6183 &zero,
6184 &state.moments,
6185 )
6186 .expect("hhh");
6187 let exact_bbbh = cell_fourth_derivative_from_moments(
6188 cell,
6189 &dc_db,
6190 &dc_db,
6191 &dc_db,
6192 &coeff_h,
6193 &dc_dbb,
6194 &dc_dbb,
6195 &coeff_bh,
6196 &dc_dbb,
6197 &coeff_bh,
6198 &coeff_bh,
6199 &dc_dbbb,
6200 &zero,
6201 &zero,
6202 &zero,
6203 &zero,
6204 &state.moments,
6205 )
6206 .expect("bbbh");
6207 let exact_aahh = cell_fourth_derivative_from_moments(
6208 cell,
6209 &dc_da,
6210 &dc_da,
6211 &coeff_h,
6212 &coeff_h,
6213 &dc_daa,
6214 &zero,
6215 &zero,
6216 &zero,
6217 &zero,
6218 &zero,
6219 &zero,
6220 &zero,
6221 &zero,
6222 &zero,
6223 &zero,
6224 &state.moments,
6225 )
6226 .expect("aahh");
6227 let exact_abhh = cell_fourth_derivative_from_moments(
6228 cell,
6229 &dc_da,
6230 &dc_db,
6231 &coeff_h,
6232 &coeff_h,
6233 &dc_dab,
6234 &zero,
6235 &zero,
6236 &coeff_bh,
6237 &coeff_bh,
6238 &zero,
6239 &zero,
6240 &zero,
6241 &zero,
6242 &zero,
6243 &zero,
6244 &state.moments,
6245 )
6246 .expect("abhh");
6247 let exact_bbhh = cell_fourth_derivative_from_moments(
6248 cell,
6249 &dc_db,
6250 &dc_db,
6251 &coeff_h,
6252 &coeff_h,
6253 &dc_dbb,
6254 &coeff_bh,
6255 &coeff_bh,
6256 &coeff_bh,
6257 &coeff_bh,
6258 &zero,
6259 &zero,
6260 &zero,
6261 &zero,
6262 &zero,
6263 &zero,
6264 &state.moments,
6265 )
6266 .expect("bbhh");
6267 let exact_bhhh = cell_fourth_derivative_from_moments(
6268 cell,
6269 &dc_db,
6270 &coeff_h,
6271 &coeff_h,
6272 &coeff_h,
6273 &coeff_bh,
6274 &coeff_bh,
6275 &coeff_bh,
6276 &zero,
6277 &zero,
6278 &zero,
6279 &zero,
6280 &zero,
6281 &zero,
6282 &zero,
6283 &zero,
6284 &state.moments,
6285 )
6286 .expect("bhhh");
6287 let exact_hhhh = cell_fourth_derivative_from_moments(
6288 cell,
6289 &coeff_h,
6290 &coeff_h,
6291 &coeff_h,
6292 &coeff_h,
6293 &zero,
6294 &zero,
6295 &zero,
6296 &zero,
6297 &zero,
6298 &zero,
6299 &zero,
6300 &zero,
6301 &zero,
6302 &zero,
6303 &zero,
6304 &state.moments,
6305 )
6306 .expect("hhhh");
6307
6308 let numeric_h = simpson_integral(cell.left, cell.right, 5000, |z| {
6309 eta_h(z) * (-cell.q(z)).exp() * INV_TWO_PI
6310 });
6311 let numeric_ah = simpson_integral(cell.left, cell.right, 5000, |z| {
6312 (-cell.eta(z) * eta_a(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6313 });
6314 let numeric_bh = simpson_integral(cell.left, cell.right, 5000, |z| {
6315 (eta_bh(z) - cell.eta(z) * eta_b(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6316 });
6317 let numeric_hh = simpson_integral(cell.left, cell.right, 5000, |z| {
6318 (-cell.eta(z) * eta_h(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6319 });
6320 let numeric_abh = simpson_integral(cell.left, cell.right, 5000, |z| {
6321 let eta = cell.eta(z);
6322 (-(eta * (eta_ab(z) * eta_h(z) + eta_bh(z) * eta_a(z)))
6323 + (eta * eta - 1.0) * eta_a(z) * eta_b(z) * eta_h(z))
6324 * (-cell.q(z)).exp()
6325 * INV_TWO_PI
6326 });
6327 let numeric_bbh = simpson_integral(cell.left, cell.right, 5000, |z| {
6328 let eta = cell.eta(z);
6329 (-(eta * (eta_bb(z) * eta_h(z) + 2.0 * eta_bh(z) * eta_b(z)))
6330 + (eta * eta - 1.0) * eta_b(z) * eta_b(z) * eta_h(z))
6331 * (-cell.q(z)).exp()
6332 * INV_TWO_PI
6333 });
6334 let numeric_bhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6335 let eta = cell.eta(z);
6336 (-(2.0 * eta * eta_bh(z) * eta_h(z))
6337 + (eta * eta - 1.0) * eta_b(z) * eta_h(z) * eta_h(z))
6338 * (-cell.q(z)).exp()
6339 * INV_TWO_PI
6340 });
6341 let numeric_hhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6342 let eta = cell.eta(z);
6343 ((eta * eta - 1.0) * eta_h(z) * eta_h(z) * eta_h(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6344 });
6345 let numeric_bbbh = simpson_integral(cell.left, cell.right, 5000, |z| {
6346 let eta = cell.eta(z);
6347 let b_z = eta_b(z);
6348 let h_z = eta_h(z);
6349 let bb_z = eta_bb(z);
6350 let bh_z = eta_bh(z);
6351 (-(eta * ((dc_dbbb[3] * z * z * z) * h_z + 3.0 * bb_z * bh_z))
6352 + (eta * eta - 1.0) * (3.0 * bb_z * b_z * h_z + 3.0 * bh_z * b_z * b_z)
6353 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * b_z * h_z)
6354 * (-cell.q(z)).exp()
6355 * INV_TWO_PI
6356 });
6357 let numeric_aahh = simpson_integral(cell.left, cell.right, 5000, |z| {
6358 let eta = cell.eta(z);
6359 let a_z = eta_a(z);
6360 let h_z = eta_h(z);
6361 ((eta * eta - 1.0) * polynomial_value(&dc_daa, z) * h_z * h_z
6362 + (-eta * eta * eta + 3.0 * eta) * a_z * a_z * h_z * h_z)
6363 * (-cell.q(z)).exp()
6364 * INV_TWO_PI
6365 });
6366 let numeric_abhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6367 let eta = cell.eta(z);
6368 let a_z = eta_a(z);
6369 let b_z = eta_b(z);
6370 let h_z = eta_h(z);
6371 ((eta * eta - 1.0) * (eta_ab(z) * h_z * h_z + 2.0 * eta_bh(z) * a_z * h_z)
6372 + (-eta * eta * eta + 3.0 * eta) * a_z * b_z * h_z * h_z)
6373 * (-cell.q(z)).exp()
6374 * INV_TWO_PI
6375 });
6376 let numeric_bbhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6377 let eta = cell.eta(z);
6378 let b_z = eta_b(z);
6379 let h_z = eta_h(z);
6380 let bh_z = eta_bh(z);
6381 (-(2.0 * eta * bh_z * bh_z)
6382 + (eta * eta - 1.0) * (eta_bb(z) * h_z * h_z + 4.0 * bh_z * b_z * h_z)
6383 + (-eta * eta * eta + 3.0 * eta) * b_z * b_z * h_z * h_z)
6384 * (-cell.q(z)).exp()
6385 * INV_TWO_PI
6386 });
6387 let numeric_bhhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6388 let eta = cell.eta(z);
6389 let h_z = eta_h(z);
6390 (-(eta * (3.0 * eta_bh(z) * h_z * h_z))
6391 + (eta * eta - 1.0) * (3.0 * eta_bh(z) * h_z * h_z)
6392 + (-eta * eta * eta + 3.0 * eta) * eta_b(z) * h_z * h_z * h_z)
6393 * (-cell.q(z)).exp()
6394 * INV_TWO_PI
6395 });
6396 let numeric_hhhh = simpson_integral(cell.left, cell.right, 5000, |z| {
6397 let eta = cell.eta(z);
6398 let h_z = eta_h(z);
6399 ((-eta * eta * eta + 3.0 * eta) * h_z * h_z * h_z * h_z)
6400 * (-cell.q(z)).exp()
6401 * INV_TWO_PI
6402 });
6403
6404 assert!((exact_h - numeric_h).abs() < 1e-8);
6405 assert!((exact_ah - numeric_ah).abs() < 1e-7);
6406 assert!((exact_bh - numeric_bh).abs() < 1e-7);
6407 assert!((exact_hh - numeric_hh).abs() < 1e-7);
6408 assert!((exact_abh - numeric_abh).abs() < 2e-6);
6409 assert!((exact_bbh - numeric_bbh).abs() < 2e-6);
6410 assert!((exact_bhh - numeric_bhh).abs() < 2e-6);
6411 assert!((exact_hhh - numeric_hhh).abs() < 2e-6);
6412 assert!((exact_bbbh - numeric_bbbh).abs() < 3e-6);
6413 assert!((exact_aahh - numeric_aahh).abs() < 3e-6);
6414 assert!((exact_abhh - numeric_abhh).abs() < 3e-6);
6415 assert!((exact_bbhh - numeric_bbhh).abs() < 3e-6);
6416 assert!((exact_bhhh - numeric_bhhh).abs() < 3e-6);
6417 assert!((exact_hhhh - numeric_hhhh).abs() < 3e-6);
6418 }
6419
6420 #[test]
6421 fn cross_basis_cell_derivatives_match_exact_integrands() {
6422 let score_span = LocalSpanCubic {
6423 left: -0.75,
6424 right: 0.25,
6425 c0: 0.08,
6426 c1: -0.03,
6427 c2: 0.02,
6428 c3: -0.01,
6429 };
6430 let score_basis_span = LocalSpanCubic {
6431 left: -0.75,
6432 right: 0.25,
6433 c0: -0.04,
6434 c1: 0.06,
6435 c2: -0.01,
6436 c3: 0.02,
6437 };
6438 let link_span = LocalSpanCubic {
6439 left: -0.6,
6440 right: 0.9,
6441 c0: -0.05,
6442 c1: 0.04,
6443 c2: -0.02,
6444 c3: 0.015,
6445 };
6446 let link_basis_span = LocalSpanCubic {
6447 left: -0.6,
6448 right: 0.9,
6449 c0: 0.02,
6450 c1: -0.01,
6451 c2: 0.03,
6452 c3: -0.02,
6453 };
6454 let a = 0.3;
6455 let b = -0.7;
6456 let coeffs = denested_cell_coefficients(score_span, link_span, a, b);
6457 let cell = DenestedCubicCell {
6458 left: score_span.left,
6459 right: score_span.right,
6460 c0: coeffs[0],
6461 c1: coeffs[1],
6462 c2: coeffs[2],
6463 c3: coeffs[3],
6464 };
6465 let state = evaluate_cell_moments(cell, 24).expect("cell moments");
6466 let (dc_da, dc_db) = denested_cell_coefficient_partials(score_span, link_span, a, b);
6467 let (dc_daa, dc_dab, _) = denested_cell_second_partials(score_span, link_span, a, b);
6468
6469 let coeff_h = score_basis_cell_coefficients(score_basis_span, b);
6470 let coeff_bh = score_basis_cell_coefficients(score_basis_span, 1.0);
6471 let coeff_w = link_basis_cell_coefficients(link_basis_span, a, b);
6472 let (coeff_aw, coeff_bw) = link_basis_cell_coefficient_partials(link_basis_span, a, b);
6473 let (coeff_aaw, coeff_abw, _) = link_basis_cell_second_partials(link_basis_span, a, b);
6474 let zero = [0.0; 4];
6475
6476 let eta_a = |z: f64| 1.0 + link_span.first_derivative(a + b * z);
6477 let eta_b = |z: f64| z + score_span.evaluate(z) + z * link_span.first_derivative(a + b * z);
6478 let eta_h = |z: f64| b * score_basis_span.evaluate(z);
6479 let eta_bh = |z: f64| score_basis_span.evaluate(z);
6480 let eta_w = |z: f64| link_basis_span.evaluate(a + b * z);
6481 let eta_ab = |z: f64| z * link_span.second_derivative(a + b * z);
6482 let eta_aw = |z: f64| link_basis_span.first_derivative(a + b * z);
6483 let eta_bw = |z: f64| z * link_basis_span.first_derivative(a + b * z);
6484
6485 let exact_hw =
6486 cell_second_derivative_from_moments(cell, &coeff_h, &coeff_w, &zero, &state.moments)
6487 .expect("hw");
6488 let exact_ahw = cell_third_derivative_from_moments(
6489 cell,
6490 &dc_da,
6491 &coeff_h,
6492 &coeff_w,
6493 &zero,
6494 &coeff_aw,
6495 &zero,
6496 &zero,
6497 &state.moments,
6498 )
6499 .expect("ahw");
6500 let exact_bhw = cell_third_derivative_from_moments(
6501 cell,
6502 &dc_db,
6503 &coeff_h,
6504 &coeff_w,
6505 &coeff_bh,
6506 &coeff_bw,
6507 &zero,
6508 &zero,
6509 &state.moments,
6510 )
6511 .expect("bhw");
6512 let exact_hhw = cell_third_derivative_from_moments(
6513 cell,
6514 &coeff_h,
6515 &coeff_h,
6516 &coeff_w,
6517 &zero,
6518 &zero,
6519 &zero,
6520 &zero,
6521 &state.moments,
6522 )
6523 .expect("hhw");
6524 let exact_hww = cell_third_derivative_from_moments(
6525 cell,
6526 &coeff_h,
6527 &coeff_w,
6528 &coeff_w,
6529 &zero,
6530 &zero,
6531 &zero,
6532 &zero,
6533 &state.moments,
6534 )
6535 .expect("hww");
6536 let exact_aahw = cell_fourth_derivative_from_moments(
6537 cell,
6538 &dc_da,
6539 &dc_da,
6540 &coeff_h,
6541 &coeff_w,
6542 &dc_daa,
6543 &zero,
6544 &coeff_aw,
6545 &zero,
6546 &coeff_aw,
6547 &zero,
6548 &zero,
6549 &coeff_aaw,
6550 &zero,
6551 &zero,
6552 &zero,
6553 &state.moments,
6554 )
6555 .expect("aahw");
6556 let exact_hhww = cell_fourth_derivative_from_moments(
6557 cell,
6558 &coeff_h,
6559 &coeff_h,
6560 &coeff_w,
6561 &coeff_w,
6562 &zero,
6563 &zero,
6564 &zero,
6565 &zero,
6566 &zero,
6567 &zero,
6568 &zero,
6569 &zero,
6570 &zero,
6571 &zero,
6572 &zero,
6573 &state.moments,
6574 )
6575 .expect("hhww");
6576 let exact_hhhw = cell_fourth_derivative_from_moments(
6577 cell,
6578 &coeff_h,
6579 &coeff_h,
6580 &coeff_h,
6581 &coeff_w,
6582 &zero,
6583 &zero,
6584 &zero,
6585 &zero,
6586 &zero,
6587 &zero,
6588 &zero,
6589 &zero,
6590 &zero,
6591 &zero,
6592 &zero,
6593 &state.moments,
6594 )
6595 .expect("hhhw");
6596 let exact_abhw = cell_fourth_derivative_from_moments(
6597 cell,
6598 &dc_da,
6599 &dc_db,
6600 &coeff_h,
6601 &coeff_w,
6602 &dc_dab,
6603 &zero,
6604 &coeff_aw,
6605 &coeff_bh,
6606 &coeff_bw,
6607 &zero,
6608 &zero,
6609 &coeff_abw,
6610 &zero,
6611 &zero,
6612 &zero,
6613 &state.moments,
6614 )
6615 .expect("abhw");
6616 let exact_ahww = cell_fourth_derivative_from_moments(
6617 cell,
6618 &dc_da,
6619 &coeff_h,
6620 &coeff_w,
6621 &coeff_w,
6622 &zero,
6623 &coeff_aw,
6624 &coeff_aw,
6625 &zero,
6626 &zero,
6627 &zero,
6628 &zero,
6629 &zero,
6630 &zero,
6631 &zero,
6632 &zero,
6633 &state.moments,
6634 )
6635 .expect("ahww");
6636 let exact_bhww = cell_fourth_derivative_from_moments(
6637 cell,
6638 &dc_db,
6639 &coeff_h,
6640 &coeff_w,
6641 &coeff_w,
6642 &coeff_bh,
6643 &coeff_bw,
6644 &coeff_bw,
6645 &zero,
6646 &zero,
6647 &zero,
6648 &zero,
6649 &zero,
6650 &zero,
6651 &zero,
6652 &zero,
6653 &state.moments,
6654 )
6655 .expect("bhww");
6656 let exact_hwww = cell_fourth_derivative_from_moments(
6657 cell,
6658 &coeff_h,
6659 &coeff_w,
6660 &coeff_w,
6661 &coeff_w,
6662 &zero,
6663 &zero,
6664 &zero,
6665 &zero,
6666 &zero,
6667 &zero,
6668 &zero,
6669 &zero,
6670 &zero,
6671 &zero,
6672 &zero,
6673 &state.moments,
6674 )
6675 .expect("hwww");
6676
6677 let numeric_hw = simpson_integral(cell.left, cell.right, 5000, |z| {
6678 (-cell.eta(z) * eta_h(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6679 });
6680 let numeric_ahw = simpson_integral(cell.left, cell.right, 5000, |z| {
6681 let eta = cell.eta(z);
6682 (-(eta * eta_aw(z) * eta_h(z)) + (eta * eta - 1.0) * eta_a(z) * eta_h(z) * eta_w(z))
6683 * (-cell.q(z)).exp()
6684 * INV_TWO_PI
6685 });
6686 let numeric_bhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6687 let eta = cell.eta(z);
6688 (-(eta * (eta_bh(z) * eta_w(z) + eta_bw(z) * eta_h(z)))
6689 + (eta * eta - 1.0) * eta_b(z) * eta_h(z) * eta_w(z))
6690 * (-cell.q(z)).exp()
6691 * INV_TWO_PI
6692 });
6693 let numeric_hhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6694 let eta = cell.eta(z);
6695 ((eta * eta - 1.0) * eta_h(z) * eta_h(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6696 });
6697 let numeric_hww = simpson_integral(cell.left, cell.right, 5000, |z| {
6698 let eta = cell.eta(z);
6699 ((eta * eta - 1.0) * eta_h(z) * eta_w(z) * eta_w(z)) * (-cell.q(z)).exp() * INV_TWO_PI
6700 });
6701 let numeric_aahw = simpson_integral(cell.left, cell.right, 5000, |z| {
6702 let eta = cell.eta(z);
6703 (-(eta * polynomial_value(&coeff_aaw, z) * eta_h(z))
6704 + (eta * eta - 1.0)
6705 * (polynomial_value(&dc_daa, z) * eta_h(z) * eta_w(z)
6706 + 2.0 * eta_aw(z) * eta_a(z) * eta_h(z))
6707 + (-eta * eta * eta + 3.0 * eta) * eta_a(z) * eta_a(z) * eta_h(z) * eta_w(z))
6708 * (-cell.q(z)).exp()
6709 * INV_TWO_PI
6710 });
6711 let numeric_hhww = simpson_integral(cell.left, cell.right, 5000, |z| {
6712 let eta = cell.eta(z);
6713 ((-eta * eta * eta + 3.0 * eta) * eta_h(z) * eta_h(z) * eta_w(z) * eta_w(z))
6714 * (-cell.q(z)).exp()
6715 * INV_TWO_PI
6716 });
6717 let numeric_hhhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6718 let eta = cell.eta(z);
6719 ((-eta * eta * eta + 3.0 * eta) * eta_h(z) * eta_h(z) * eta_h(z) * eta_w(z))
6720 * (-cell.q(z)).exp()
6721 * INV_TWO_PI
6722 });
6723 let numeric_abhw = simpson_integral(cell.left, cell.right, 5000, |z| {
6724 let eta = cell.eta(z);
6725 (-(eta * polynomial_value(&coeff_abw, z) * eta_h(z) + eta * eta_aw(z) * eta_bh(z))
6726 + (eta * eta - 1.0)
6727 * (eta_ab(z) * eta_h(z) * eta_w(z)
6728 + eta_aw(z) * eta_b(z) * eta_h(z)
6729 + eta_bh(z) * eta_a(z) * eta_w(z)
6730 + eta_bw(z) * eta_a(z) * eta_h(z))
6731 + (-eta * eta * eta + 3.0 * eta) * eta_a(z) * eta_b(z) * eta_h(z) * eta_w(z))
6732 * (-cell.q(z)).exp()
6733 * INV_TWO_PI
6734 });
6735 let numeric_ahww = simpson_integral(cell.left, cell.right, 5000, |z| {
6736 let eta = cell.eta(z);
6737 (2.0 * (eta * eta - 1.0) * eta_aw(z) * eta_h(z) * eta_w(z)
6738 + (-eta * eta * eta + 3.0 * eta) * eta_a(z) * eta_h(z) * eta_w(z) * eta_w(z))
6739 * (-cell.q(z)).exp()
6740 * INV_TWO_PI
6741 });
6742 let numeric_bhww = simpson_integral(cell.left, cell.right, 5000, |z| {
6743 let eta = cell.eta(z);
6744 let h_z = eta_h(z);
6745 let w_z = eta_w(z);
6746 ((eta * eta - 1.0) * (eta_bh(z) * w_z * w_z + 2.0 * eta_bw(z) * h_z * w_z)
6747 + (-eta * eta * eta + 3.0 * eta) * eta_b(z) * h_z * w_z * w_z)
6748 * (-cell.q(z)).exp()
6749 * INV_TWO_PI
6750 });
6751 let numeric_hwww = simpson_integral(cell.left, cell.right, 5000, |z| {
6752 let eta = cell.eta(z);
6753 ((-eta * eta * eta + 3.0 * eta) * eta_h(z) * eta_w(z) * eta_w(z) * eta_w(z))
6754 * (-cell.q(z)).exp()
6755 * INV_TWO_PI
6756 });
6757
6758 assert!((exact_hw - numeric_hw).abs() < 1e-7);
6759 assert!((exact_ahw - numeric_ahw).abs() < 2e-6);
6760 assert!((exact_bhw - numeric_bhw).abs() < 2e-6);
6761 assert!((exact_hhw - numeric_hhw).abs() < 2e-6);
6762 assert!((exact_hww - numeric_hww).abs() < 2e-6);
6763 assert!((exact_aahw - numeric_aahw).abs() < 3e-6);
6764 assert!((exact_hhww - numeric_hhww).abs() < 3e-6);
6765 assert!((exact_hhhw - numeric_hhhw).abs() < 3e-6);
6766 assert!((exact_abhw - numeric_abhw).abs() < 3e-6);
6767 assert!((exact_ahww - numeric_ahww).abs() < 3e-6);
6768 assert!((exact_bhww - numeric_bhww).abs() < 3e-6);
6769 assert!((exact_hwww - numeric_hwww).abs() < 3e-6);
6770 }
6771
6772 #[test]
6773 fn cell_moment_scratch_reuses_buffers_under_margslope_like_pressure() {
6774 let cells = [
6775 DenestedCubicCell {
6776 left: -1.2,
6777 right: -0.35,
6778 c0: 0.18,
6779 c1: 0.72,
6780 c2: -0.045,
6781 c3: 0.018,
6782 },
6783 DenestedCubicCell {
6784 left: -0.35,
6785 right: 0.48,
6786 c0: -0.08,
6787 c1: 0.91,
6788 c2: 0.038,
6789 c3: -0.014,
6790 },
6791 DenestedCubicCell {
6792 left: 0.48,
6793 right: 1.4,
6794 c0: 0.11,
6795 c1: 0.83,
6796 c2: 0.022,
6797 c3: 0.012,
6798 },
6799 ];
6800 let mut scratch = CellMomentScratch::with_capacity(MAX_AFFINE_ANCHOR_DEGREE);
6801 for cell in cells {
6802 let baseline = evaluate_cell_moments(cell, 9).expect("baseline moments");
6803 let scratch_state =
6804 evaluate_cell_moments_with_scratch(cell, 9, &mut scratch).expect("scratch moments");
6805 assert_eq!(baseline.branch, scratch_state.branch);
6806 assert!((baseline.value - scratch_state.value).abs() <= 1e-10);
6807 assert_eq!(baseline.moments.len(), scratch_state.moments.len());
6808 for (lhs, rhs) in baseline.moments.iter().zip(scratch_state.moments.iter()) {
6809 assert!((lhs - rhs).abs() <= 1e-10, "{lhs} vs {rhs}");
6810 }
6811 }
6812
6813 reset_cell_moment_test_reallocs();
6814 let mut checksum = 0.0;
6815 for i in 0..5_000 {
6816 let cell = cells[i % cells.len()];
6817 let state = evaluate_cell_moments_with_scratch(cell, 9, &mut scratch)
6818 .expect("scratch moments under repeated pressure");
6819 checksum += state.value + state.moments[0] * 1e-12;
6820 }
6821 assert!(checksum.is_finite());
6822 assert_eq!(
6823 cell_moment_test_reallocs(),
6824 0,
6825 "scratch-backed inner cell-moment calls should not grow Vec buffers"
6826 );
6827 }
6828
6829 #[test]
6830 fn evaluate_cell_moments_matches_numeric_integrals() {
6831 let cell = DenestedCubicCell {
6832 left: -0.9,
6833 right: 0.8,
6834 c0: 0.15,
6835 c1: -0.35,
6836 c2: 0.11,
6837 c3: -0.07,
6838 };
6839 let state = evaluate_cell_moments(cell, 6).expect("cell moments");
6840 let value_numeric = simpson_integral(cell.left, cell.right, 4000, |z| {
6841 super::normal_cdf(cell.eta(z)) * normal_pdf(z)
6842 });
6843 assert!((state.value - value_numeric).abs() < 1e-9);
6844 for degree in 0..=6 {
6845 let target = simpson_integral(cell.left, cell.right, 4000, |z| {
6846 z.powi(degree as i32) * (-cell.q(z)).exp()
6847 });
6848 assert!((state.moments[degree] - target).abs() < 1e-9);
6849 }
6850 }
6851
6852 #[test]
6853 fn partition_builder_moves_link_preimages_with_intercept() {
6854 let score_breaks = [-2.0, -1.0, 0.0, 1.0, 2.0];
6855 let link_breaks = [-1.5, -0.5, 0.5, 1.5];
6856 let score_span = |z: f64| {
6857 let left = if z < -1.0 {
6858 -2.0
6859 } else if z < 0.0 {
6860 -1.0
6861 } else if z < 1.0 {
6862 0.0
6863 } else {
6864 1.0
6865 };
6866 Ok(LocalSpanCubic {
6867 left,
6868 right: left + 1.0,
6869 c0: 0.1,
6870 c1: 0.2,
6871 c2: 0.0,
6872 c3: 0.0,
6873 })
6874 };
6875 let link_span = |u: f64| {
6876 let left = if u < -0.5 {
6877 -1.5
6878 } else if u < 0.5 {
6879 -0.5
6880 } else {
6881 0.5
6882 };
6883 Ok(LocalSpanCubic {
6884 left,
6885 right: left + 1.0,
6886 c0: -0.05,
6887 c1: 0.1,
6888 c2: 0.0,
6889 c3: 0.0,
6890 })
6891 };
6892 let cells_a0 = build_denested_partition_cells(
6893 0.25,
6894 0.9,
6895 &score_breaks,
6896 &link_breaks,
6897 score_span,
6898 link_span,
6899 )
6900 .expect("cells a0");
6901 let cells_a1 = build_denested_partition_cells(
6902 0.55,
6903 0.9,
6904 &score_breaks,
6905 &link_breaks,
6906 score_span,
6907 link_span,
6908 )
6909 .expect("cells a1");
6910 assert!(cells_a0.len() >= score_breaks.len() - 1);
6911 assert!(
6912 cells_a0
6913 .windows(2)
6914 .all(|w| (w[0].cell.right - w[1].cell.left).abs() <= 1e-12)
6915 );
6916 assert!(
6917 cells_a0
6918 .iter()
6919 .zip(cells_a1.iter())
6920 .any(|(lhs, rhs)| (lhs.cell.left - rhs.cell.left).abs() > 1e-10)
6921 );
6922 assert!(cells_a0.first().unwrap().cell.left.is_infinite());
6923 assert!(cells_a0.last().unwrap().cell.right.is_infinite());
6924 }
6925
6926 #[test]
6927 fn partition_builder_without_breaks_returns_single_global_cell() {
6928 let cells = build_denested_partition_cells_with_tails(
6929 0.3,
6930 -0.4,
6931 &[],
6932 &[],
6933 |z| {
6934 if z.is_nan() {
6935 return Err("probe z is NaN".to_string());
6936 }
6937 Ok(LocalSpanCubic {
6938 left: 0.0,
6939 right: 1.0,
6940 c0: 0.0,
6941 c1: 0.0,
6942 c2: 0.0,
6943 c3: 0.0,
6944 })
6945 },
6946 |u| {
6947 if u.is_nan() {
6948 return Err("probe u is NaN".to_string());
6949 }
6950 Ok(LocalSpanCubic {
6951 left: 0.0,
6952 right: 1.0,
6953 c0: 0.0,
6954 c1: 0.0,
6955 c2: 0.0,
6956 c3: 0.0,
6957 })
6958 },
6959 )
6960 .expect("global cell");
6961 assert_eq!(cells.len(), 1);
6962 assert_eq!(cells[0].cell.left, f64::NEG_INFINITY);
6963 assert_eq!(cells[0].cell.right, f64::INFINITY);
6964 assert!(cells[0].cell.c2.abs() < 1e-12);
6965 assert!(cells[0].cell.c3.abs() < 1e-12);
6966 }
6967
6968 #[test]
6969 fn polynomial_integral_helper_matches_moment_sum() {
6970 let cell = DenestedCubicCell {
6971 left: -1.5,
6972 right: 1.25,
6973 c0: 0.2,
6974 c1: -0.4,
6975 c2: 0.15,
6976 c3: 0.03,
6977 };
6978 let state = evaluate_cell_moments(cell, 8).expect("cell moments");
6979 let coeffs = [1.5, -0.25, 0.75, 0.1];
6980 let expected = INV_TWO_PI
6981 * coeffs
6982 .iter()
6983 .enumerate()
6984 .map(|(idx, coeff)| coeff * state.moments[idx])
6985 .sum::<f64>();
6986 let got = cell_polynomial_integral_from_moments(&coeffs, &state.moments, "test poly")
6987 .expect("poly integral");
6988 assert!((got - expected).abs() < 1e-14);
6989 }
6990
6991 #[test]
6992 fn batched_cell_moment_max_degree_matches_direct_non_affine_grid() {
6993 let cells = [
6994 DenestedCubicCell {
6995 left: -2.0,
6996 right: -0.25,
6997 c0: -0.7,
6998 c1: 0.8,
6999 c2: 0.015,
7000 c3: -0.004,
7001 },
7002 DenestedCubicCell {
7003 left: -0.5,
7004 right: 0.75,
7005 c0: 0.2,
7006 c1: -0.35,
7007 c2: -0.025,
7008 c3: 0.0,
7009 },
7010 DenestedCubicCell {
7011 left: 0.1,
7012 right: 1.6,
7013 c0: 0.4,
7014 c1: 0.25,
7015 c2: 0.01,
7016 c3: 0.006,
7017 },
7018 DenestedCubicCell {
7019 left: -1.25,
7020 right: 2.25,
7021 c0: -0.1,
7022 c1: 0.55,
7023 c2: -0.012,
7024 c3: 0.003,
7025 },
7026 ];
7027 for cell in cells {
7028 let branch = branch_cell(cell).expect("branch");
7029 if branch == ExactCellBranch::Affine {
7030 continue;
7031 }
7032 let batched =
7033 evaluate_non_affine_cell_state(cell, branch, 21).expect("degree-21 state");
7034 for degree in [9usize, 15, 21] {
7035 let direct =
7036 evaluate_non_affine_cell_state(cell, branch, degree).expect("direct state");
7037 assert_eq!(batched.branch, direct.branch);
7038 let denom = direct.value.abs().max(1.0);
7039 assert!(((batched.value - direct.value).abs() / denom) < 1e-10);
7040 for k in 0..=degree {
7041 let denom = direct.moments[k].abs().max(1.0);
7042 let rel = (batched.moments[k] - direct.moments[k]).abs() / denom;
7043 assert!(
7044 rel < 1e-10,
7045 "cell={cell:?} degree={degree} moment={k} rel={rel:e}"
7046 );
7047 }
7048 }
7049 }
7050 }
7051
7052 #[test]
7053 fn derivative_moment_evaluator_matches_value_evaluator_moments() {
7054 let cells = [
7055 DenestedCubicCell {
7056 left: -2.0,
7057 right: -0.4,
7058 c0: 0.15,
7059 c1: -0.8,
7060 c2: 0.0,
7061 c3: 0.0,
7062 },
7063 DenestedCubicCell {
7064 left: -0.75,
7065 right: 1.4,
7066 c0: -0.25,
7067 c1: 0.6,
7068 c2: 0.12,
7069 c3: 0.0,
7070 },
7071 DenestedCubicCell {
7072 left: -1.1,
7073 right: 0.9,
7074 c0: 0.35,
7075 c1: -0.3,
7076 c2: 0.05,
7077 c3: -0.015,
7078 },
7079 ];
7080 for cell in cells {
7081 for degree in [4usize, 9, 15, 21] {
7082 let full = evaluate_cell_moments_uncached(cell, degree).expect("full moments");
7083 let derivative = evaluate_cell_derivative_moments_uncached(cell, degree)
7084 .expect("derivative moments");
7085 assert_eq!(full.branch, derivative.branch);
7086 assert_eq!(full.moments.len(), derivative.moments.len());
7087 for k in 0..full.moments.len() {
7088 assert_eq!(full.moments[k].to_bits(), derivative.moments[k].to_bits());
7089 }
7090 }
7091 }
7092 }
7093
7094 #[test]
7095 fn cell_moment_lru_matches_uncached_non_affine_grid() {
7096 let cache = CellMomentLruCache::new(16 * 1024 * 1024);
7097 let stats = CellMomentCacheStats::default();
7098 let c0s = [-0.75, 0.0, 0.5];
7099 let c1s = [-1.2, 0.25, 1.1];
7100 let c2s = [-0.18, 0.07];
7101 let c3s = [0.0, 0.025];
7102 let bounds = [(-2.0, -0.5), (-0.25, 1.5)];
7103 let degrees = [4usize, 9, 15, 21];
7104 for &c0 in &c0s {
7105 for &c1 in &c1s {
7106 for &c2 in &c2s {
7107 for &c3 in &c3s {
7108 for &(left, right) in &bounds {
7109 for &max_degree in °rees {
7110 let cell = DenestedCubicCell {
7111 left,
7112 right,
7113 c0,
7114 c1,
7115 c2,
7116 c3,
7117 };
7118 let branch = branch_cell(cell).expect("branch");
7119 if branch == ExactCellBranch::Affine {
7120 continue;
7121 }
7122 let expected =
7123 evaluate_non_affine_cell_state(cell, branch, max_degree)
7124 .expect("uncached non-affine moments");
7125 let got = evaluate_cell_moments_cached(
7126 cell,
7127 max_degree,
7128 &cache,
7129 Some(&stats),
7130 )
7131 .expect("cached moments");
7132 assert_eq!(got.branch, expected.branch);
7133 assert_eq!(got.moments.len(), max_degree + 1);
7134 let denom = expected.value.abs().max(1.0);
7135 assert!(
7136 ((got.value - expected.value).abs() / denom) < 1e-10,
7137 "value mismatch for {cell:?} degree {max_degree}: got {} expected {}",
7138 got.value,
7139 expected.value
7140 );
7141 for (idx, (&lhs, &rhs)) in
7142 got.moments.iter().zip(expected.moments.iter()).enumerate()
7143 {
7144 let denom = rhs.abs().max(1.0);
7145 assert!(
7146 ((lhs - rhs).abs() / denom) < 1e-10,
7147 "moment {idx} mismatch for {cell:?} degree {max_degree}: got {lhs} expected {rhs}"
7148 );
7149 }
7150 let warm = evaluate_cell_moments_cached(
7151 cell,
7152 max_degree,
7153 &cache,
7154 Some(&stats),
7155 )
7156 .expect("warm cached moments");
7157 assert_eq!(warm, got);
7158 }
7159 }
7160 }
7161 }
7162 }
7163 }
7164 let (hits, misses) = stats.snapshot();
7165 assert!(hits > 0, "expected warm LRU hits");
7166 assert!(misses > 0, "expected cold LRU misses");
7167 }
7168
7169 #[test]
7170 fn cell_moment_fingerprint_exact_cache_matches_current_evaluator() {
7171 let cells = [
7172 DenestedCubicCell {
7173 left: -1.75,
7174 right: -0.25,
7175 c0: 0.15,
7176 c1: -0.35,
7177 c2: 0.08,
7178 c3: -0.015,
7179 },
7180 DenestedCubicCell {
7181 left: -0.5,
7182 right: 0.8,
7183 c0: -0.2,
7184 c1: 0.45,
7185 c2: -0.12,
7186 c3: 0.025,
7187 },
7188 DenestedCubicCell {
7189 left: 0.1,
7190 right: 1.6,
7191 c0: 0.05,
7192 c1: 0.2,
7193 c2: 0.03,
7194 c3: 0.004,
7195 },
7196 ];
7197 let mut cache = std::collections::HashMap::new();
7198 for max_degree in [0usize, 3, 4, 9, 16] {
7199 for cell in cells {
7200 let baseline = evaluate_cell_moments(cell, max_degree).expect("baseline moments");
7201 let key = cell_moment_cache_key(cell, max_degree, 0.0);
7202 let cached = cache.entry(key).or_insert_with(|| {
7203 evaluate_cell_moments(cell, max_degree).expect("cached moments")
7204 });
7205 assert_eq!(baseline.branch, cached.branch);
7206 assert_eq!(baseline.value.to_bits(), cached.value.to_bits());
7207 assert_eq!(baseline.moments.len(), cached.moments.len());
7208 for (lhs, rhs) in baseline.moments.iter().zip(cached.moments.iter()) {
7209 assert_eq!(lhs.to_bits(), rhs.to_bits());
7210 }
7211 }
7212 }
7213 }
7214
7215 #[test]
7216 fn fuzzy_cell_moment_fingerprint_error_scales_with_epsilon() {
7217 for epsilon in [1e-8, 1e-6] {
7218 let base = DenestedCubicCell {
7219 left: -1.25,
7220 right: 1.1,
7221 c0: 0.1,
7222 c1: -0.25,
7223 c2: 0.04,
7224 c3: -0.006,
7225 };
7226 let perturbed = DenestedCubicCell {
7227 left: base.left + 0.001 * epsilon,
7228 right: base.right - 0.001 * epsilon,
7229 c0: base.c0 + 0.001 * epsilon,
7230 c1: base.c1 - 0.001 * epsilon,
7231 c2: base.c2 + 0.001 * epsilon,
7232 c3: base.c3 - 0.001 * epsilon,
7233 };
7234 assert_eq!(
7235 cell_moment_cache_key(base, 9, epsilon),
7236 cell_moment_cache_key(perturbed, 9, epsilon)
7237 );
7238 let lhs = evaluate_cell_moments(base, 9).expect("base moments");
7239 let rhs = evaluate_cell_moments(perturbed, 9).expect("perturbed moments");
7240 let max_rel = lhs
7241 .moments
7242 .iter()
7243 .zip(rhs.moments.iter())
7244 .map(|(a, b)| (a - b).abs() / a.abs().max(b.abs()).max(1.0))
7245 .fold(0.0_f64, f64::max);
7246 assert!(
7247 max_rel <= 10.0 * epsilon,
7248 "epsilon={epsilon:.1e} max_rel={max_rel:.3e}"
7249 );
7250 }
7251 }
7252
7253 #[test]
7261 fn non_affine_cell_state_matches_prefold_reference_to_1e_minus_13() {
7262 fn reference(
7266 cell: DenestedCubicCell,
7267 branch: ExactCellBranch,
7268 max_degree: usize,
7269 ) -> CellMomentState {
7270 let mut moments: CellMomentVec = smallvec![0.0_f64; max_degree + 1];
7271 let mut value_integral = 0.0_f64;
7272 let center = 0.5 * (cell.left + cell.right);
7273 let half_width = 0.5 * (cell.right - cell.left);
7274 for (&node, &weight) in GL_NODES.iter().zip(GL_WEIGHTS.iter()) {
7275 let z = center + half_width * node;
7276 let eta = cell.eta(z);
7277 let moment_weight = weight * (-cell.q(z)).exp();
7278 let mut z_pow = 1.0_f64;
7279 for moment in &mut moments {
7280 *moment = moment_weight.mul_add(z_pow, *moment);
7281 z_pow *= z;
7282 }
7283 value_integral += weight * (-0.5 * z * z).exp() * normal_cdf(eta);
7284 }
7285 for moment in &mut moments {
7286 *moment *= half_width;
7287 }
7288 CellMomentState {
7289 branch,
7290 value: value_integral * half_width / (std::f64::consts::TAU).sqrt(),
7291 moments,
7292 }
7293 }
7294
7295 let cells = [
7300 DenestedCubicCell {
7301 left: -1.25,
7302 right: -0.2,
7303 c0: -0.35,
7304 c1: 0.85,
7305 c2: 0.04,
7306 c3: -0.015,
7307 },
7308 DenestedCubicCell {
7309 left: -0.2,
7310 right: 0.55,
7311 c0: 0.12,
7312 c1: -0.65,
7313 c2: -0.025,
7314 c3: 0.02,
7315 },
7316 DenestedCubicCell {
7317 left: 0.55,
7318 right: 1.6,
7319 c0: 0.42,
7320 c1: 0.35,
7321 c2: 0.018,
7322 c3: 0.012,
7323 },
7324 DenestedCubicCell {
7325 left: -3.0,
7326 right: -1.0,
7327 c0: 1.7,
7328 c1: -0.4,
7329 c2: 0.11,
7330 c3: -0.07,
7331 },
7332 ];
7333 let degrees = [0_usize, 4, 9, 16, 24];
7334 for cell in cells {
7335 let branch = branch_cell(cell).expect("branch");
7336 assert_ne!(branch, ExactCellBranch::Affine);
7337 for max_degree in degrees {
7338 let actual = evaluate_non_affine_cell_state(cell, branch, max_degree)
7339 .expect("optimized non-affine");
7340 let expected = reference(cell, branch, max_degree);
7341 assert_eq!(actual.branch, expected.branch);
7342 assert_eq!(actual.moments.len(), expected.moments.len());
7343 let denom_v = expected.value.abs().max(1.0);
7344 let rel_v = (actual.value - expected.value).abs() / denom_v;
7345 let actual_v = actual.value;
7346 let expected_v = expected.value;
7347 assert!(
7348 rel_v <= 1e-13,
7349 "value rel mismatch for {cell:?} degree {max_degree}: \
7350 actual={actual_v:.17e} expected={expected_v:.17e} rel={rel_v:.3e}"
7351 );
7352 for (k, (lhs, rhs)) in actual
7353 .moments
7354 .iter()
7355 .zip(expected.moments.iter())
7356 .enumerate()
7357 {
7358 let denom = rhs.abs().max(1.0);
7359 let rel = (lhs - rhs).abs() / denom;
7360 assert!(
7361 rel <= 1e-13,
7362 "moment {k} rel mismatch for {cell:?} degree {max_degree}: \
7363 actual={lhs:.17e} expected={rhs:.17e} rel={rel:.3e}"
7364 );
7365 }
7366
7367 let actual_deriv =
7370 evaluate_non_affine_cell_derivative_state(cell, branch, max_degree)
7371 .expect("optimized derivative");
7372 for (k, (lhs, rhs)) in actual_deriv
7373 .moments
7374 .iter()
7375 .zip(expected.moments.iter())
7376 .enumerate()
7377 {
7378 let denom = rhs.abs().max(1.0);
7379 let rel = (lhs - rhs).abs() / denom;
7380 assert!(
7381 rel <= 1e-13,
7382 "deriv moment {k} rel mismatch for {cell:?} degree {max_degree}: \
7383 actual={lhs:.17e} expected={rhs:.17e} rel={rel:.3e}"
7384 );
7385 }
7386 }
7387 }
7388 }
7389
7390 #[test]
7396 fn third_derivative_kernel_matches_fd_of_second_with_eta_perturbation() {
7397 let base = DenestedCubicCell {
7399 left: -0.6,
7400 right: 0.9,
7401 c0: 0.30,
7402 c1: 0.45,
7403 c2: -0.20,
7404 c3: 0.12,
7405 };
7406 let eta_u = [0.11_f64, -0.07, 0.05, 0.02];
7409 let eta_v = [-0.09_f64, 0.13, -0.04, 0.03];
7410 let eta_t = [0.17_f64, 0.06, -0.10, 0.04]; let eta_uv = [0.02_f64, 0.01, -0.015, 0.005];
7413 let eta_ut = [-0.01_f64, 0.02, 0.007, -0.003];
7414 let eta_vt = [0.015_f64, -0.008, 0.01, 0.004];
7415 let eta_uvt = [0.003_f64, -0.002, 0.001, 0.0005];
7417
7418 let neg = |a: &[f64; 4]| a.map(|v| -v);
7419 let max_degree = 15usize;
7420
7421 let f_uv_at = |s: f64| -> f64 {
7428 let cell_s = DenestedCubicCell {
7429 c0: base.c0 + s * eta_t[0],
7430 c1: base.c1 + s * eta_t[1],
7431 c2: base.c2 + s * eta_t[2],
7432 c3: base.c3 + s * eta_t[3],
7433 ..base
7434 };
7435 let st = evaluate_cell_moments(cell_s, max_degree).unwrap();
7437 let neg_cell = DenestedCubicCell {
7438 c0: -cell_s.c0,
7439 c1: -cell_s.c1,
7440 c2: -cell_s.c2,
7441 c3: -cell_s.c3,
7442 ..cell_s
7443 };
7444 let u_s = [
7445 eta_u[0] + s * eta_ut[0],
7446 eta_u[1] + s * eta_ut[1],
7447 eta_u[2] + s * eta_ut[2],
7448 eta_u[3] + s * eta_ut[3],
7449 ];
7450 let v_s = [
7451 eta_v[0] + s * eta_vt[0],
7452 eta_v[1] + s * eta_vt[1],
7453 eta_v[2] + s * eta_vt[2],
7454 eta_v[3] + s * eta_vt[3],
7455 ];
7456 let uv_s = [
7457 eta_uv[0] + s * eta_uvt[0],
7458 eta_uv[1] + s * eta_uvt[1],
7459 eta_uv[2] + s * eta_uvt[2],
7460 eta_uv[3] + s * eta_uvt[3],
7461 ];
7462 cell_second_derivative_from_moments(
7463 neg_cell,
7464 &neg(&u_s),
7465 &neg(&v_s),
7466 &neg(&uv_s),
7467 &st.moments,
7468 )
7469 .unwrap()
7470 };
7471
7472 let h = 1e-5;
7473 let fd = (f_uv_at(h) - f_uv_at(-h)) / (2.0 * h);
7474
7475 let st0 = evaluate_cell_moments(base, max_degree).unwrap();
7478 let neg_cell0 = DenestedCubicCell {
7479 c0: -base.c0,
7480 c1: -base.c1,
7481 c2: -base.c2,
7482 c3: -base.c3,
7483 ..base
7484 };
7485 let analytic = cell_third_derivative_from_moments(
7486 neg_cell0,
7487 &neg(&eta_u),
7488 &neg(&eta_v),
7489 &neg(&eta_t),
7490 &neg(&eta_uv),
7491 &neg(&eta_ut),
7492 &neg(&eta_vt),
7493 &neg(&eta_uvt),
7494 &st0.moments,
7495 )
7496 .unwrap();
7497
7498 let denom = fd.abs().max(1e-3);
7499 let rel = (analytic - fd).abs() / denom;
7500 assert!(
7501 rel <= 1e-5,
7502 "third kernel vs FD-of-second mismatch: analytic={analytic:.12e} fd={fd:.12e} rel={rel:.3e}"
7503 );
7504 }
7505
7506 #[test]
7507 fn moving_shared_edge_second_integral_derivative_has_leibniz_jump_sign() {
7508 let edge0 = 0.2_f64;
7509 let edge_velocity = -0.37_f64;
7510
7511 let left_eta = [0.22_f64, -0.18, 0.09, 0.03];
7512 let right_eta = [-0.11_f64, 0.26, -0.04, 0.02];
7513 let left_r = [0.08_f64, -0.05, 0.03, 0.01];
7514 let left_s = [-0.06_f64, 0.04, 0.02, -0.015];
7515 let left_rs = [0.025_f64, -0.012, 0.006, 0.004];
7516 let right_r = [-0.03_f64, 0.07, -0.02, 0.012];
7517 let right_s = [0.05_f64, -0.025, 0.018, 0.007];
7518 let right_rs = [-0.018_f64, 0.014, -0.005, 0.003];
7519
7520 let integral_at = |shift: f64| -> f64 {
7521 let edge = edge0 + edge_velocity * shift;
7522 let left = DenestedCubicCell {
7523 left: -0.7,
7524 right: edge,
7525 c0: left_eta[0],
7526 c1: left_eta[1],
7527 c2: left_eta[2],
7528 c3: left_eta[3],
7529 };
7530 let right = DenestedCubicCell {
7531 left: edge,
7532 right: 1.1,
7533 c0: right_eta[0],
7534 c1: right_eta[1],
7535 c2: right_eta[2],
7536 c3: right_eta[3],
7537 };
7538 let left_state = evaluate_cell_moments(left, 12).expect("left moments");
7539 let right_state = evaluate_cell_moments(right, 12).expect("right moments");
7540 cell_second_derivative_from_moments(
7541 left,
7542 &left_r,
7543 &left_s,
7544 &left_rs,
7545 &left_state.moments,
7546 )
7547 .expect("left second")
7548 + cell_second_derivative_from_moments(
7549 right,
7550 &right_r,
7551 &right_s,
7552 &right_rs,
7553 &right_state.moments,
7554 )
7555 .expect("right second")
7556 };
7557
7558 let h = 1e-5;
7559 let fd = (integral_at(h) - integral_at(-h)) / (2.0 * h);
7560
7561 let left = DenestedCubicCell {
7562 left: -0.7,
7563 right: edge0,
7564 c0: left_eta[0],
7565 c1: left_eta[1],
7566 c2: left_eta[2],
7567 c3: left_eta[3],
7568 };
7569 let right = DenestedCubicCell {
7570 left: edge0,
7571 right: 1.1,
7572 c0: right_eta[0],
7573 c1: right_eta[1],
7574 c2: right_eta[2],
7575 c3: right_eta[3],
7576 };
7577 let f_left =
7578 cell_second_derivative_boundary_integrand(left, &left_r, &left_s, &left_rs, edge0);
7579 let f_right =
7580 cell_second_derivative_boundary_integrand(right, &right_r, &right_s, &right_rs, edge0);
7581 let analytic = edge_velocity * (f_left - f_right);
7582
7583 let denom = analytic.abs().max(1e-8);
7584 let rel = (fd - analytic).abs() / denom;
7585 assert!(
7586 rel <= 5e-8,
7587 "moving edge sign mismatch: fd={fd:.12e} analytic={analytic:.12e} rel={rel:.3e}"
7588 );
7589 }
7590
7591 #[test]
7592 fn moving_shared_edge_second_integral_mixed_derivative_has_full_leibniz_terms() {
7593 let edge0 = -0.15_f64;
7594 let edge_d1 = 0.31_f64;
7595 let edge_d2 = -0.27_f64;
7596 let edge_d12 = 0.19_f64;
7597
7598 let left_eta = [0.16_f64, -0.21, 0.07, -0.025];
7599 let right_eta = [-0.09_f64, 0.18, -0.055, 0.018];
7600 let left_r = [0.075_f64, -0.045, 0.018, 0.009];
7601 let left_s = [-0.052_f64, 0.033, 0.014, -0.011];
7602 let left_rs = [0.021_f64, -0.009, 0.005, 0.0025];
7603 let right_r = [-0.028_f64, 0.063, -0.017, 0.010];
7604 let right_s = [0.047_f64, -0.023, 0.016, 0.006];
7605 let right_rs = [-0.015_f64, 0.012, -0.004, 0.002];
7606
7607 let integral_at = |s1: f64, s2: f64| -> f64 {
7608 let edge = edge0 + edge_d1 * s1 + edge_d2 * s2 + edge_d12 * s1 * s2;
7609 let left = DenestedCubicCell {
7610 left: -0.8,
7611 right: edge,
7612 c0: left_eta[0],
7613 c1: left_eta[1],
7614 c2: left_eta[2],
7615 c3: left_eta[3],
7616 };
7617 let right = DenestedCubicCell {
7618 left: edge,
7619 right: 0.9,
7620 c0: right_eta[0],
7621 c1: right_eta[1],
7622 c2: right_eta[2],
7623 c3: right_eta[3],
7624 };
7625 let left_state = evaluate_cell_moments(left, 12).expect("left moments");
7626 let right_state = evaluate_cell_moments(right, 12).expect("right moments");
7627 cell_second_derivative_from_moments(
7628 left,
7629 &left_r,
7630 &left_s,
7631 &left_rs,
7632 &left_state.moments,
7633 )
7634 .expect("left second")
7635 + cell_second_derivative_from_moments(
7636 right,
7637 &right_r,
7638 &right_s,
7639 &right_rs,
7640 &right_state.moments,
7641 )
7642 .expect("right second")
7643 };
7644
7645 let h = 2e-4;
7646 let fd = (integral_at(h, h) - integral_at(h, -h) - integral_at(-h, h)
7647 + integral_at(-h, -h))
7648 / (4.0 * h * h);
7649
7650 let left = DenestedCubicCell {
7651 left: -0.8,
7652 right: edge0,
7653 c0: left_eta[0],
7654 c1: left_eta[1],
7655 c2: left_eta[2],
7656 c3: left_eta[3],
7657 };
7658 let right = DenestedCubicCell {
7659 left: edge0,
7660 right: 0.9,
7661 c0: right_eta[0],
7662 c1: right_eta[1],
7663 c2: right_eta[2],
7664 c3: right_eta[3],
7665 };
7666
7667 let boundary_z_derivative =
7668 |cell: DenestedCubicCell, r: &[f64], s: &[f64], rs: &[f64]| -> f64 {
7669 let eta = cell.eta(edge0);
7670 let eta_z = cell.c1 + 2.0 * cell.c2 * edge0 + 3.0 * cell.c3 * edge0 * edge0;
7671 let cr = poly_eval_at(r, edge0);
7672 let cs = poly_eval_at(s, edge0);
7673 let crs = poly_eval_at(rs, edge0);
7674 let cr_z = r.iter().enumerate().skip(1).fold(0.0, |acc, (k, val)| {
7675 acc + (k as f64) * val * edge0.powi(k as i32 - 1)
7676 });
7677 let cs_z = s.iter().enumerate().skip(1).fold(0.0, |acc, (k, val)| {
7678 acc + (k as f64) * val * edge0.powi(k as i32 - 1)
7679 });
7680 let crs_z = rs.iter().enumerate().skip(1).fold(0.0, |acc, (k, val)| {
7681 acc + (k as f64) * val * edge0.powi(k as i32 - 1)
7682 });
7683 let amp = crs - eta * cr * cs;
7684 let amp_z = crs_z - eta_z * cr * cs - eta * cr_z * cs - eta * cr * cs_z;
7685 let q_z = edge0 + eta * eta_z;
7686 (amp_z - amp * q_z) * (-cell.q(edge0)).exp() * INV_TWO_PI
7687 };
7688
7689 let f_left =
7690 cell_second_derivative_boundary_integrand(left, &left_r, &left_s, &left_rs, edge0);
7691 let f_right =
7692 cell_second_derivative_boundary_integrand(right, &right_r, &right_s, &right_rs, edge0);
7693 let fz_left = boundary_z_derivative(left, &left_r, &left_s, &left_rs);
7694 let fz_right = boundary_z_derivative(right, &right_r, &right_s, &right_rs);
7695 let analytic = edge_d12 * (f_left - f_right) + edge_d1 * edge_d2 * (fz_left - fz_right);
7696
7697 let denom = analytic.abs().max(1e-8);
7698 let rel = (fd - analytic).abs() / denom;
7699 assert!(
7700 rel <= 2e-7,
7701 "moving edge mixed term mismatch: fd={fd:.12e} analytic={analytic:.12e} rel={rel:.3e}"
7702 );
7703 }
7704
7705 #[test]
7730 fn third_order_self_flux_telescopes_but_third_integrand_jumps_at_c2_knot_1454() {
7731 let edge0 = 0.13_f64;
7732 let edge_velocity = -0.41_f64;
7733
7734 let left_eta = [0.18_f64, -0.12, 0.07, 0.04];
7738 let right_c3 = 0.04_f64 + 0.09; let l0 = left_eta[0];
7745 let l1 = left_eta[1];
7746 let l2 = left_eta[2];
7747 let l3 = left_eta[3];
7748 let e = edge0;
7749 let eta_val = l0 + l1 * e + l2 * e * e + l3 * e * e * e;
7750 let eta_d1 = l1 + 2.0 * l2 * e + 3.0 * l3 * e * e;
7751 let eta_d2 = 2.0 * l2 + 6.0 * l3 * e;
7752 let rc2 = (eta_d2 - 6.0 * right_c3 * e) / 2.0;
7753 let rc1 = eta_d1 - 2.0 * rc2 * e - 3.0 * right_c3 * e * e;
7754 let rc0 = eta_val - rc1 * e - rc2 * e * e - right_c3 * e * e * e;
7755 let right_eta = [rc0, rc1, rc2, right_c3];
7756
7757 let common_r = [0.06_f64, -0.04, 0.02, 0.0];
7763 let common_s = [-0.05_f64, 0.03, 0.015, 0.0];
7764 let common_t = [0.08_f64, 0.05, -0.03, 0.0];
7765 let common_rs = [0.02_f64, -0.01, 0.005, 0.0];
7766 let common_rt = [-0.012_f64, 0.008, 0.004, 0.0];
7767 let common_st = [0.015_f64, -0.006, 0.003, 0.0];
7768 let left_rst = [6.0 * l3, 0.0, 0.0, 0.0];
7770 let right_rst = [6.0 * right_c3, 0.0, 0.0, 0.0];
7771
7772 let max_degree = 15usize;
7773 let neg = |a: &[f64; 4]| a.map(|v| -v);
7774
7775 let integral_at = |shift: f64| -> f64 {
7780 let edge = edge0 + edge_velocity * shift;
7781 let left = DenestedCubicCell {
7782 left: -0.7,
7783 right: edge,
7784 c0: left_eta[0],
7785 c1: left_eta[1],
7786 c2: left_eta[2],
7787 c3: left_eta[3],
7788 };
7789 let right = DenestedCubicCell {
7790 left: edge,
7791 right: 1.0,
7792 c0: right_eta[0],
7793 c1: right_eta[1],
7794 c2: right_eta[2],
7795 c3: right_eta[3],
7796 };
7797 let lst = evaluate_cell_moments(left, max_degree).unwrap();
7798 let rst_m = evaluate_cell_moments(right, max_degree).unwrap();
7799 let neg_left = DenestedCubicCell {
7800 c0: -left.c0,
7801 c1: -left.c1,
7802 c2: -left.c2,
7803 c3: -left.c3,
7804 ..left
7805 };
7806 let neg_right = DenestedCubicCell {
7807 c0: -right.c0,
7808 c1: -right.c1,
7809 c2: -right.c2,
7810 c3: -right.c3,
7811 ..right
7812 };
7813 let li = cell_third_derivative_from_moments(
7814 neg_left,
7815 &neg(&common_r),
7816 &neg(&common_s),
7817 &neg(&common_t),
7818 &neg(&common_rs),
7819 &neg(&common_rt),
7820 &neg(&common_st),
7821 &neg(&left_rst),
7822 &lst.moments,
7823 )
7824 .unwrap();
7825 let ri = cell_third_derivative_from_moments(
7826 neg_right,
7827 &neg(&common_r),
7828 &neg(&common_s),
7829 &neg(&common_t),
7830 &neg(&common_rs),
7831 &neg(&common_rt),
7832 &neg(&common_st),
7833 &neg(&right_rst),
7834 &rst_m.moments,
7835 )
7836 .unwrap();
7837 li + ri
7838 };
7839
7840 let h = 1e-5;
7841 let fd = (integral_at(h) - integral_at(-h)) / (2.0 * h);
7842
7843 let neg_eta = |eta: &[f64; 4]| [-eta[0], -eta[1], -eta[2], -eta[3]];
7862 let left_eta_neg = neg_eta(&left_eta);
7863 let right_eta_neg = neg_eta(&right_eta);
7864 let left0 = DenestedCubicCell {
7865 left: -0.7,
7866 right: edge0,
7867 c0: left_eta_neg[0],
7868 c1: left_eta_neg[1],
7869 c2: left_eta_neg[2],
7870 c3: left_eta_neg[3],
7871 };
7872 let right0 = DenestedCubicCell {
7873 left: edge0,
7874 right: 1.0,
7875 c0: right_eta_neg[0],
7876 c1: right_eta_neg[1],
7877 c2: right_eta_neg[2],
7878 c3: right_eta_neg[3],
7879 };
7880 let f_left = cell_third_derivative_boundary_integrand(
7881 left0,
7882 &neg(&common_r),
7883 &neg(&common_s),
7884 &neg(&common_t),
7885 &neg(&common_rs),
7886 &neg(&common_rt),
7887 &neg(&common_st),
7888 &neg(&left_rst),
7889 edge0,
7890 );
7891 let f_right = cell_third_derivative_boundary_integrand(
7892 right0,
7893 &neg(&common_r),
7894 &neg(&common_s),
7895 &neg(&common_t),
7896 &neg(&common_rs),
7897 &neg(&common_rt),
7898 &neg(&common_st),
7899 &neg(&right_rst),
7900 edge0,
7901 );
7902
7903 let jump = f_left - f_right;
7907 assert!(
7908 jump.abs() > 1e-4,
7909 "third-derivative integrand must jump across the C² knot (α₃ discontinuity); \
7910 got jump={jump:.3e}"
7911 );
7912
7913 let analytic_flux = edge_velocity * jump;
7914 let denom = fd.abs().max(1e-6);
7915 let rel = (fd - analytic_flux).abs() / denom;
7916 assert!(
7917 rel <= 1e-5,
7918 "moving-edge third-derivative flux mismatch (#1454): fd={fd:.12e} \
7919 analytic_flux={analytic_flux:.12e} rel={rel:.3e}"
7920 );
7921
7922 let a_row = 0.21_f64;
7935 let b_row = 1.37_f64;
7936 let knot = a_row + b_row * edge0; let left_link = LocalSpanCubic {
7940 left: knot - 0.6,
7941 right: knot + 0.6,
7942 c0: 0.0,
7943 c1: 0.0,
7944 c2: 0.08,
7945 c3: -0.05,
7946 };
7947 let right_alpha3 = -0.05_f64 + 0.11; let right_left_coord = knot - 0.4;
7950 let lhs = 2.0 * left_link.c2 + 6.0 * left_link.c3 * (knot - left_link.left);
7951 let right_alpha2 = (lhs - 6.0 * right_alpha3 * (knot - right_left_coord)) / 2.0;
7952 let right_link = LocalSpanCubic {
7953 left: right_left_coord,
7954 right: right_left_coord + 0.8,
7955 c0: 0.0,
7956 c1: 0.0,
7957 c2: right_alpha2,
7958 c3: right_alpha3,
7959 };
7960 let (_, _, dc_dbb_left) = link_cubic_second_partials(left_link, a_row, b_row);
7961 let (_, _, dc_dbb_right) = link_cubic_second_partials(right_link, a_row, b_row);
7962 assert!(
7964 (dc_dbb_left[3] - dc_dbb_right[3]).abs() > 1e-3,
7965 "α₃ jump must make the raw dc_dbb coefficient arrays differ"
7966 );
7967 let c_bb_left = poly_eval_at(&dc_dbb_left, edge0);
7970 let c_bb_right = poly_eval_at(&dc_dbb_right, edge0);
7971 assert!(
7972 (c_bb_left - c_bb_right).abs() <= 1e-12,
7973 "second-derivative slope-slope integrand must be CONTINUOUS across the \
7974 C² knot (telescoping self-flux): left={c_bb_left:.15e} right={c_bb_right:.15e}"
7975 );
7976 }
7977}