1#![allow(clippy::indexing_slicing)] use crate::{Complex, Error, Result, numerics::finite};
11
12#[derive(Clone, Copy, Debug)]
17pub enum Transform {
18 CarToCyl,
20 CarToSph,
22 CylToCar,
24 CylToSph,
26 SphToCar,
28 SphToCyl,
30 CarToPol,
32 PolToCar,
34}
35use Transform::{CarToCyl, CarToPol, CarToSph, CylToCar, CylToSph, PolToCar, SphToCar, SphToCyl};
36
37impl Transform {
38 #[must_use]
40 pub fn dimension(self) -> usize {
41 match self {
42 Self::CarToPol | Self::PolToCar => 2,
43 _ => 3,
44 }
45 }
46}
47
48#[inline]
51fn radius(a: f64, b: f64) -> f64 {
52 let scale = a.abs().max(b.abs());
53 if (1e-150..=1e150).contains(&scale) {
54 a.mul_add(a, b * b).sqrt()
55 } else {
56 a.hypot(b)
57 }
58}
59
60fn validate(position: [f64; 3]) -> Result<()> {
62 if position.iter().any(|v| !v.is_finite()) {
63 return Err(Error::InvalidInput("coordinates must be finite".into()));
64 }
65 Ok(())
66}
67
68#[inline]
70fn cylindrical_from_cartesian([a, b, c]: [f64; 3]) -> [f64; 3] {
71 [radius(a, b), b.atan2(a), c]
72}
73
74#[inline]
76fn cartesian_from_cylindrical([a, b, c]: [f64; 3]) -> [f64; 3] {
77 let (sin, cos) = b.sin_cos();
78 [a * cos, a * sin, c]
79}
80
81#[inline]
83fn spherical_from_cylindrical([a, b, c]: [f64; 3]) -> [f64; 3] {
84 [radius(a, c), a.atan2(c), b]
85}
86
87#[inline]
89fn cylindrical_from_spherical([a, b, c]: [f64; 3]) -> [f64; 3] {
90 let (sin, cos) = b.sin_cos();
91 [a * sin, c, a * cos]
92}
93
94#[allow(clippy::inline_always)]
103#[inline(always)]
104pub fn point(position: [f64; 3], transform: Transform) -> Result<[f64; 3]> {
105 validate(position)?;
106 Ok(match transform {
107 CarToCyl => cylindrical_from_cartesian(position),
108 CarToPol => {
109 let [rho, phi, _] = cylindrical_from_cartesian(position);
110 [rho, phi, 0.0]
111 }
112 CylToCar => cartesian_from_cylindrical(position),
113 PolToCar => {
114 let [x, y, _] = cartesian_from_cylindrical(position);
115 [x, y, 0.0]
116 }
117 CylToSph => spherical_from_cylindrical(position),
118 SphToCyl => cylindrical_from_spherical(position),
119 CarToSph => spherical_from_cylindrical(cylindrical_from_cartesian(position)),
122 SphToCar => cartesian_from_cylindrical(cylindrical_from_spherical(position)),
123 })
124}
125
126pub fn point_pullback(
132 position: [f64; 3],
133 transform: Transform,
134 cotangent: [f64; 3],
135) -> Result<[f64; 3]> {
136 validate(position)?;
137 validate(cotangent)?;
138 let [a, b, c] = position;
139 let undefined =
140 || Error::InvalidInput("coordinate derivative is undefined at this axis or origin".into());
141 Ok(match transform {
142 CarToCyl | CarToPol => {
143 let rho = radius(a, b);
144 if rho == 0.0 && (cotangent[0] != 0.0 || cotangent[1] != 0.0) {
145 return Err(undefined());
146 }
147 let (sp, cp) = if rho == 0.0 {
148 (0.0, 1.0)
149 } else {
150 (b / rho, a / rho)
151 };
152 let angular = if cotangent[1] == 0.0 {
153 0.0
154 } else {
155 cotangent[1] / rho
156 };
157 [
158 cotangent[0] * cp - angular * sp,
159 cotangent[0] * sp + angular * cp,
160 if matches!(transform, CarToPol) {
161 0.0
162 } else {
163 cotangent[2]
164 },
165 ]
166 }
167 CylToCar | PolToCar => {
168 let (sin, cos) = b.sin_cos();
169 [
170 cotangent[0] * cos + cotangent[1] * sin,
171 a * (-cotangent[0] * sin + cotangent[1] * cos),
172 if matches!(transform, PolToCar) {
173 0.0
174 } else {
175 cotangent[2]
176 },
177 ]
178 }
179 CylToSph => {
180 let r = radius(a, c);
181 if r == 0.0 && (cotangent[0] != 0.0 || cotangent[1] != 0.0) {
182 return Err(undefined());
183 }
184 if r == 0.0 {
185 [0.0, cotangent[2], 0.0]
186 } else {
187 [
188 cotangent[0] * (a / r) + cotangent[1] * (c / r) / r,
189 cotangent[2],
190 cotangent[0] * (c / r) - cotangent[1] * (a / r) / r,
191 ]
192 }
193 }
194 SphToCyl => {
195 let (sin, cos) = b.sin_cos();
196 [
197 cotangent[0] * sin + cotangent[2] * cos,
198 a * (cotangent[0] * cos - cotangent[2] * sin),
199 cotangent[1],
200 ]
201 }
202 CarToSph => {
203 if radius(a, b) == 0.0 && (cotangent[1] != 0.0 || cotangent[2] != 0.0) {
206 return Err(undefined());
207 }
208 let cylindrical = point(position, CarToCyl)?;
209 point_pullback(
210 position,
211 CarToCyl,
212 point_pullback(cylindrical, CylToSph, cotangent)?,
213 )?
214 }
215 SphToCar => {
216 let cylindrical = point(position, SphToCyl)?;
217 point_pullback(
218 position,
219 SphToCyl,
220 point_pullback(cylindrical, CylToCar, cotangent)?,
221 )?
222 }
223 })
224}
225
226pub fn point_pushforward(
231 position: [f64; 3],
232 transform: Transform,
233 tangent: [f64; 3],
234) -> Result<[f64; 3]> {
235 validate(position)?;
236 validate(tangent)?;
237 let [a, b, c] = position;
238 let [da, db, dc] = tangent;
239 let undefined =
240 || Error::InvalidInput("coordinate derivative is undefined at this axis or origin".into());
241 Ok(match transform {
242 CarToCyl | CarToPol => {
243 let rho = radius(a, b);
244 if rho == 0.0 && (da != 0.0 || db != 0.0) {
245 return Err(undefined());
246 }
247 let (drho, dphi) = if rho == 0.0 {
248 (0.0, 0.0)
249 } else {
250 let (sin, cos) = (b / rho, a / rho);
251 (cos * da + sin * db, (cos * db - sin * da) / rho)
252 };
253 [
254 drho,
255 dphi,
256 if matches!(transform, CarToPol) {
257 0.0
258 } else {
259 dc
260 },
261 ]
262 }
263 CylToCar | PolToCar => {
264 let (sin, cos) = b.sin_cos();
265 [
266 cos * da - a * sin * db,
267 sin * da + a * cos * db,
268 if matches!(transform, PolToCar) {
269 0.0
270 } else {
271 dc
272 },
273 ]
274 }
275 CylToSph => {
276 let r = radius(a, c);
277 if r == 0.0 && (da != 0.0 || dc != 0.0) {
278 return Err(undefined());
279 }
280 if r == 0.0 {
281 [0.0, 0.0, db]
282 } else {
283 [
284 (a / r) * da + (c / r) * dc,
285 ((c / r) * da - (a / r) * dc) / r,
286 db,
287 ]
288 }
289 }
290 SphToCyl => {
291 let (sin, cos) = b.sin_cos();
292 [sin * da + a * cos * db, dc, cos * da - a * sin * db]
293 }
294 CarToSph => point_pushforward(
295 point(position, CarToCyl)?,
296 CylToSph,
297 point_pushforward(position, CarToCyl, tangent)?,
298 )?,
299 SphToCar => point_pushforward(
300 point(position, SphToCyl)?,
301 CylToCar,
302 point_pushforward(position, SphToCyl, tangent)?,
303 )?,
304 })
305}
306
307#[inline]
312fn frame(position: [f64; 3], transform: Transform) -> ([[f64; 3]; 3], [[[f64; 3]; 3]; 2]) {
313 let [a, b, c] = position;
314 let inverse = matches!(transform, CylToCar | PolToCar | SphToCyl | SphToCar);
315 let zero = [[0.0; 3]; 3];
316 let (rotation, derivatives) = match transform {
317 CarToCyl | CarToPol | CylToCar | PolToCar => {
318 let phi = if inverse { b } else { b.atan2(a) };
319 let (sin, cos) = phi.sin_cos();
320 (
321 [[cos, sin, 0.0], [-sin, cos, 0.0], [0.0, 0.0, 1.0]],
322 [[[-sin, cos, 0.0], [-cos, -sin, 0.0], [0.0; 3]], zero],
323 )
324 }
325 CylToSph | SphToCyl => {
326 let theta = if inverse { b } else { a.atan2(c) };
327 let (sin, cos) = theta.sin_cos();
328 (
329 [[sin, 0.0, cos], [cos, 0.0, -sin], [0.0, 1.0, 0.0]],
330 [[[cos, 0.0, -sin], [-sin, 0.0, -cos], [0.0; 3]], zero],
331 )
332 }
333 CarToSph | SphToCar => {
334 let theta = if inverse { b } else { radius(a, b).atan2(c) };
335 let phi = if inverse { c } else { b.atan2(a) };
336 let (st, ct) = theta.sin_cos();
337 let (sp, cp) = phi.sin_cos();
338 (
339 [
340 [st * cp, st * sp, ct],
341 [ct * cp, ct * sp, -st],
342 [-sp, cp, 0.0],
343 ],
344 [
345 [[ct * cp, ct * sp, -st], [-st * cp, -st * sp, -ct], [0.0; 3]],
346 [
347 [-st * sp, st * cp, 0.0],
348 [-ct * sp, ct * cp, 0.0],
349 [-cp, -sp, 0.0],
350 ],
351 ],
352 )
353 }
354 };
355 if inverse {
356 (transpose(rotation), derivatives.map(transpose))
357 } else {
358 (rotation, derivatives)
359 }
360}
361
362fn transpose(matrix: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
364 std::array::from_fn(|i| std::array::from_fn(|j| matrix[j][i]))
365}
366
367fn apply(matrix: [[f64; 3]; 3], vector: [Complex; 3]) -> [Complex; 3] {
369 matrix.map(|row| row.into_iter().zip(vector).map(|(a, b)| a * b).sum())
370}
371
372#[allow(clippy::inline_always)]
378#[inline(always)]
379pub fn vector(
380 value: [Complex; 3],
381 position: [f64; 3],
382 transform: Transform,
383) -> Result<[Complex; 3]> {
384 validate(position)?;
385 if value.into_iter().any(|v| !finite(v)) {
386 return Err(Error::InvalidInput(
387 "vector components must be finite".into(),
388 ));
389 }
390 let r = frame(position, transform).0;
393 let [x, y, z] = value;
394 let row = |i: usize| x * r[i][0] + y * r[i][1];
395 Ok(match transform {
396 CarToCyl | CarToPol | CylToCar | PolToCar => [row(0), row(1), z],
397 CylToSph => [x * r[0][0] + z * r[0][2], x * r[1][0] + z * r[1][2], y],
398 SphToCyl => [row(0), z, row(2)],
399 CarToSph | SphToCar => [row(0) + z * r[0][2], row(1) + z * r[1][2], row(2)],
400 })
401}
402
403pub fn vector_pullback(
406 value: [Complex; 3],
407 position: [f64; 3],
408 transform: Transform,
409 cotangent: [Complex; 3],
410) -> Result<([Complex; 3], [f64; 3])> {
411 validate(position)?;
412 if value.into_iter().chain(cotangent).any(|v| !finite(v)) {
413 return Err(Error::InvalidInput(
414 "vector and cotangent must be finite".into(),
415 ));
416 }
417 let (rotation, derivatives) = frame(position, transform);
418 let angles = derivatives.map(|d| {
419 apply(d, value)
420 .into_iter()
421 .zip(cotangent)
422 .map(|(d, g)| (g.conj() * d).re)
423 .sum::<f64>()
424 });
425 let point = match transform {
426 CarToCyl | CarToPol | CylToSph => {
427 point_pullback(position, transform, [0.0, angles[0], 0.0])?
428 }
429 CarToSph => point_pullback(position, transform, [0.0, angles[0], angles[1]])?,
430 CylToCar | PolToCar | SphToCyl => [0.0, angles[0], 0.0],
431 SphToCar => [0.0, angles[0], angles[1]],
432 };
433 Ok((apply(transpose(rotation), cotangent), point))
434}
435
436pub fn vector_pushforward(
441 value: [Complex; 3],
442 position: [f64; 3],
443 transform: Transform,
444 value_tangent: [Complex; 3],
445 position_tangent: [f64; 3],
446) -> Result<[Complex; 3]> {
447 validate(position)?;
448 validate(position_tangent)?;
449 if value.into_iter().chain(value_tangent).any(|v| !finite(v)) {
450 return Err(Error::InvalidInput(
451 "vector and tangent must be finite".into(),
452 ));
453 }
454 let angles = match transform {
455 CarToCyl | CarToPol | CylToSph => [
456 point_pushforward(position, transform, position_tangent)?[1],
457 0.0,
458 ],
459 CarToSph => {
460 let tangent = point_pushforward(position, transform, position_tangent)?;
461 [tangent[1], tangent[2]]
462 }
463 CylToCar | PolToCar | SphToCyl => [position_tangent[1], 0.0],
464 SphToCar => [position_tangent[1], position_tangent[2]],
465 };
466 let (rotation, derivatives) = frame(position, transform);
467 let mut result = apply(rotation, value_tangent);
468 for (derivative, angle) in derivatives.into_iter().zip(angles) {
469 if angle != 0.0 {
470 for (result, angular) in result.iter_mut().zip(apply(derivative, value)) {
471 *result += angular * angle;
472 }
473 }
474 }
475 Ok(result)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::{
484 Complex, Transform, point, point_pullback, point_pushforward, radius, vector,
485 vector_pullback, vector_pushforward,
486 };
487 use crate::test_support::{
488 ALGEBRA_CASES, DEFAULT_CASES, complex, log_uniform, prop_assert_close, re_dot,
489 };
490 use Transform::{
491 CarToCyl, CarToPol, CarToSph, CylToCar, CylToSph, PolToCar, SphToCar, SphToCyl,
492 };
493 use proptest::{prelude::*, test_runner::TestCaseError};
494
495 const PAIRS: [(Transform, Transform); 4] = [
497 (CarToCyl, CylToCar),
498 (CarToSph, SphToCar),
499 (CylToSph, SphToCyl),
500 (CarToPol, PolToCar),
501 ];
502
503 #[test]
504 fn coordinate_norm_preserves_extreme_scales() {
505 for scale in [
506 f64::from_bits(1),
507 1e-300,
508 1e-151,
509 1e-149,
510 1.0,
511 1e149,
512 1e151,
513 1e300,
514 f64::MAX / 2.0,
515 ] {
516 for (a, b) in [(0.0, 1.0), (1.0, 0.0), (0.3, -0.4), (1.0, -1.0)] {
517 let a = a * scale;
518 let b = b * scale;
519 let expected = a.hypot(b);
520 assert!((radius(a, b) - expected).abs() <= 3.0 * f64::EPSILON * expected);
521 }
522 }
523 }
524
525 fn cartesian_point() -> impl Strategy<Value = [f64; 3]> {
528 let direction = prop_oneof![
529 3 => prop::array::uniform3(-2.0_f64..2.0),
530 1 => (-2.0_f64..2.0).prop_map(|z| [0.0, 0.0, z]),
531 ];
532 let scale = prop_oneof![Just(1e-300), Just(1.0), Just(1e300), log_uniform(-3.0..3.0),];
533 (direction, scale).prop_map(|(direction, scale)| direction.map(|v| v * scale))
534 }
535
536 proptest! {
537 #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
538
539 #[test]
540 fn charts_invert_compose_and_rotate_vectors_orthogonally(
541 position in cartesian_point(),
542 value in prop::array::uniform3(complex(1.0)),
543 ) {
544 check_charts(position, value)?;
545 }
546 }
547
548 proptest! {
549 #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
550
551 #[test]
552 fn coordinate_and_vector_adjoints(
553 a in 0.2_f64..2.0,
554 b in -1.0_f64..1.0,
555 c in -1.0_f64..1.0,
556 value in prop::array::uniform3(complex(1.0)),
557 cotangent in prop::array::uniform3(complex(1.0)),
558 ) {
559 check_adjoints([a, b, c], value, cotangent)?;
560 }
561
562 #[test]
563 fn coordinate_and_vector_pushforwards(
564 a in 0.2_f64..2.0,
565 b in -1.0_f64..1.0,
566 c in -1.0_f64..1.0,
567 value in prop::array::uniform3(complex(1.0)),
568 cotangent in prop::array::uniform3(complex(1.0)),
569 dp in prop::array::uniform3(-0.2_f64..0.2),
570 dv in prop::array::uniform3(complex(0.2)),
571 ) {
572 check_pushforwards([a, b, c], value, cotangent, dp, dv)?;
573 }
574 }
575
576 fn check_pushforwards(
579 cartesian: [f64; 3],
580 value: [Complex; 3],
581 cotangent: [Complex; 3],
582 dp: [f64; 3],
583 dv: [Complex; 3],
584 ) -> Result<(), TestCaseError> {
585 let h = 1e-6;
586 for (forward, inverse) in PAIRS {
587 for (transform, other) in [(forward, inverse), (inverse, forward)] {
588 let (position, value) = input(cartesian, value, transform);
589 let planar = transform.dimension() == 2;
590 let dp = if planar { [dp[0], dp[1], 0.0] } else { dp };
591 let dv = if planar {
592 [dv[0], dv[1], Complex::default()]
593 } else {
594 dv
595 };
596 let g = cotangent.map(|g| g.re);
597 let tangent = point_pushforward(position, transform, dp).unwrap();
598 let rotated = vector_pushforward(value, position, transform, dv, dp).unwrap();
599 let plus = std::array::from_fn(|i| position[i] + h * dp[i]);
600 let minus = std::array::from_fn(|i| position[i] - h * dp[i]);
601 let p_plus = point(plus, transform).unwrap();
602 let p_minus = point(minus, transform).unwrap();
603 let numeric: [f64; 3] =
604 std::array::from_fn(|i| (p_plus[i] - p_minus[i]) / (2.0 * h));
605 prop_assert_close!(tangent, numeric, 2e-8, "{transform:?}");
606 let v_plus = vector(
607 std::array::from_fn(|i| value[i] + h * dv[i]),
608 plus,
609 transform,
610 )
611 .unwrap();
612 let v_minus = vector(
613 std::array::from_fn(|i| value[i] - h * dv[i]),
614 minus,
615 transform,
616 )
617 .unwrap();
618 let numeric: [Complex; 3] =
619 std::array::from_fn(|i| (v_plus[i] - v_minus[i]) / (2.0 * h));
620 prop_assert_close!(rotated, numeric, 2e-8, "{transform:?}");
621 let gp = point_pullback(position, transform, g).unwrap();
622 let dot = |a: [f64; 3], b: [f64; 3]| {
623 a.into_iter().zip(b).map(|(a, b)| a * b).sum::<f64>()
624 };
625 prop_assert_close!(dot(tangent, g), dot(dp, gp), 1e-13, "{transform:?}");
626 let (gv, gp) = vector_pullback(value, position, transform, cotangent).unwrap();
627 prop_assert_close!(
628 re_dot(rotated, cotangent),
629 re_dot(dv, gv) + dot(dp, gp),
630 1e-13,
631 "{transform:?}"
632 );
633 let image = point(position, transform).unwrap();
634 let chained = point_pushforward(image, other, tangent).unwrap();
635 prop_assert_close!(chained, dp, 1e-13, "{transform:?}");
636 let image_value = vector(value, position, transform).unwrap();
637 let chained =
638 vector_pushforward(image_value, image, other, rotated, tangent).unwrap();
639 prop_assert_close!(chained, dv, 1e-13, "{transform:?}");
640 }
641 }
642 Ok(())
643 }
644
645 fn input(
648 position: [f64; 3],
649 value: [Complex; 3],
650 transform: Transform,
651 ) -> ([f64; 3], [Complex; 3]) {
652 let source = match transform {
653 CylToCar | CylToSph => Some(CarToCyl),
654 SphToCar | SphToCyl => Some(CarToSph),
655 PolToCar => Some(CarToPol),
656 CarToCyl | CarToSph | CarToPol => None,
657 };
658 let planar = transform.dimension() == 2;
659 let (position, value) = if planar {
660 (
661 [position[0], position[1], 0.0],
662 [value[0], value[1], Complex::default()],
663 )
664 } else {
665 (position, value)
666 };
667 match source {
668 Some(source) => (
669 point(position, source).unwrap(),
670 vector(value, position, source).unwrap(),
671 ),
672 None => (position, value),
673 }
674 }
675
676 fn slot_tolerances(transform: Transform, scale: f64) -> [f64; 3] {
679 let length = 1e-14 * scale + f64::MIN_POSITIVE;
680 match transform {
681 CarToCyl | SphToCyl | CarToPol => [length, 1e-14, length],
682 CarToSph | CylToSph => [length, 1e-14, 1e-14],
683 CylToCar | SphToCar | PolToCar => [length; 3],
684 }
685 }
686
687 #[allow(clippy::float_cmp)] fn check_charts(cartesian: [f64; 3], value: [Complex; 3]) -> Result<(), TestCaseError> {
692 let scale = cartesian.iter().map(|v| v.abs()).fold(0.0, f64::max);
693 for (forward, inverse) in PAIRS {
694 for (from, to) in [(forward, inverse), (inverse, forward)] {
695 let (position, value) = input(cartesian, value, from);
696 let image = point(position, from).unwrap();
697 let back = point(image, to).unwrap();
698 let tolerances = slot_tolerances(to, scale);
699 for ((back, position), tolerance) in back.into_iter().zip(position).zip(tolerances)
700 {
701 prop_assert_close!(back, position, tolerance, "{from:?} then {to:?}");
702 }
703 let rotated = vector(value, position, from).unwrap();
704 let norm = re_dot(value, value);
705 prop_assert_close!(re_dot(rotated, rotated), norm, 1e-14 * norm, "{from:?}");
706 let restored = vector(rotated, image, to).unwrap();
707 prop_assert_close!(restored, value, 1e-14 * norm.sqrt(), "{from:?} then {to:?}");
708 }
709 }
710 let cylindrical = point(cartesian, CarToCyl).unwrap();
711 let spherical = point(cartesian, CarToSph).unwrap();
712 prop_assert_eq!(point(cylindrical, CylToSph).unwrap(), spherical);
713 prop_assert_eq!(
714 point(point(spherical, SphToCyl).unwrap(), CylToCar).unwrap(),
715 point(spherical, SphToCar).unwrap()
716 );
717 let tolerance = 1e-14 * re_dot(value, value).sqrt();
718 let via_cylinder = vector(
719 vector(value, cartesian, CarToCyl).unwrap(),
720 cylindrical,
721 CylToSph,
722 );
723 prop_assert_close!(
724 via_cylinder.unwrap(),
725 vector(value, cartesian, CarToSph).unwrap(),
726 tolerance
727 );
728 let components = vector(value, spherical, SphToCyl).unwrap();
729 let cylinder = point(spherical, SphToCyl).unwrap();
730 let via_cylinder = vector(components, cylinder, CylToCar).unwrap();
731 prop_assert_close!(
732 via_cylinder,
733 vector(value, spherical, SphToCar).unwrap(),
734 tolerance
735 );
736 Ok(())
737 }
738
739 fn check_adjoints(
743 cartesian: [f64; 3],
744 value: [Complex; 3],
745 cotangent: [Complex; 3],
746 ) -> Result<(), TestCaseError> {
747 let h = 1e-6;
748 for (forward, inverse) in PAIRS {
749 for (transform, other) in [(forward, inverse), (inverse, forward)] {
750 let (position, value) = input(cartesian, value, transform);
751 let planar = transform.dimension() == 2;
752 let cotangent = if planar {
753 [cotangent[0], cotangent[1], Complex::default()]
754 } else {
755 cotangent
756 };
757 let g = cotangent.map(|g| g.re);
758 let point_g = point_pullback(position, transform, g).unwrap();
759 let (vector_g, position_g) =
760 vector_pullback(value, position, transform, cotangent).unwrap();
761 let rotated = vector(value, position, transform).unwrap();
762 prop_assert_close!(
763 re_dot(cotangent, rotated),
764 re_dot(vector_g, value),
765 1e-14 * (1.0 + re_dot(cotangent, cotangent) + re_dot(value, value)),
766 "{transform:?}"
767 );
768 let image = point(position, transform).unwrap();
769 let chained = point_pullback(
770 position,
771 transform,
772 point_pullback(image, other, g).unwrap(),
773 );
774 prop_assert_close!(
775 chained.unwrap(),
776 g,
777 1e-13 * (1.0 + g.iter().map(|v| v.abs()).sum::<f64>()),
778 "{transform:?}"
779 );
780 for axis in 0..transform.dimension() {
781 let step = |t: f64| {
782 let mut shifted = position;
783 shifted[axis] += t * h;
784 shifted
785 };
786 let numeric = (point(step(1.0), transform).unwrap().into_iter())
787 .zip(point(step(-1.0), transform).unwrap())
788 .zip(g)
789 .map(|((p, m), g)| g * (p - m) / (2.0 * h))
790 .sum::<f64>();
791 prop_assert_close!(
792 point_g[axis],
793 numeric,
794 2e-8 * (1.0 + numeric.abs()),
795 "{transform:?}"
796 );
797 let numeric = re_dot(
798 cotangent,
799 (0..3).map(|c| {
800 (vector(value, step(1.0), transform).unwrap()[c]
801 - vector(value, step(-1.0), transform).unwrap()[c])
802 / (2.0 * h)
803 }),
804 );
805 prop_assert_close!(
806 position_g[axis],
807 numeric,
808 2e-8 * (1.0 + numeric.abs()),
809 "{transform:?}"
810 );
811 }
812 }
813 }
814 Ok(())
815 }
816
817 #[test]
818 #[allow(clippy::float_cmp)] fn coordinate_axis_derivative_contract() {
820 assert!(point_pullback([0.0, 0.0, 1.0], CarToSph, [0.0, 0.0, 1.0]).is_err());
821 assert!(point_pullback([0.0, 0.0, 1e300], CarToSph, [0.0, 1e-30, 0.0]).is_err());
824 assert_eq!(
825 point_pullback([0.0, 0.0, -1.0], CarToSph, [1.0, 0.0, 0.0]).unwrap(),
826 [0.0, 0.0, -1.0]
827 );
828 assert_eq!(
829 point_pullback([0.0; 3], CarToPol, [0.0; 3]).unwrap(),
830 [0.0; 3]
831 );
832 assert!(point_pushforward([0.0, 0.0, 1.0], CarToSph, [1.0, 0.0, 0.0]).is_err());
833 assert!(point_pushforward([0.0; 3], CylToSph, [0.0, 0.0, 1.0]).is_err());
834 assert_eq!(
835 point_pushforward([0.0, 0.0, -1.0], CarToSph, [0.0, 0.0, 0.7]).unwrap(),
836 [-0.7, 0.0, 0.0]
837 );
838 assert_eq!(
839 point_pushforward([0.0; 3], CarToPol, [0.0; 3]).unwrap(),
840 [0.0; 3]
841 );
842 }
843}