Skip to main content

copp\path/
mod.rs

1//! Path abstraction for trajectory planning.
2//!
3//! The main entry point is [`Path`]. Pick the constructor by the path data you own:
4//!
5//! | You have | Use | Notes |
6//! |---|---|---|
7//! | Analytic formula `q(s)` | [`Path::from_parametric`](crate::path::Path::from_parametric) | Computes derivatives up to third order by [`Jet3`] automatic differentiation. |
8//! | Waypoints the path must pass through | [`Path::from_waypoints_interpolating`](crate::path::Path::from_waypoints_interpolating) | Interpolates every column of a `(dim x n_points)` waypoint matrix. |
9//! | A reference polyline that may be approximated | [`Path::from_waypoints_fitting`](crate::path::Path::from_waypoints_fitting) | `C4` quintic fitting; per-axis tolerance defaults to `0.001`. |
10//! | Explicit `q/dq/ddq` evaluator | [`Path::from_evaluator_2nd`](crate::path::Path::from_evaluator_2nd) | Use for TOPP2/COPP2 or other workflows that do not need jerk. See [`PathEvaluator2nd`]. |
11//! | Explicit `q/dq/ddq/dddq` evaluator | [`Path::from_evaluator_3rd`](crate::path::Path::from_evaluator_3rd) | Use for TOPP3/COPP3 workflows. See [`PathEvaluator3rd`]. |
12//!
13//! The waypoint constructor families also provide borrowed-view companions:
14//! [`Path::from_waypoints_interpolating_view`] and
15//! [`Path::from_waypoints_fitting_view`]. They accept compatible strided
16//! column-major nalgebra views without requiring callers to first materialize a
17//! `DMatrix`.
18//!
19//! # Interpolating versus fitting waypoint paths
20//!
21//! [`Path::from_waypoints_interpolating`](crate::path::Path::from_waypoints_interpolating)
22//! constrains the path to pass through **every** input waypoint at its assigned
23//! parameter, up to floating-point roundoff. Choose it when each waypoint is a
24//! position or event that must be retained.
25//!
26//! [`Path::from_waypoints_fitting`](crate::path::Path::from_waypoints_fitting)
27//! instead treats adjacent columns as a reference polyline under one common
28//! parameter. The fitted curve is **not** required to pass through interior
29//! waypoints: selected axes may deviate from them within their configured
30//! tolerances. Their absolute error at the same parameter is numerically audited
31//! over every complete reference interval, while endpoint positions are retained
32//! up to floating-point roundoff. Configure axis selection, parameters,
33//! tolerances, and construction budgets with [`SmoothingConfig`] and
34//! [`SmoothingTolerance`]; inspect final bounds and refinement work through
35//! [`SmoothingReport`].
36//!
37//! Fitting is geometric preprocessing, not time parameterization. Values from
38//! the evaluation methods below are derivatives with respect to `s`; physical
39//! velocity, acceleration, and jerk also depend on the later time law `s(t)`.
40//!
41//! # API stability
42//!
43//! The waypoint interpolation and fitting constructor families, including
44//! their `_view` variants, are currently **unstable**. As additional algorithms
45//! are introduced, their names, signatures, configuration types, and
46//! method-selection interfaces may change in future releases.
47//!
48//! Once built, call one of the evaluation methods with a one-dimensional parameter slice:
49//!
50//! | Method | Output |
51//! |---|---|
52//! | [`Path::evaluate_q`](crate::path::Path::evaluate_q)         | position `q` only |
53//! | [`Path::evaluate_up_to_2nd`](crate::path::Path::evaluate_up_to_2nd) | `q`, `dq`, `ddq` |
54//! | [`Path::evaluate_up_to_3rd`](crate::path::Path::evaluate_up_to_3rd) | `q`, `dq`, `ddq`, `dddq` |
55
56pub mod autodiff;
57mod path_core;
58mod smoothing;
59pub mod spline;
60
61pub use autodiff::{Jet3, cos, exp, ln, powi, sin, sqrt};
62pub use path_core::{
63    ParametricFn, Path, PathDerivatives, PathEvaluator, PathEvaluator2nd, PathEvaluator3rd,
64};
65pub use smoothing::{SmoothingConfig, SmoothingReport, SmoothingTolerance};
66pub use spline::{Parametrization, SplineConfig};
67
68#[cfg(test)]
69use crate::diag::PathError;
70
71/// Policy for handling path queries outside the configured `s` range.
72#[derive(Clone, Copy, Debug)]
73pub enum OutOfRangeMode {
74    /// Return an error when s is outside `[s_min, s_max]`.
75    Error,
76    /// Silently clamp s to `[s_min, s_max]`.
77    Clamp,
78}
79
80/// Build a random Lissajous analytic path.
81///
82/// This helper is intended for cross-module unit tests to avoid repeating the
83/// same hand-written path construction logic.
84#[cfg(test)]
85#[allow(clippy::type_complexity)]
86pub(crate) fn lissajous_path_for_test(
87    dim: usize,
88    s_len: usize,
89    rng: &mut impl rand::RngExt,
90) -> Result<(nalgebra::DMatrix<f64>, Path, Vec<f64>, Vec<f64>), PathError> {
91    let omega = (0..dim)
92        .map(|_| rng.random_range(0.1..(2.0 * std::f64::consts::PI)))
93        .collect::<Vec<f64>>();
94    let phi = (0..dim)
95        .map(|_| rng.random_range(0.0..(2.0 * std::f64::consts::PI)))
96        .collect::<Vec<f64>>();
97
98    let (s, path) = lissajous_path_fixed_for_test(dim, s_len, omega.clone(), phi.clone())?;
99
100    Ok((s, path, omega, phi))
101}
102
103/// Build a Lissajous analytic path from fixed frequencies/phases.
104///
105/// This helper is intended for cross-module unit tests to avoid repeating the
106/// same hand-written path construction logic.
107#[cfg(test)]
108pub(crate) fn lissajous_path_fixed_for_test(
109    dim: usize,
110    s_len: usize,
111    omega: Vec<f64>,
112    phi: Vec<f64>,
113) -> Result<(nalgebra::DMatrix<f64>, Path), PathError> {
114    let s = nalgebra::DMatrix::<f64>::from_fn(1, s_len, |_, j| j as f64 / (s_len - 1) as f64);
115
116    let path = Path::from_parametric(
117        move |s: Jet3| {
118            (0..dim)
119                .map(|i| sin(omega[i] * s + phi[i]))
120                .collect::<Vec<Jet3>>()
121        },
122        0.0,
123        1.0,
124    )?;
125
126    Ok((s, path))
127}
128
129/// Add symmetric axial limits (`+limit` / `-limit`) to all currently stored stations.
130///
131/// The helper applies velocity, acceleration and jerk constraints in one call.
132#[cfg(test)]
133pub(crate) fn add_symmetric_axial_limits_for_test<M: crate::robot::robot_core::RobotBasic>(
134    robot: &mut crate::robot::robot_core::Robot<M>,
135    vel_limit: f64,
136    acc_limit: f64,
137    jerk_limit: Option<f64>,
138) -> Result<(), crate::diag::ConstraintError> {
139    let dim = robot.dim();
140    let n = robot.constraints.len();
141    if n == 0 {
142        return Ok(());
143    }
144
145    let vel_max = vec![vel_limit; dim];
146    let vel_min = vec![-vel_limit; dim];
147
148    let acc_max = vec![acc_limit; dim];
149    let acc_min = vec![-acc_limit; dim];
150
151    let robot = robot
152        .with_axial_velocity((vel_max.as_slice(), n), (vel_min.as_slice(), n), 0)?
153        .with_axial_acceleration((acc_max.as_slice(), n), (acc_min.as_slice(), n), 0)?;
154
155    if let Some(jerk_limit) = jerk_limit {
156        let jerk_max = vec![jerk_limit; dim];
157        let jerk_min = vec![-jerk_limit; dim];
158        robot.with_axial_jerk((jerk_max.as_slice(), n), (jerk_min.as_slice(), n), 0)?;
159    }
160
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
168    use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
169    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
170    use crate::copp::copp3::stable::basic::{
171        Topp3ProblemBuilder, Topp3Profile, s_to_t_topp3, t_to_s_topp3,
172    };
173    use crate::copp::copp3::stable::topp3_lp::topp3_lp;
174    use crate::copp::{ClarabelOptionsBuilder, InterpolationMode};
175    use crate::diag::ConstraintError;
176    use crate::diag::Verbosity;
177    use crate::robot::robot_core::Robot;
178    use nalgebra::DMatrix;
179    use plotters::prelude::*;
180    use rand::RngExt;
181    use std::error::Error;
182    use std::fs::create_dir_all;
183    use std::path::Path;
184    use std::time::Instant;
185
186    const DIM: usize = 6;
187    const N: usize = 1000;
188
189    #[test]
190    fn test_topp3_with_spline_path() -> Result<(), Box<dyn Error>> {
191        crate::verbosity_log!(
192            crate::diag::Verbosity::Summary,
193            "\n=== TOPP3 with Spline Path (6-DOF) ==="
194        );
195
196        let n_waypoints = 10;
197        let start = Instant::now();
198        let waypoints = make_random_waypoints(DIM, n_waypoints);
199        let sample_ms = start.elapsed().as_secs_f64() * 1e3;
200
201        let start = Instant::now();
202        let path = super::Path::from_waypoints_interpolating(&waypoints, SplineConfig::default())?;
203        let build_ms = start.elapsed().as_secs_f64() * 1e3;
204
205        let s = make_s_vector(N);
206        let start = Instant::now();
207        let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
208        let eval_ms = start.elapsed().as_secs_f64() * 1e3;
209
210        crate::verbosity_log!(
211            crate::diag::Verbosity::Summary,
212            "[Waypoint Sample] {sample_ms:.3} ms, [Spline Build] {build_ms:.3} ms, [Spline Eval] {eval_ms:.3} ms"
213        );
214
215        let mut robot = setup_robot_with_constraints(&s, &derivs)?;
216        let s_slice: Vec<f64> = (0..N).map(|j| s[(0, j)]).collect();
217        let (profile, timings) = solve_topp_pipeline(&mut robot, &s_slice)?;
218
219        crate::verbosity_log!(
220            crate::diag::Verbosity::Summary,
221            "[TOPP2-RA] {:.3} ms, t_motion = {:.3} s",
222            timings.topp2_ms,
223            timings.t_motion_2
224        );
225        crate::verbosity_log!(
226            crate::diag::Verbosity::Summary,
227            "[TOPP3-LP] {:.3} ms, t_motion = {:.3} s",
228            timings.topp3_ms,
229            timings.t_motion_3
230        );
231
232        let (t, q_t, dq_t, ddq_t, dddq_t, interp_ms) =
233            interpolate_to_time_domain(&path, &s_slice, &profile, 1e-3)?;
234        crate::verbosity_log!(
235            crate::diag::Verbosity::Summary,
236            "[Interpolation] {:.3} ms, {} samples",
237            interp_ms,
238            t.len()
239        );
240
241        plot_topp3_grid(
242            "data/path_topp_plots/spline_path_topp3.png",
243            "TOPP3 Result: Spline Path (6-DOF, 50 waypoints)",
244            &t,
245            &q_t,
246            &dq_t,
247            &ddq_t,
248            &dddq_t,
249        )?;
250
251        Ok(())
252    }
253
254    fn make_s_vector(n: usize) -> DMatrix<f64> {
255        DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
256    }
257
258    fn make_parametric_path_6dof() -> Result<super::Path, PathError> {
259        super::Path::from_parametric(
260            |s: Jet3| {
261                vec![
262                    sin(s),
263                    cos(s),
264                    exp(0.3 * s) - 1.0,
265                    s + 0.1 * s * s - 0.01 * s * s * s * s,
266                    sin(2.0 * s) + 0.15 * cos(3.0 * s),
267                    sin(s) * cos(s),
268                ]
269            },
270            0.0,
271            1.0,
272        )
273    }
274
275    fn make_random_waypoints(dim: usize, n_pts: usize) -> DMatrix<f64> {
276        let mut rng = rand::rng();
277        // Build a random-walk waypoint matrix: each row is one dimension,
278        // each column is a waypoint; steps are small bounded increments.
279        let mut waypoints = DMatrix::<f64>::zeros(dim, n_pts);
280        for mut row in waypoints.row_iter_mut() {
281            row[0] = rng.random_range(-1.0..1.0);
282            for j in 1..n_pts {
283                let step = rng.random_range(-0.5..0.5);
284                row[j] = row[j - 1] + step;
285            }
286        }
287        waypoints
288    }
289
290    fn setup_robot_with_constraints(
291        s: &DMatrix<f64>,
292        derivs: &PathDerivatives,
293    ) -> Result<Robot<usize>, ConstraintError> {
294        let n = s.ncols();
295        let mut robot = Robot::with_capacity(DIM, n);
296
297        let vel_max = vec![1.0; DIM];
298        let vel_min = vec![-1.0; DIM];
299        let acc_max = vec![1.0; DIM];
300        let acc_min = vec![-1.0; DIM];
301        let jerk_max = vec![5.0; DIM];
302        let jerk_min = vec![-5.0; DIM];
303        robot
304            .with_s(&s.as_view())?
305            .with_q(
306                &derivs.q.as_view(),
307                &derivs.dq.as_ref().unwrap().as_view(),
308                &derivs.ddq.as_ref().unwrap().as_view(),
309                derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
310                0,
311            )?
312            .with_axial_velocity((vel_max.as_slice(), n), (vel_min.as_slice(), n), 0)?
313            .with_axial_acceleration((acc_max.as_slice(), n), (acc_min.as_slice(), n), 0)?
314            .with_axial_jerk((jerk_max.as_slice(), n), (jerk_min.as_slice(), n), 0)?;
315
316        Ok(robot)
317    }
318
319    struct ToppTimings {
320        topp2_ms: f64,
321        topp3_ms: f64,
322        t_motion_2: f64,
323        t_motion_3: f64,
324    }
325
326    fn solve_topp_pipeline(
327        robot: &mut Robot<usize>,
328        s_slice: &[f64],
329    ) -> Result<(Topp3Profile, ToppTimings), Box<dyn Error>> {
330        let n = s_slice.len();
331
332        let topp2_problem = Topp2ProblemBuilder::new(robot, (0, n - 1), (0.0, 0.0)).build()?;
333
334        let options = ReachSet2OptionsBuilder::new()
335            .a_cmp_abs_tol(1e-9)
336            .a_cmp_rel_tol(1e-9)
337            .lp_feas_tol(1e-9)
338            .verbosity(Verbosity::Silent)
339            .build()?;
340
341        let start = Instant::now();
342        let a_ra = topp2_ra(&topp2_problem, &options)?;
343        let topp2_ms = start.elapsed().as_secs_f64() * 1e3;
344        let (t_motion_2, _) = s_to_t_topp2(s_slice, &a_ra, 0.0)?;
345
346        robot.constraints.amax_substitute(&a_ra, 0)?;
347
348        let topp3_problem = Topp3ProblemBuilder::new(robot, 0, &a_ra, (0.0, 0.0), (0.0, 0.0))
349            .with_num_stationary_max(2)
350            .build_with_linearization()?;
351        let options_lp = ClarabelOptionsBuilder::new()
352            .allow_almost_solved(true)
353            .build()?;
354
355        let start = Instant::now();
356        let profile = topp3_lp(&topp3_problem, &options_lp)?;
357        let topp3_ms = start.elapsed().as_secs_f64() * 1e3;
358        let (t_motion_3, _) = s_to_t_topp3(s_slice, profile.as_parts(), 0.0)?;
359
360        Ok((
361            profile,
362            ToppTimings {
363                topp2_ms,
364                topp3_ms,
365                t_motion_2,
366                t_motion_3,
367            },
368        ))
369    }
370
371    #[allow(clippy::type_complexity)]
372    fn interpolate_to_time_domain(
373        path: &super::Path,
374        s_slice: &[f64],
375        profile: &Topp3Profile,
376        dt: f64,
377    ) -> Result<
378        (
379            Vec<f64>,
380            DMatrix<f64>,
381            DMatrix<f64>,
382            DMatrix<f64>,
383            DMatrix<f64>,
384            f64,
385        ),
386        Box<dyn Error>,
387    > {
388        let start = Instant::now();
389        let (t_final, t_s) = s_to_t_topp3(s_slice, profile.as_parts(), 0.0)?;
390
391        let t_sample: Vec<f64> = (0..=((t_final / dt).floor() as usize))
392            .map(|i| i as f64 * dt)
393            .collect();
394
395        let s_t = t_to_s_topp3(
396            s_slice,
397            profile.as_parts(),
398            &t_s,
399            InterpolationMode::NonUniformTimeGrid(&t_sample),
400        )?;
401
402        // Evaluate q(s(t)): only position needed, derivatives computed via finite diff
403        let n_t = t_sample.len();
404        let s_t_matrix = DMatrix::<f64>::from_row_slice(1, n_t, &s_t);
405        let q_t = path.evaluate_q(s_t_matrix.as_slice())?.q;
406
407        // Compute time-domain derivatives using finite differences
408        let mut dq_t = DMatrix::<f64>::zeros(DIM, n_t);
409        let mut ddq_t = DMatrix::<f64>::zeros(DIM, n_t);
410        let mut dddq_t = DMatrix::<f64>::zeros(DIM, n_t);
411
412        // Compute dq_t, ddq_t, dddq_t via central finite differences (forward/backward at ends).
413        finite_diff_inplace(&q_t, &mut dq_t, dt);
414        finite_diff_inplace(&dq_t, &mut ddq_t, dt);
415        finite_diff_inplace(&ddq_t, &mut dddq_t, dt);
416
417        let interp_ms = start.elapsed().as_secs_f64() * 1e3;
418
419        Ok((t_sample, q_t, dq_t, ddq_t, dddq_t, interp_ms))
420    }
421
422    fn plot_topp3_grid(
423        filename: &str,
424        title: &str,
425        t: &[f64],
426        q_t: &DMatrix<f64>,
427        dq_t: &DMatrix<f64>,
428        ddq_t: &DMatrix<f64>,
429        dddq_t: &DMatrix<f64>,
430    ) -> Result<(), Box<dyn Error>> {
431        if let Some(parent) = Path::new(filename).parent()
432            && !parent.as_os_str().is_empty()
433        {
434            create_dir_all(parent)?;
435        }
436
437        let root = BitMapBackend::new(filename, (2400, 1600)).into_drawing_area();
438        root.fill(&WHITE)?;
439
440        let areas = root.split_evenly((4, DIM));
441        let mats = [q_t, dq_t, ddq_t, dddq_t];
442        let labels = ["q(t)", "dq/dt", "d²q/dt²", "d³q/dt³"];
443        let constraints_bounds = [
444            None,
445            Some((-1.0, 1.0)),
446            Some((-1.0, 1.0)),
447            Some((-5.0, 5.0)),
448        ];
449
450        for row in 0..4 {
451            for col in 0..DIM {
452                let area = &areas[row * DIM + col];
453                let series: Vec<f64> = (0..mats[row].ncols())
454                    .map(|j| mats[row][(col, j)])
455                    .collect();
456
457                let (mut y_min, mut y_max) = min_max(&series);
458                if let Some((lb, ub)) = constraints_bounds[row] {
459                    y_min = y_min.min(lb);
460                    y_max = y_max.max(ub);
461                }
462                let pad = 0.1 * (y_max - y_min).max(0.1);
463                y_min -= pad;
464                y_max += pad;
465
466                let mut chart = ChartBuilder::on(area)
467                    .margin(10)
468                    .caption(
469                        format!("{} axis {}", labels[row], col + 1),
470                        ("sans-serif", 16),
471                    )
472                    .x_label_area_size(30)
473                    .y_label_area_size(45)
474                    .build_cartesian_2d(t[0]..t[t.len() - 1], y_min..y_max)?;
475
476                chart
477                    .configure_mesh()
478                    .x_desc(if row == 3 { "t (s)" } else { "" })
479                    .draw()?;
480
481                chart.draw_series(LineSeries::new(
482                    t.iter().zip(series.iter()).map(|(&x, &y)| (x, y)),
483                    &BLACK,
484                ))?;
485
486                if let Some((lb, ub)) = constraints_bounds[row] {
487                    chart.draw_series(LineSeries::new(
488                        vec![(t[0], lb), (t[t.len() - 1], lb)],
489                        ShapeStyle::from(&RED.mix(0.5)).stroke_width(2).filled(),
490                    ))?;
491                    chart.draw_series(LineSeries::new(
492                        vec![(t[0], ub), (t[t.len() - 1], ub)],
493                        ShapeStyle::from(&RED.mix(0.5)).stroke_width(2).filled(),
494                    ))?;
495                }
496            }
497        }
498
499        root.titled(title, ("sans-serif", 32))?;
500        root.present()?;
501        crate::verbosity_log!(crate::diag::Verbosity::Summary, "Saved plot: {filename}");
502        Ok(())
503    }
504
505    /// Compute first-order finite differences of `src` into `dst` (in-place).
506    /// Uses forward difference at index 0, backward at index n-1, central elsewhere.
507    fn finite_diff_inplace(src: &DMatrix<f64>, dst: &mut DMatrix<f64>, h: f64) {
508        let n = src.ncols();
509        if n < 2 {
510            return;
511        }
512        let inv2h = 1.0 / (2.0 * h);
513        // Iterate over each row (dimension) paired between src and dst.
514        for (src_row, mut dst_row) in src.row_iter().zip(dst.row_iter_mut()) {
515            // Forward difference at left boundary
516            dst_row[0] = (src_row[1] - src_row[0]) / h;
517            // Backward difference at right boundary
518            dst_row[n - 1] = (src_row[n - 1] - src_row[n - 2]) / h;
519            // Central differences for all interior points
520            for j in 1..n - 1 {
521                dst_row[j] = (src_row[j + 1] - src_row[j - 1]) * inv2h;
522            }
523        }
524    }
525
526    fn min_max(data: &[f64]) -> (f64, f64) {
527        data.iter()
528            .copied()
529            .filter(|v| v.is_finite())
530            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), v| {
531                (mn.min(v), mx.max(v))
532            })
533    }
534
535    #[test]
536    fn test_topp3_with_parametric_path() -> Result<(), Box<dyn Error>> {
537        crate::verbosity_log!(
538            crate::diag::Verbosity::Summary,
539            "\n=== TOPP3 with Parametric Path (6-DOF) ==="
540        );
541
542        let start = Instant::now();
543        let path = make_parametric_path_6dof()?;
544        let build_ms = start.elapsed().as_secs_f64() * 1e3;
545
546        let s = make_s_vector(N);
547        let start = Instant::now();
548        let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
549        let eval_ms = start.elapsed().as_secs_f64() * 1e3;
550
551        crate::verbosity_log!(
552            crate::diag::Verbosity::Summary,
553            "[Path Build] {build_ms:.3} ms, [Path Eval] {eval_ms:.3} ms"
554        );
555
556        let mut robot = setup_robot_with_constraints(&s, &derivs)?;
557        let s_slice: Vec<f64> = (0..N).map(|j| s[(0, j)]).collect();
558        let (profile, timings) = solve_topp_pipeline(&mut robot, &s_slice)?;
559
560        crate::verbosity_log!(
561            crate::diag::Verbosity::Summary,
562            "[TOPP2-RA] {:.3} ms, t_motion = {:.3} s",
563            timings.topp2_ms,
564            timings.t_motion_2
565        );
566        crate::verbosity_log!(
567            crate::diag::Verbosity::Summary,
568            "[TOPP3-LP] {:.3} ms, t_motion = {:.3} s",
569            timings.topp3_ms,
570            timings.t_motion_3
571        );
572
573        let (t, q_t, dq_t, ddq_t, dddq_t, interp_ms) =
574            interpolate_to_time_domain(&path, &s_slice, &profile, 1e-3)?;
575        crate::verbosity_log!(
576            crate::diag::Verbosity::Summary,
577            "[Interpolation] {:.3} ms, {} samples",
578            interp_ms,
579            t.len()
580        );
581
582        plot_topp3_grid(
583            "data/path_topp_plots/parametric_path_topp3.png",
584            "TOPP3 Result: Parametric Path (6-DOF)",
585            &t,
586            &q_t,
587            &dq_t,
588            &ddq_t,
589            &dddq_t,
590        )?;
591
592        Ok(())
593    }
594}