bevy_math/curve/derivatives/
adaptor_impls.rs

1//! Implementations of derivatives on curve adaptors. These allow
2//! compositionality for derivatives.
3
4use super::{SampleDerivative, SampleTwoDerivatives};
5use crate::common_traits::{HasTangent, Sum, VectorSpace, WithDerivative, WithTwoDerivatives};
6use crate::curve::{
7    adaptors::{
8        ChainCurve, ConstantCurve, ContinuationCurve, CurveReparamCurve, ForeverCurve, GraphCurve,
9        LinearReparamCurve, PingPongCurve, RepeatCurve, ReverseCurve, ZipCurve,
10    },
11    Curve,
12};
13
14// -- ConstantCurve
15
16impl<T> SampleDerivative<T> for ConstantCurve<T>
17where
18    T: HasTangent + Clone,
19{
20    fn sample_with_derivative_unchecked(&self, _t: f32) -> WithDerivative<T> {
21        WithDerivative {
22            value: self.value.clone(),
23            derivative: VectorSpace::ZERO,
24        }
25    }
26}
27
28impl<T> SampleTwoDerivatives<T> for ConstantCurve<T>
29where
30    T: HasTangent + Clone,
31{
32    fn sample_with_two_derivatives_unchecked(&self, _t: f32) -> WithTwoDerivatives<T> {
33        WithTwoDerivatives {
34            value: self.value.clone(),
35            derivative: VectorSpace::ZERO,
36            second_derivative: VectorSpace::ZERO,
37        }
38    }
39}
40
41// -- ChainCurve
42
43impl<T, C, D> SampleDerivative<T> for ChainCurve<T, C, D>
44where
45    T: HasTangent,
46    C: SampleDerivative<T>,
47    D: SampleDerivative<T>,
48{
49    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
50        if t > self.first.domain().end() {
51            self.second.sample_with_derivative_unchecked(
52                // `t - first.domain.end` computes the offset into the domain of the second.
53                t - self.first.domain().end() + self.second.domain().start(),
54            )
55        } else {
56            self.first.sample_with_derivative_unchecked(t)
57        }
58    }
59}
60
61impl<T, C, D> SampleTwoDerivatives<T> for ChainCurve<T, C, D>
62where
63    T: HasTangent,
64    C: SampleTwoDerivatives<T>,
65    D: SampleTwoDerivatives<T>,
66{
67    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
68        if t > self.first.domain().end() {
69            self.second.sample_with_two_derivatives_unchecked(
70                // `t - first.domain.end` computes the offset into the domain of the second.
71                t - self.first.domain().end() + self.second.domain().start(),
72            )
73        } else {
74            self.first.sample_with_two_derivatives_unchecked(t)
75        }
76    }
77}
78
79// -- ContinuationCurve
80
81impl<T, C, D> SampleDerivative<T> for ContinuationCurve<T, C, D>
82where
83    T: VectorSpace,
84    C: SampleDerivative<T>,
85    D: SampleDerivative<T>,
86{
87    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
88        if t > self.first.domain().end() {
89            let mut output = self.second.sample_with_derivative_unchecked(
90                // `t - first.domain.end` computes the offset into the domain of the second.
91                t - self.first.domain().end() + self.second.domain().start(),
92            );
93            output.value = output.value + self.offset;
94            output
95        } else {
96            self.first.sample_with_derivative_unchecked(t)
97        }
98    }
99}
100
101impl<T, C, D> SampleTwoDerivatives<T> for ContinuationCurve<T, C, D>
102where
103    T: VectorSpace,
104    C: SampleTwoDerivatives<T>,
105    D: SampleTwoDerivatives<T>,
106{
107    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
108        if t > self.first.domain().end() {
109            let mut output = self.second.sample_with_two_derivatives_unchecked(
110                // `t - first.domain.end` computes the offset into the domain of the second.
111                t - self.first.domain().end() + self.second.domain().start(),
112            );
113            output.value = output.value + self.offset;
114            output
115        } else {
116            self.first.sample_with_two_derivatives_unchecked(t)
117        }
118    }
119}
120
121// -- RepeatCurve
122
123impl<T, C> SampleDerivative<T> for RepeatCurve<T, C>
124where
125    T: HasTangent,
126    C: SampleDerivative<T>,
127{
128    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
129        let t = self.base_curve_sample_time(t);
130        self.curve.sample_with_derivative_unchecked(t)
131    }
132}
133
134impl<T, C> SampleTwoDerivatives<T> for RepeatCurve<T, C>
135where
136    T: HasTangent,
137    C: SampleTwoDerivatives<T>,
138{
139    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
140        let t = self.base_curve_sample_time(t);
141        self.curve.sample_with_two_derivatives_unchecked(t)
142    }
143}
144
145// -- ForeverCurve
146
147impl<T, C> SampleDerivative<T> for ForeverCurve<T, C>
148where
149    T: HasTangent,
150    C: SampleDerivative<T>,
151{
152    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
153        let t = self.base_curve_sample_time(t);
154        self.curve.sample_with_derivative_unchecked(t)
155    }
156}
157
158impl<T, C> SampleTwoDerivatives<T> for ForeverCurve<T, C>
159where
160    T: HasTangent,
161    C: SampleTwoDerivatives<T>,
162{
163    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
164        let t = self.base_curve_sample_time(t);
165        self.curve.sample_with_two_derivatives_unchecked(t)
166    }
167}
168
169// -- PingPongCurve
170
171impl<T, C> SampleDerivative<T> for PingPongCurve<T, C>
172where
173    T: HasTangent,
174    C: SampleDerivative<T>,
175{
176    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
177        if t > self.curve.domain().end() {
178            let t = self.curve.domain().end() * 2.0 - t;
179            // The derivative of the preceding expression is -1, so the chain
180            // rule implies the derivative should be negated.
181            let mut output = self.curve.sample_with_derivative_unchecked(t);
182            output.derivative = -output.derivative;
183            output
184        } else {
185            self.curve.sample_with_derivative_unchecked(t)
186        }
187    }
188}
189
190impl<T, C> SampleTwoDerivatives<T> for PingPongCurve<T, C>
191where
192    T: HasTangent,
193    C: SampleTwoDerivatives<T>,
194{
195    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
196        if t > self.curve.domain().end() {
197            let t = self.curve.domain().end() * 2.0 - t;
198            // See the implementation on `ReverseCurve` for an explanation of
199            // why this is correct.
200            let mut output = self.curve.sample_with_two_derivatives_unchecked(t);
201            output.derivative = -output.derivative;
202            output
203        } else {
204            self.curve.sample_with_two_derivatives_unchecked(t)
205        }
206    }
207}
208
209// -- ZipCurve
210
211impl<S, T, C, D> SampleDerivative<(S, T)> for ZipCurve<S, T, C, D>
212where
213    S: HasTangent,
214    T: HasTangent,
215    C: SampleDerivative<S>,
216    D: SampleDerivative<T>,
217{
218    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(S, T)> {
219        let first_output = self.first.sample_with_derivative_unchecked(t);
220        let second_output = self.second.sample_with_derivative_unchecked(t);
221        WithDerivative {
222            value: (first_output.value, second_output.value),
223            derivative: Sum(first_output.derivative, second_output.derivative),
224        }
225    }
226}
227
228impl<S, T, C, D> SampleTwoDerivatives<(S, T)> for ZipCurve<S, T, C, D>
229where
230    S: HasTangent,
231    T: HasTangent,
232    C: SampleTwoDerivatives<S>,
233    D: SampleTwoDerivatives<T>,
234{
235    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(S, T)> {
236        let first_output = self.first.sample_with_two_derivatives_unchecked(t);
237        let second_output = self.second.sample_with_two_derivatives_unchecked(t);
238        WithTwoDerivatives {
239            value: (first_output.value, second_output.value),
240            derivative: Sum(first_output.derivative, second_output.derivative),
241            second_derivative: Sum(
242                first_output.second_derivative,
243                second_output.second_derivative,
244            ),
245        }
246    }
247}
248
249// -- GraphCurve
250
251impl<T, C> SampleDerivative<(f32, T)> for GraphCurve<T, C>
252where
253    T: HasTangent,
254    C: SampleDerivative<T>,
255{
256    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(f32, T)> {
257        let output = self.base.sample_with_derivative_unchecked(t);
258        WithDerivative {
259            value: (t, output.value),
260            derivative: Sum(1.0, output.derivative),
261        }
262    }
263}
264
265impl<T, C> SampleTwoDerivatives<(f32, T)> for GraphCurve<T, C>
266where
267    T: HasTangent,
268    C: SampleTwoDerivatives<T>,
269{
270    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(f32, T)> {
271        let output = self.base.sample_with_two_derivatives_unchecked(t);
272        WithTwoDerivatives {
273            value: (t, output.value),
274            derivative: Sum(1.0, output.derivative),
275            second_derivative: Sum(0.0, output.second_derivative),
276        }
277    }
278}
279
280// -- ReverseCurve
281
282impl<T, C> SampleDerivative<T> for ReverseCurve<T, C>
283where
284    T: HasTangent,
285    C: SampleDerivative<T>,
286{
287    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
288        // This gets almost the correct value, but we haven't accounted for the
289        // reversal of orientation yet.
290        let mut output = self
291            .curve
292            .sample_with_derivative_unchecked(self.domain().end() - (t - self.domain().start()));
293
294        output.derivative = -output.derivative;
295
296        output
297    }
298}
299
300impl<T, C> SampleTwoDerivatives<T> for ReverseCurve<T, C>
301where
302    T: HasTangent,
303    C: SampleTwoDerivatives<T>,
304{
305    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
306        // This gets almost the correct value, but we haven't accounted for the
307        // reversal of orientation yet.
308        let mut output = self.curve.sample_with_two_derivatives_unchecked(
309            self.domain().end() - (t - self.domain().start()),
310        );
311
312        output.derivative = -output.derivative;
313
314        // (Note that the reparametrization that reverses the curve satisfies
315        // g'(t)^2 = 1 and g''(t) = 0, so the second derivative is already
316        // correct.)
317
318        output
319    }
320}
321
322// -- CurveReparamCurve
323
324impl<T, C, D> SampleDerivative<T> for CurveReparamCurve<T, C, D>
325where
326    T: HasTangent,
327    C: SampleDerivative<T>,
328    D: SampleDerivative<f32>,
329{
330    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
331        // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t)
332        // is `self.reparam_curve`.
333
334        // Start by computing g(t) and g'(t).
335        let reparam_output = self.reparam_curve.sample_with_derivative_unchecked(t);
336
337        // Compute:
338        // - value: f(g(t))
339        // - derivative: f'(g(t))
340        let mut output = self
341            .base
342            .sample_with_derivative_unchecked(reparam_output.value);
343
344        // Do the multiplication part of the chain rule.
345        output.derivative = output.derivative * reparam_output.derivative;
346
347        // value: f(g(t)), derivative: f'(g(t)) g'(t)
348        output
349    }
350}
351
352impl<T, C, D> SampleTwoDerivatives<T> for CurveReparamCurve<T, C, D>
353where
354    T: HasTangent,
355    C: SampleTwoDerivatives<T>,
356    D: SampleTwoDerivatives<f32>,
357{
358    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
359        // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t)
360        // is `self.reparam_curve`.
361
362        // Start by computing g(t), g'(t), g''(t).
363        let reparam_output = self.reparam_curve.sample_with_two_derivatives_unchecked(t);
364
365        // Compute:
366        // - value: f(g(t))
367        // - derivative: f'(g(t))
368        // - second derivative: f''(g(t))
369        let mut output = self
370            .base
371            .sample_with_two_derivatives_unchecked(reparam_output.value);
372
373        // Set the second derivative according to the chain and product rules
374        // r''(t) = f''(g(t)) g'(t)^2 + f'(g(t)) g''(t)
375        output.second_derivative = (output.second_derivative
376            * (reparam_output.derivative * reparam_output.derivative))
377            + (output.derivative * reparam_output.second_derivative);
378
379        // Set the first derivative according to the chain rule
380        // r'(t) = f'(g(t)) g'(t)
381        output.derivative = output.derivative * reparam_output.derivative;
382
383        output
384    }
385}
386
387// -- LinearReparamCurve
388
389impl<T, C> SampleDerivative<T> for LinearReparamCurve<T, C>
390where
391    T: HasTangent,
392    C: SampleDerivative<T>,
393{
394    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
395        // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) is
396        // the linear map bijecting `self.new_domain` onto `self.base.domain()`.
397
398        // The invariants imply this unwrap always succeeds.
399        let g = self.new_domain.linear_map_to(self.base.domain()).unwrap();
400
401        // Compute g'(t) from the domain lengths.
402        let g_derivative = self.base.domain().length() / self.new_domain.length();
403
404        // Compute:
405        // - value: f(g(t))
406        // - derivative: f'(g(t))
407        let mut output = self.base.sample_with_derivative_unchecked(g(t));
408
409        // Adjust the derivative according to the chain rule.
410        output.derivative = output.derivative * g_derivative;
411
412        output
413    }
414}
415
416impl<T, C> SampleTwoDerivatives<T> for LinearReparamCurve<T, C>
417where
418    T: HasTangent,
419    C: SampleTwoDerivatives<T>,
420{
421    fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
422        // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) is
423        // the linear map bijecting `self.new_domain` onto `self.base.domain()`.
424
425        // The invariants imply this unwrap always succeeds.
426        let g = self.new_domain.linear_map_to(self.base.domain()).unwrap();
427
428        // Compute g'(t) from the domain lengths.
429        let g_derivative = self.base.domain().length() / self.new_domain.length();
430
431        // Compute:
432        // - value: f(g(t))
433        // - derivative: f'(g(t))
434        // - second derivative: f''(g(t))
435        let mut output = self.base.sample_with_two_derivatives_unchecked(g(t));
436
437        // Set the second derivative according to the chain and product rules
438        // r''(t) = f''(g(t)) g'(t)^2  (g''(t) = 0)
439        output.second_derivative = output.second_derivative * (g_derivative * g_derivative);
440
441        // Set the first derivative according to the chain rule
442        // r'(t) = f'(g(t)) g'(t)
443        output.derivative = output.derivative * g_derivative;
444
445        output
446    }
447}
448
449#[cfg(test)]
450mod tests {
451
452    use approx::assert_abs_diff_eq;
453
454    use super::*;
455    use crate::cubic_splines::{CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator};
456    use crate::curve::{Curve, CurveExt, Interval};
457    use crate::{vec2, Vec2, Vec3};
458
459    fn test_curve() -> CubicCurve<Vec2> {
460        let control_pts = [[
461            vec2(0.0, 0.0),
462            vec2(1.0, 0.0),
463            vec2(0.0, 1.0),
464            vec2(1.0, 1.0),
465        ]];
466
467        CubicBezier::new(control_pts).to_curve().unwrap()
468    }
469
470    fn other_test_curve() -> CubicCurve<Vec2> {
471        let control_pts = [
472            vec2(1.0, 1.0),
473            vec2(2.0, 1.0),
474            vec2(2.0, 0.0),
475            vec2(1.0, 0.0),
476        ];
477
478        CubicCardinalSpline::new(0.5, control_pts)
479            .to_curve()
480            .unwrap()
481    }
482
483    fn reparam_curve() -> CubicCurve<f32> {
484        let control_pts = [[0.0, 0.25, 0.75, 1.0]];
485
486        CubicBezier::new(control_pts).to_curve().unwrap()
487    }
488
489    #[test]
490    fn constant_curve() {
491        let curve = ConstantCurve::new(Interval::UNIT, Vec3::new(0.2, 1.5, -2.6));
492        let jet = curve.sample_with_derivative(0.5).unwrap();
493        assert_abs_diff_eq!(jet.derivative, Vec3::ZERO);
494    }
495
496    #[test]
497    fn chain_curve() {
498        let curve1 = test_curve();
499        let curve2 = other_test_curve();
500        let curve = curve1.by_ref().chain(&curve2).unwrap();
501
502        let jet = curve.sample_with_derivative(0.65).unwrap();
503        let true_jet = curve1.sample_with_derivative(0.65).unwrap();
504        assert_abs_diff_eq!(jet.value, true_jet.value);
505        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
506
507        let jet = curve.sample_with_derivative(1.1).unwrap();
508        let true_jet = curve2.sample_with_derivative(0.1).unwrap();
509        assert_abs_diff_eq!(jet.value, true_jet.value);
510        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
511    }
512
513    #[test]
514    fn continuation_curve() {
515        let curve1 = test_curve();
516        let curve2 = other_test_curve();
517        let curve = curve1.by_ref().chain_continue(&curve2).unwrap();
518
519        let jet = curve.sample_with_derivative(0.99).unwrap();
520        let true_jet = curve1.sample_with_derivative(0.99).unwrap();
521        assert_abs_diff_eq!(jet.value, true_jet.value);
522        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
523
524        let jet = curve.sample_with_derivative(1.3).unwrap();
525        let true_jet = curve2.sample_with_derivative(0.3).unwrap();
526        assert_abs_diff_eq!(jet.value, true_jet.value);
527        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
528    }
529
530    #[test]
531    fn repeat_curve() {
532        let curve1 = test_curve();
533        let curve = curve1.by_ref().repeat(3).unwrap();
534
535        let jet = curve.sample_with_derivative(0.73).unwrap();
536        let true_jet = curve1.sample_with_derivative(0.73).unwrap();
537        assert_abs_diff_eq!(jet.value, true_jet.value);
538        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
539
540        let jet = curve.sample_with_derivative(3.5).unwrap();
541        let true_jet = curve1.sample_with_derivative(0.5).unwrap();
542        assert_abs_diff_eq!(jet.value, true_jet.value);
543        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
544    }
545
546    #[test]
547    fn forever_curve() {
548        let curve1 = test_curve();
549        let curve = curve1.by_ref().forever().unwrap();
550
551        let jet = curve.sample_with_derivative(0.12).unwrap();
552        let true_jet = curve1.sample_with_derivative(0.12).unwrap();
553        assert_abs_diff_eq!(jet.value, true_jet.value);
554        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
555
556        let jet = curve.sample_with_derivative(500.5).unwrap();
557        let true_jet = curve1.sample_with_derivative(0.5).unwrap();
558        assert_abs_diff_eq!(jet.value, true_jet.value);
559        assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
560    }
561
562    #[test]
563    fn ping_pong_curve() {
564        let curve1 = test_curve();
565        let curve = curve1.by_ref().ping_pong().unwrap();
566
567        let jet = curve.sample_with_derivative(0.99).unwrap();
568        let comparison_jet = curve1.sample_with_derivative(0.99).unwrap();
569        assert_abs_diff_eq!(jet.value, comparison_jet.value);
570        assert_abs_diff_eq!(jet.derivative, comparison_jet.derivative);
571
572        let jet = curve.sample_with_derivative(1.3).unwrap();
573        let comparison_jet = curve1.sample_with_derivative(0.7).unwrap();
574        assert_abs_diff_eq!(jet.value, comparison_jet.value);
575        assert_abs_diff_eq!(jet.derivative, -comparison_jet.derivative, epsilon = 1.0e-5);
576    }
577
578    #[test]
579    fn zip_curve() {
580        let curve1 = test_curve();
581        let curve2 = other_test_curve();
582        let curve = curve1.by_ref().zip(&curve2).unwrap();
583
584        let jet = curve.sample_with_derivative(0.7).unwrap();
585        let comparison_jet1 = curve1.sample_with_derivative(0.7).unwrap();
586        let comparison_jet2 = curve2.sample_with_derivative(0.7).unwrap();
587        assert_abs_diff_eq!(jet.value.0, comparison_jet1.value);
588        assert_abs_diff_eq!(jet.value.1, comparison_jet2.value);
589        let Sum(derivative1, derivative2) = jet.derivative;
590        assert_abs_diff_eq!(derivative1, comparison_jet1.derivative);
591        assert_abs_diff_eq!(derivative2, comparison_jet2.derivative);
592    }
593
594    #[test]
595    fn graph_curve() {
596        let curve1 = test_curve();
597        let curve = curve1.by_ref().graph();
598
599        let jet = curve.sample_with_derivative(0.25).unwrap();
600        let comparison_jet = curve1.sample_with_derivative(0.25).unwrap();
601        assert_abs_diff_eq!(jet.value.0, 0.25);
602        assert_abs_diff_eq!(jet.value.1, comparison_jet.value);
603        let Sum(one, derivative) = jet.derivative;
604        assert_abs_diff_eq!(one, 1.0);
605        assert_abs_diff_eq!(derivative, comparison_jet.derivative);
606    }
607
608    #[test]
609    fn reverse_curve() {
610        let curve1 = test_curve();
611        let curve = curve1.by_ref().reverse().unwrap();
612
613        let jet = curve.sample_with_derivative(0.23).unwrap();
614        let comparison_jet = curve1.sample_with_derivative(0.77).unwrap();
615        assert_abs_diff_eq!(jet.value, comparison_jet.value);
616        assert_abs_diff_eq!(jet.derivative, -comparison_jet.derivative);
617    }
618
619    #[test]
620    fn curve_reparam_curve() {
621        let reparam_curve = reparam_curve();
622        let reparam_jet = reparam_curve.sample_with_derivative(0.6).unwrap();
623
624        let curve1 = test_curve();
625        let curve = curve1.by_ref().reparametrize_by_curve(&reparam_curve);
626
627        let jet = curve.sample_with_derivative(0.6).unwrap();
628        let base_jet = curve1
629            .sample_with_derivative(reparam_curve.sample(0.6).unwrap())
630            .unwrap();
631        assert_abs_diff_eq!(jet.value, base_jet.value);
632        assert_abs_diff_eq!(jet.derivative, base_jet.derivative * reparam_jet.derivative);
633    }
634
635    #[test]
636    fn linear_reparam_curve() {
637        let curve1 = test_curve();
638        let curve = curve1
639            .by_ref()
640            .reparametrize_linear(Interval::new(0.0, 0.5).unwrap())
641            .unwrap();
642
643        let jet = curve.sample_with_derivative(0.23).unwrap();
644        let comparison_jet = curve1.sample_with_derivative(0.46).unwrap();
645        assert_abs_diff_eq!(jet.value, comparison_jet.value);
646        assert_abs_diff_eq!(jet.derivative, comparison_jet.derivative * 2.0);
647    }
648}