1use std::collections::BTreeSet;
10
11use crate::astro::math::linear::{
12 dot4, invert_4x4_cofactor, mat4_vec4, normal_matrix_4_unweighted_row_outer,
13};
14use crate::astro::math::vec3;
15
16use crate::constants::{C_M_S, F_L1_HZ};
17use crate::id::GnssSatelliteId;
18use crate::observables::{predict, ObservableEphemerisSource, PredictOptions};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum VelocityObservable {
23 RangeRate,
25 Doppler,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct VelocityObservation {
33 pub satellite_id: GnssSatelliteId,
35 pub value: f64,
38 pub carrier_hz: f64,
40 pub sat_clock_drift_s_s: f64,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct VelocitySolveOptions {
47 pub observable: VelocityObservable,
49 pub light_time: bool,
51 pub sagnac: bool,
53}
54
55impl Default for VelocitySolveOptions {
56 fn default() -> Self {
57 Self {
58 observable: VelocityObservable::RangeRate,
59 light_time: true,
60 sagnac: true,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub struct VelocitySolution {
68 pub velocity_m_s: [f64; 3],
70 pub speed_m_s: f64,
72 pub clock_drift_s_s: f64,
74 pub residuals_m_s: Vec<(GnssSatelliteId, f64)>,
76 pub used_sats: Vec<GnssSatelliteId>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum VelocityError {
84 NoObservations,
86 TooFewSatellites { used: usize, required: usize },
88 SingularGeometry,
90 DuplicateObservation { satellite_id: GnssSatelliteId },
92 InvalidCarrier { satellite_id: GnssSatelliteId },
94 InvalidInput {
96 field: &'static str,
97 reason: &'static str,
98 },
99 InvalidObservation { satellite_id: GnssSatelliteId },
101 InvalidReceiverState,
103}
104
105impl core::fmt::Display for VelocityError {
106 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
107 match self {
108 Self::NoObservations => write!(f, "no observations"),
109 Self::TooFewSatellites { used, required } => {
110 write!(f, "too few satellites: {used}, required {required}")
111 }
112 Self::SingularGeometry => write!(f, "singular geometry"),
113 Self::DuplicateObservation { satellite_id } => {
114 write!(f, "duplicate observation for {satellite_id}")
115 }
116 Self::InvalidCarrier { satellite_id } => {
117 write!(f, "invalid carrier for {satellite_id}")
118 }
119 Self::InvalidInput { field, reason } => {
120 write!(f, "invalid velocity input {field}: {reason}")
121 }
122 Self::InvalidObservation { satellite_id } => {
123 write!(f, "invalid observation for {satellite_id}")
124 }
125 Self::InvalidReceiverState => write!(f, "invalid receiver state"),
126 }
127 }
128}
129
130impl std::error::Error for VelocityError {}
131
132#[derive(Debug, Clone, Copy)]
133struct Row {
134 sat: GnssSatelliteId,
135 h: [f64; 4],
136 y: f64,
137}
138
139pub fn doppler_to_range_rate(doppler_hz: f64, carrier_hz: f64) -> Result<f64, VelocityError> {
141 let doppler_hz = velocity_finite(doppler_hz, "doppler_hz")?;
142 let carrier_hz = velocity_positive(carrier_hz, "carrier_hz")?;
143 velocity_finite_output(-doppler_hz * C_M_S / carrier_hz, "range_rate_m_s")
144}
145
146pub fn range_rate_to_doppler(range_rate_m_s: f64, carrier_hz: f64) -> Result<f64, VelocityError> {
148 let range_rate_m_s = velocity_finite(range_rate_m_s, "range_rate_m_s")?;
149 let carrier_hz = velocity_positive(carrier_hz, "carrier_hz")?;
150 velocity_finite_output(-range_rate_m_s * carrier_hz / C_M_S, "doppler_hz")
151}
152
153pub fn solve(
155 source: &dyn ObservableEphemerisSource,
156 observations: &[VelocityObservation],
157 receiver_ecef_m: [f64; 3],
158 t_rx_j2000_s: f64,
159 options: VelocitySolveOptions,
160) -> Result<VelocitySolution, VelocityError> {
161 if observations.is_empty() {
162 return Err(VelocityError::NoObservations);
163 }
164
165 validate_receiver_state(receiver_ecef_m, t_rx_j2000_s)?;
166 ensure_no_duplicates(observations)?;
167 validate_observations(observations)?;
168 let rows = build_rows(source, observations, receiver_ecef_m, t_rx_j2000_s, options)?;
169 if rows.len() < 4 {
170 return Err(VelocityError::TooFewSatellites {
171 used: rows.len(),
172 required: 4,
173 });
174 }
175
176 let x = solve_normal_equations(&rows)?;
177 assemble_solution(x, &rows)
178}
179
180fn validate_receiver_state(
181 receiver_ecef_m: [f64; 3],
182 t_rx_j2000_s: f64,
183) -> Result<(), VelocityError> {
184 if receiver_ecef_m.iter().all(|value| value.is_finite()) && t_rx_j2000_s.is_finite() {
185 Ok(())
186 } else {
187 Err(VelocityError::InvalidReceiverState)
188 }
189}
190
191fn ensure_no_duplicates(observations: &[VelocityObservation]) -> Result<(), VelocityError> {
192 let mut seen = BTreeSet::new();
193 for obs in observations {
194 if !seen.insert(obs.satellite_id) {
195 return Err(VelocityError::DuplicateObservation {
196 satellite_id: obs.satellite_id,
197 });
198 }
199 }
200 Ok(())
201}
202
203fn validate_observations(observations: &[VelocityObservation]) -> Result<(), VelocityError> {
204 for obs in observations {
205 if !(obs.value.is_finite() && obs.sat_clock_drift_s_s.is_finite()) {
206 return Err(VelocityError::InvalidObservation {
207 satellite_id: obs.satellite_id,
208 });
209 }
210 }
211 Ok(())
212}
213
214fn velocity_finite(x: f64, field: &'static str) -> Result<f64, VelocityError> {
215 if x.is_finite() {
216 Ok(x)
217 } else {
218 Err(VelocityError::InvalidInput {
219 field,
220 reason: "not finite",
221 })
222 }
223}
224
225fn velocity_positive(x: f64, field: &'static str) -> Result<f64, VelocityError> {
226 let x = velocity_finite(x, field)?;
227 if x > 0.0 {
228 Ok(x)
229 } else {
230 Err(VelocityError::InvalidInput {
231 field,
232 reason: "not positive",
233 })
234 }
235}
236
237fn velocity_finite_output(value: f64, field: &'static str) -> Result<f64, VelocityError> {
238 if value.is_finite() {
239 Ok(value)
240 } else {
241 Err(VelocityError::InvalidInput {
242 field,
243 reason: "out of range",
244 })
245 }
246}
247
248fn build_rows(
249 source: &dyn ObservableEphemerisSource,
250 observations: &[VelocityObservation],
251 receiver_ecef_m: [f64; 3],
252 t_rx_j2000_s: f64,
253 options: VelocitySolveOptions,
254) -> Result<Vec<Row>, VelocityError> {
255 let predict_options = PredictOptions {
256 carrier_hz: F_L1_HZ,
257 light_time: options.light_time,
258 sagnac: options.sagnac,
259 };
260 let mut rows = Vec::with_capacity(observations.len());
261
262 for obs in observations {
263 let rho_dot_m_s = match options.observable {
264 VelocityObservable::RangeRate => obs.value,
265 VelocityObservable::Doppler => {
266 if !(obs.carrier_hz.is_finite() && obs.carrier_hz > 0.0) {
267 return Err(VelocityError::InvalidCarrier {
268 satellite_id: obs.satellite_id,
269 });
270 }
271 doppler_to_range_rate(obs.value, obs.carrier_hz).map_err(|error| match error {
272 VelocityError::InvalidInput {
273 field: "carrier_hz",
274 ..
275 } => VelocityError::InvalidCarrier {
276 satellite_id: obs.satellite_id,
277 },
278 _ => VelocityError::InvalidObservation {
279 satellite_id: obs.satellite_id,
280 },
281 })?
282 }
283 };
284
285 let Ok(predicted) = predict(
286 source,
287 obs.satellite_id,
288 receiver_ecef_m,
289 t_rx_j2000_s,
290 predict_options,
291 ) else {
292 continue;
293 };
294
295 let [ex, ey, ez] = predicted.los_unit;
296 let y = rho_dot_m_s - predicted.range_rate_m_s + C_M_S * obs.sat_clock_drift_s_s;
297 if ![ex, ey, ez, predicted.range_rate_m_s, y]
298 .iter()
299 .all(|value| value.is_finite())
300 {
301 return Err(VelocityError::InvalidInput {
302 field: "velocity row",
303 reason: "out of range",
304 });
305 }
306 rows.push(Row {
307 sat: obs.satellite_id,
308 h: [-ex, -ey, -ez, 1.0],
309 y,
310 });
311 }
312
313 Ok(rows)
314}
315
316#[allow(clippy::needless_range_loop)] fn solve_normal_equations(rows: &[Row]) -> Result<[f64; 4], VelocityError> {
318 let mut aty = [0.0_f64; 4];
319
320 for row in rows {
321 for i in 0..4 {
322 aty[i] += row.h[i] * row.y;
323 }
324 }
325 let row_h: Vec<[f64; 4]> = rows.iter().map(|row| row.h).collect();
326 let ata = normal_matrix_4_unweighted_row_outer(&row_h);
327
328 let inv = invert_4x4_cofactor(&ata).ok_or(VelocityError::SingularGeometry)?;
329 let solution = mat4_vec4(&inv, &aty);
330 if solution.iter().all(|value| value.is_finite()) {
331 Ok(solution)
332 } else {
333 Err(VelocityError::InvalidInput {
334 field: "velocity solution",
335 reason: "out of range",
336 })
337 }
338}
339
340fn assemble_solution(x: [f64; 4], rows: &[Row]) -> Result<VelocitySolution, VelocityError> {
341 let velocity_m_s = [x[0], x[1], x[2]];
342 let speed_m_s = vec3::norm3(velocity_m_s);
343 let clock_drift_s_s = x[3] / C_M_S;
344 let residuals_m_s: Vec<_> = rows
345 .iter()
346 .map(|row| (row.sat, row.y - hx(&row.h, &x)))
347 .collect();
348 if !velocity_m_s.iter().all(|value| value.is_finite())
349 || !speed_m_s.is_finite()
350 || !clock_drift_s_s.is_finite()
351 || !residuals_m_s
352 .iter()
353 .all(|(_, residual)| residual.is_finite())
354 {
355 return Err(VelocityError::InvalidInput {
356 field: "velocity solution",
357 reason: "out of range",
358 });
359 }
360 let used_sats = rows.iter().map(|row| row.sat).collect();
361 Ok(VelocitySolution {
362 velocity_m_s,
363 speed_m_s,
364 clock_drift_s_s,
365 residuals_m_s,
366 used_sats,
367 })
368}
369
370fn hx(h: &[f64; 4], x: &[f64; 4]) -> f64 {
371 dot4(h, x)
372}
373
374#[cfg(all(test, sidereon_repo_tests))]
375mod tests {
376 use super::*;
377 use crate::ephemeris::Sp3;
378 use crate::observables::{
379 j2000_seconds_from_split, predict, ObservableState, ObservablesError,
380 };
381 use crate::{GnssSatelliteId, GnssSystem};
382
383 const T_RX_J2000_S: f64 = 646_272_000.0;
384 const RECEIVER: [f64; 3] = [4_500_000.0, 500_000.0, 4_500_000.0];
385 const V_TRUE: [f64; 3] = [12.0, -7.0, 3.0];
386 const DRIFT_TRUE: f64 = 1.0e-9;
387
388 fn sp3_fixture() -> Sp3 {
389 let path = concat!(
390 env!("CARGO_MANIFEST_DIR"),
391 "/tests/fixtures/sp3/GRG0MGXFIN_20201760000_01D_15M_ORB.SP3"
392 );
393 let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read SP3 fixture {path}: {e}"));
394 Sp3::parse(&bytes).expect("parse SP3 fixture")
395 }
396
397 fn visible_gps(sp3: &Sp3) -> Vec<GnssSatelliteId> {
398 let planning = PredictOptions {
399 light_time: false,
400 ..PredictOptions::default()
401 };
402 sp3.satellites()
403 .iter()
404 .copied()
405 .filter(|sat| sat.system == GnssSystem::Gps)
406 .filter(|sat| {
407 predict(sp3, *sat, RECEIVER, T_RX_J2000_S, planning)
408 .map(|obs| obs.elevation_deg >= 5.0)
409 .unwrap_or(false)
410 })
411 .collect()
412 }
413
414 fn synth_range_rate(sp3: &Sp3, sat: GnssSatelliteId, v_true: [f64; 3], drift: f64) -> f64 {
415 let obs = predict(sp3, sat, RECEIVER, T_RX_J2000_S, PredictOptions::default())
416 .expect("predict synthetic observation");
417 let e_dot_vtrue =
418 obs.los_unit[0] * v_true[0] + obs.los_unit[1] * v_true[1] + obs.los_unit[2] * v_true[2];
419 obs.range_rate_m_s - e_dot_vtrue + C_M_S * drift
420 }
421
422 fn synth_observations(sp3: &Sp3, sats: &[GnssSatelliteId]) -> Vec<VelocityObservation> {
423 sats.iter()
424 .map(|&sat| VelocityObservation {
425 satellite_id: sat,
426 value: synth_range_rate(sp3, sat, V_TRUE, DRIFT_TRUE),
427 carrier_hz: F_L1_HZ,
428 sat_clock_drift_s_s: 0.0,
429 })
430 .collect()
431 }
432
433 #[derive(Debug, Clone, Copy)]
434 struct StaticVelocitySource {
435 state: ObservableState,
436 }
437
438 impl ObservableEphemerisSource for StaticVelocitySource {
439 fn observable_state_at_j2000_s(
440 &self,
441 _sat: GnssSatelliteId,
442 _t_j2000_s: f64,
443 ) -> Result<ObservableState, ObservablesError> {
444 Ok(self.state)
445 }
446 }
447
448 fn static_velocity_source(position_ecef_m: [f64; 3]) -> StaticVelocitySource {
449 StaticVelocitySource {
450 state: ObservableState {
451 position_ecef_m,
452 clock_s: Some(0.0),
453 },
454 }
455 }
456
457 #[test]
458 fn split_epoch_constant_matches_orbis_velocity_fixture() {
459 assert_eq!(
460 j2000_seconds_from_split(2_459_024.5, 0.5).expect("valid split Julian date"),
461 T_RX_J2000_S
462 );
463 }
464
465 #[test]
466 fn range_rate_solve_has_frozen_bits_golden() {
467 let sp3 = sp3_fixture();
468 let sats = visible_gps(&sp3);
469 assert!(sats.len() >= 4);
470 let observations = synth_observations(&sp3, &sats);
471
472 let solution = solve(
473 &sp3,
474 &observations,
475 RECEIVER,
476 T_RX_J2000_S,
477 VelocitySolveOptions::default(),
478 )
479 .expect("solve velocity");
480
481 assert_eq!(
482 solution.velocity_m_s.map(f64::to_bits),
483 [0x4028000000000000, 0xc01c000000000016, 0x4007ffffffffff00]
484 );
485 assert_eq!(solution.speed_m_s.to_bits(), 0x402c6ce322982a37);
486 assert_eq!(solution.clock_drift_s_s.to_bits(), 0x3e112e0be826d2ee);
487 assert_eq!(
488 solution
489 .used_sats
490 .iter()
491 .map(ToString::to_string)
492 .collect::<Vec<_>>(),
493 ["G07", "G08", "G10", "G16", "G18", "G20", "G21", "G26", "G27"]
494 );
495 assert_eq!(
496 solution
497 .residuals_m_s
498 .iter()
499 .map(|(_, residual)| residual.to_bits())
500 .collect::<Vec<_>>(),
501 [
502 0xbd01000000000000,
503 0xbd24000000000000,
504 0x3cfc000000000000,
505 0xbd16000000000000,
506 0xbd1a800000000000,
507 0x3cf0000000000000,
508 0xbd14000000000000,
509 0x3d31800000000000,
510 0x3d18000000000000,
511 ]
512 );
513 }
514
515 #[test]
516 fn doppler_path_has_frozen_bits_with_per_sat_carriers() {
517 let sp3 = sp3_fixture();
518 let sats = visible_gps(&sp3);
519 let range_rate_observations = synth_observations(&sp3, &sats);
520 let doppler_observations: Vec<_> = range_rate_observations
521 .iter()
522 .enumerate()
523 .map(|(idx, obs)| {
524 let k = (idx % 14) as i8 - 7;
525 let carrier_hz =
526 crate::frequencies::rinex_band_frequency_hz(GnssSystem::Glonass, '1', Some(k))
527 .expect("canonical GLONASS G1 channel carrier exists");
528 VelocityObservation {
529 value: range_rate_to_doppler(obs.value, carrier_hz)
530 .expect("valid range-rate conversion"),
531 carrier_hz,
532 ..*obs
533 }
534 })
535 .collect();
536
537 let range_rate = solve(
538 &sp3,
539 &range_rate_observations,
540 RECEIVER,
541 T_RX_J2000_S,
542 VelocitySolveOptions::default(),
543 )
544 .expect("range-rate solve");
545 let doppler = solve(
546 &sp3,
547 &doppler_observations,
548 RECEIVER,
549 T_RX_J2000_S,
550 VelocitySolveOptions {
551 observable: VelocityObservable::Doppler,
552 ..VelocitySolveOptions::default()
553 },
554 )
555 .expect("doppler solve");
556
557 assert_eq!(
558 range_rate.velocity_m_s.map(f64::to_bits),
559 [0x4028000000000000, 0xc01c000000000016, 0x4007ffffffffff00]
560 );
561 assert_eq!(
562 doppler.velocity_m_s.map(f64::to_bits),
563 [0x402800000000000c, 0xc01c00000000000f, 0x4007ffffffffff60]
564 );
565 assert_eq!(doppler.speed_m_s.to_bits(), 0x402c6ce322982a44);
566 assert_eq!(doppler.clock_drift_s_s.to_bits(), 0x3e112e0be826d4b8);
567 assert_eq!(
568 doppler
569 .residuals_m_s
570 .iter()
571 .map(|(_, residual)| residual.to_bits())
572 .collect::<Vec<_>>(),
573 [
574 0x3d24c00000000000,
575 0xbd2b000000000000,
576 0xbd00000000000000,
577 0xbd00000000000000,
578 0xbd0b000000000000,
579 0x3d06000000000000,
580 0x0000000000000000,
581 0x3d40c00000000000,
582 0x3d22000000000000,
583 ]
584 );
585 }
586
587 #[test]
588 fn validates_core_error_cases() {
589 let sp3 = sp3_fixture();
590 let sats = visible_gps(&sp3);
591 let mut observations = synth_observations(&sp3, &sats);
592 let first = observations[0].satellite_id;
593
594 assert_eq!(
595 solve(
596 &sp3,
597 &[],
598 RECEIVER,
599 T_RX_J2000_S,
600 VelocitySolveOptions::default()
601 ),
602 Err(VelocityError::NoObservations)
603 );
604
605 assert_eq!(
606 solve(
607 &sp3,
608 &observations[..3],
609 RECEIVER,
610 T_RX_J2000_S,
611 VelocitySolveOptions::default()
612 ),
613 Err(VelocityError::TooFewSatellites {
614 used: 3,
615 required: 4
616 })
617 );
618
619 observations[1].satellite_id = first;
620 assert_eq!(
621 solve(
622 &sp3,
623 &observations,
624 RECEIVER,
625 T_RX_J2000_S,
626 VelocitySolveOptions::default()
627 ),
628 Err(VelocityError::DuplicateObservation {
629 satellite_id: first
630 })
631 );
632
633 let invalid_carrier = [VelocityObservation {
634 satellite_id: first,
635 value: 1.0,
636 carrier_hz: -1.0,
637 sat_clock_drift_s_s: 0.0,
638 }];
639 assert_eq!(
640 solve(
641 &sp3,
642 &invalid_carrier,
643 RECEIVER,
644 T_RX_J2000_S,
645 VelocitySolveOptions {
646 observable: VelocityObservable::Doppler,
647 ..VelocitySolveOptions::default()
648 }
649 ),
650 Err(VelocityError::InvalidCarrier {
651 satellite_id: first
652 })
653 );
654 }
655
656 #[test]
657 fn rejects_non_finite_velocity_inputs() {
658 let sp3 = sp3_fixture();
659 let sats = visible_gps(&sp3);
660 let mut observations = synth_observations(&sp3, &sats);
661 let first = observations[0].satellite_id;
662
663 observations[0].value = f64::NAN;
664 assert_eq!(
665 solve(
666 &sp3,
667 &observations,
668 RECEIVER,
669 T_RX_J2000_S,
670 VelocitySolveOptions::default()
671 ),
672 Err(VelocityError::InvalidObservation {
673 satellite_id: first
674 })
675 );
676
677 observations[0].value = 0.0;
678 observations[0].sat_clock_drift_s_s = f64::NAN;
679 assert_eq!(
680 solve(
681 &sp3,
682 &observations,
683 RECEIVER,
684 T_RX_J2000_S,
685 VelocitySolveOptions::default()
686 ),
687 Err(VelocityError::InvalidObservation {
688 satellite_id: first
689 })
690 );
691
692 observations[0].sat_clock_drift_s_s = 0.0;
693 let mut bad_receiver = RECEIVER;
694 bad_receiver[0] = f64::NAN;
695 assert_eq!(
696 solve(
697 &sp3,
698 &observations,
699 bad_receiver,
700 T_RX_J2000_S,
701 VelocitySolveOptions::default()
702 ),
703 Err(VelocityError::InvalidReceiverState)
704 );
705
706 assert_eq!(
707 solve(
708 &sp3,
709 &observations,
710 RECEIVER,
711 f64::NAN,
712 VelocitySolveOptions::default()
713 ),
714 Err(VelocityError::InvalidReceiverState)
715 );
716 }
717
718 #[test]
719 fn conversion_helpers_reject_invalid_domains() {
720 assert_eq!(
721 doppler_to_range_rate(f64::NAN, F_L1_HZ),
722 Err(VelocityError::InvalidInput {
723 field: "doppler_hz",
724 reason: "not finite"
725 })
726 );
727 assert_eq!(
728 range_rate_to_doppler(f64::INFINITY, F_L1_HZ),
729 Err(VelocityError::InvalidInput {
730 field: "range_rate_m_s",
731 reason: "not finite"
732 })
733 );
734
735 for carrier_hz in [f64::NAN, f64::INFINITY] {
736 assert_eq!(
737 doppler_to_range_rate(1.0, carrier_hz),
738 Err(VelocityError::InvalidInput {
739 field: "carrier_hz",
740 reason: "not finite"
741 })
742 );
743 assert_eq!(
744 range_rate_to_doppler(1.0, carrier_hz),
745 Err(VelocityError::InvalidInput {
746 field: "carrier_hz",
747 reason: "not finite"
748 })
749 );
750 }
751
752 for carrier_hz in [0.0, -1.0] {
753 assert_eq!(
754 doppler_to_range_rate(1.0, carrier_hz),
755 Err(VelocityError::InvalidInput {
756 field: "carrier_hz",
757 reason: "not positive"
758 })
759 );
760 assert_eq!(
761 range_rate_to_doppler(1.0, carrier_hz),
762 Err(VelocityError::InvalidInput {
763 field: "carrier_hz",
764 reason: "not positive"
765 })
766 );
767 }
768 }
769
770 #[test]
771 fn solve_rejects_non_finite_internal_rows() {
772 let source = static_velocity_source([20_200_000.0, 14_000_000.0, 21_700_000.0]);
773 let observations: Vec<_> = (1..=4)
774 .map(|prn| VelocityObservation {
775 satellite_id: GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid sat"),
776 value: 0.0,
777 carrier_hz: F_L1_HZ,
778 sat_clock_drift_s_s: f64::MAX,
779 })
780 .collect();
781
782 assert_eq!(
783 solve(
784 &source,
785 &observations,
786 [0.0, 0.0, 0.0],
787 646_272_000.0,
788 VelocitySolveOptions {
789 light_time: false,
790 sagnac: false,
791 ..VelocitySolveOptions::default()
792 }
793 ),
794 Err(VelocityError::InvalidInput {
795 field: "velocity row",
796 reason: "out of range",
797 })
798 );
799 }
800}