efd/posed.rs
1//! Posed EFD (Elliptic Fourier Descriptor) is a special shape to describe the
2//! pose of a curve. It is a combination of two curves, the first is the
3//! original curve, and the second is the pose unit vectors.
4//!
5//! Please see the [`PosedEfd`] type for more information.
6use crate::*;
7use alloc::vec::Vec;
8use core::{array, iter::zip};
9#[cfg(not(feature = "std"))]
10#[allow(unused_imports)]
11use num_traits::*;
12
13/// A 1D shape with a pose described by EFD.
14pub type PosedEfd1 = PosedEfd<1>;
15/// A 2D shape with a pose described by EFD.
16pub type PosedEfd2 = PosedEfd<2>;
17/// A 3D shape with a pose described by EFD.
18pub type PosedEfd3 = PosedEfd<3>;
19
20/// Transform 2D angles to unit vectors.
21/// ```
22/// # let angles = efd::tests::ANGLE2D_POSE;
23/// let vectors = efd::posed::ang2vec(angles);
24/// ```
25pub fn ang2vec(angles: &[f64]) -> Vec<[f64; 2]> {
26 angles.iter().map(|a| [a.cos(), a.sin()]).collect()
27}
28
29/// Create a guide curve from a curve and its unit vectors.
30/// ```
31/// # let curve_p = efd::tests::CURVE2D_POSE;
32/// # let vectors = efd::posed::ang2vec(efd::tests::ANGLE2D_POSE);
33/// // A custom length
34/// let length = 20.;
35/// let curve_q = efd::posed::guide_from_curve(curve_p, vectors, length);
36/// let efd = efd::posed::PosedEfd2::from_series(curve_p, curve_q, true);
37/// ```
38/// See also [`PosedEfd::from_uvec()`] / [`PosedEfd::from_series()`].
39pub fn guide_from_curve<C, V, const D: usize>(curve: C, vectors: V, length: f64) -> Vec<[f64; D]>
40where
41 C: Curve<D>,
42 V: Curve<D>,
43{
44 zip(curve.as_curve(), vectors.as_curve())
45 .map(|(p, v)| core::array::from_fn(|i| p[i] + length * v[i]))
46 .collect()
47}
48
49/// Motion signature with the target position.
50///
51/// Used to present an "original motion". Can be compared with [`PosedEfd`] by
52/// [`PosedEfd::err_sig()`].
53#[derive(Clone)]
54pub struct MotionSig<const D: usize>
55where
56 U<D>: EfdDim<D>,
57{
58 /// Normalized curve
59 pub curve: Vec<[f64; D]>,
60 /// Normalized unit vectors
61 pub vectors: Vec<[f64; D]>,
62 /// Normalized time parameters
63 pub t: Vec<f64>,
64 /// Geometric variables
65 pub geo: GeoVar<Rot<D>, D>,
66}
67
68impl<const D: usize> MotionSig<D>
69where
70 U<D>: EfdDim<D>,
71{
72 /// Get the path signature and its target position from a curve and its unit
73 /// vectors.
74 ///
75 /// This function is faster than building [`PosedEfd`] since it only
76 /// calculates **two harmonics**.
77 /// ```
78 /// use efd::posed::{ang2vec, MotionSig};
79 /// # let curve = efd::tests::CURVE2D_POSE;
80 /// # let angles = efd::tests::ANGLE2D_POSE;
81 ///
82 /// let sig = MotionSig::new(curve, ang2vec(angles), true);
83 /// ```
84 /// See also [`PathSig`].
85 pub fn new<C, V>(curve: C, vectors: V, is_open: bool) -> Self
86 where
87 C: Curve<D>,
88 V: Curve<D>,
89 {
90 let PathSig { curve, t, geo } = PathSig::new(curve.as_curve(), is_open);
91 let mut vectors = geo.inverse().only_rot().transform(vectors);
92 for (p, v) in zip(&curve, &mut vectors) {
93 *v = array::from_fn(|i| p[i] + v[i]);
94 }
95 Self { curve, vectors, t, geo }
96 }
97
98 /// Get the reference of normalized time parameters.
99 pub fn as_t(&self) -> &[f64] {
100 &self.t
101 }
102
103 /// Get the reference of geometric variables.
104 pub fn as_geo(&self) -> &GeoVar<Rot<D>, D> {
105 &self.geo
106 }
107}
108
109/// An open-curve shape with a pose described by EFD.
110///
111/// These are the same as [`Efd`] except that it has a pose, and the data are
112/// always normalized and readonly.
113///
114/// Start with [`PosedEfd::from_angles()`] / [`PosedEfd::from_series()`] /
115/// [`PosedEfd::from_uvec()`] and their related methods.
116///
117/// # Pose Representation
118/// Pose is represented by an unit vector, which is rotated by the rotation
119/// of the original shape.
120#[derive(Clone)]
121pub struct PosedEfd<const D: usize>
122where
123 U<D>: EfdDim<D>,
124{
125 curve: Efd<D>,
126 pose: Efd<D>,
127}
128
129impl PosedEfd2 {
130 /// Calculate the coefficients from a curve and its angles from each point.
131 pub fn from_angles<C>(curve: C, angles: &[f64], is_open: bool) -> Self
132 where
133 C: Curve<2>,
134 {
135 let harmonic = harmonic(false, curve.len());
136 Self::from_angles_harmonic(curve, angles, is_open, harmonic).fourier_power_anaysis(None)
137 }
138
139 /// Calculate the coefficients from a curve and its angles from each point.
140 ///
141 /// The `harmonic` is the number of the coefficients to be calculated.
142 pub fn from_angles_harmonic<C>(curve: C, angles: &[f64], is_open: bool, harmonic: usize) -> Self
143 where
144 C: Curve<2>,
145 {
146 Self::from_uvec_harmonic(curve, ang2vec(angles), is_open, harmonic)
147 }
148}
149
150impl<const D: usize> PosedEfd<D>
151where
152 U<D>: EfdDim<D>,
153{
154 /// Create object from an [`Efd`] object.
155 ///
156 /// Posed EFD is a special shape to describe the pose, `efd` is only used to
157 /// describe this motion signature.
158 ///
159 /// See also [`PosedEfd::into_inner()`].
160 pub const fn from_parts_unchecked(curve: Efd<D>, pose: Efd<D>) -> Self {
161 Self { curve, pose }
162 }
163
164 /// Calculate the coefficients from two series of points.
165 ///
166 /// The second series is the pose series, the `curve_q[i]` has the same time
167 /// as `curve_p[i]`.
168 pub fn from_series<C1, C2>(curve_p: C1, curve_q: C2, is_open: bool) -> Self
169 where
170 C1: Curve<D>,
171 C2: Curve<D>,
172 {
173 let harmonic = harmonic(true, curve_p.len());
174 Self::from_series_harmonic(curve_p, curve_q, is_open, harmonic).fourier_power_anaysis(None)
175 }
176
177 /// Calculate the coefficients from two series of points.
178 ///
179 /// The `harmonic` is the number of the coefficients to be calculated.
180 pub fn from_series_harmonic<C1, C2>(
181 curve_p: C1,
182 curve_q: C2,
183 is_open: bool,
184 harmonic: usize,
185 ) -> Self
186 where
187 C1: Curve<D>,
188 C2: Curve<D>,
189 {
190 let vectors = zip(curve_p.as_curve(), curve_q.as_curve())
191 .map(|(p, q)| array::from_fn(|i| q[i] - p[i]))
192 .collect::<Vec<_>>();
193 Self::from_uvec_harmonic(curve_p, vectors, is_open, harmonic)
194 }
195
196 /// Calculate the coefficients from a curve and its unit vectors from each
197 /// point.
198 ///
199 /// If the unit vectors is not normalized, the length of the first vector
200 /// will be used as the scaling factor.
201 pub fn from_uvec<C, V>(curve: C, vectors: V, is_open: bool) -> Self
202 where
203 C: Curve<D>,
204 V: Curve<D>,
205 {
206 let harmonic = harmonic(true, curve.len());
207 Self::from_uvec_harmonic(curve, vectors, is_open, harmonic).fourier_power_anaysis(None)
208 }
209
210 /// Calculate the coefficients from a curve and its unit vectors from each
211 /// point.
212 ///
213 /// If the unit vectors is not normalized, the length of the first vector
214 /// will be used as the scaling factor.
215 ///
216 /// The `harmonic` is the number of the coefficients to be calculated.
217 pub fn from_uvec_harmonic<C, V>(curve: C, vectors: V, is_open: bool, harmonic: usize) -> Self
218 where
219 C: Curve<D>,
220 V: Curve<D>,
221 {
222 debug_assert!(harmonic != 0, "harmonic must not be 0");
223 debug_assert!(curve.len() > 2, "the curve length must greater than 2");
224 debug_assert!(
225 curve.len() == vectors.len(),
226 "the curve length must be equal to the vectors length"
227 );
228 let curve = curve.as_curve();
229 let guide = {
230 let dxyz = util::diff(if !is_open && curve.first() != curve.last() {
231 to_mat(curve.closed_lin())
232 } else {
233 to_mat(curve)
234 });
235 dxyz.map(util::pow2).row_sum().map(f64::sqrt)
236 };
237 let (_, mut coeffs, geo) = U::get_coeff(curve, is_open, harmonic, Some(guide.as_slice()));
238 let geo = geo * U::norm_coeff(&mut coeffs, None);
239 let geo_inv = geo.inverse();
240 let p_norm = geo_inv.transform(curve);
241 let mut q_norm = geo_inv.only_rot().transform(vectors);
242 for (p, q) in zip(p_norm, &mut q_norm) {
243 *q = array::from_fn(|i| p[i] + q[i]);
244 }
245 let curve = Efd::from_parts_unchecked(coeffs, geo);
246 let (_, mut coeffs, q_trans) =
247 U::get_coeff(&q_norm, is_open, harmonic, Some(guide.as_slice()));
248 U::norm_zeta(&mut coeffs, None);
249 let pose = Efd::from_parts_unchecked(coeffs, q_trans);
250 Self { curve, pose }
251 }
252
253 /// Use Fourier Power Anaysis (FPA) to reduce the harmonic number.
254 ///
255 /// The posed EFD will set the harmonic number to the maximum harmonic
256 /// number of the curve and the pose.
257 ///
258 /// See also [`Efd::fourier_power_anaysis()`].
259 ///
260 /// # Panics
261 /// Panics if the threshold is not in 0..1, or the harmonic is zero.
262 pub fn fourier_power_anaysis(mut self, threshold: impl Into<Option<f64>>) -> Self {
263 self.fpa_inplace(threshold);
264 self
265 }
266
267 /// Fourier Power Anaysis (FPA) function with in-place operation.
268 ///
269 /// See also [`PosedEfd::fourier_power_anaysis()`].
270 ///
271 /// # Panics
272 /// Panics if the threshold is not in 0..1, or the harmonic is zero.
273 pub fn fpa_inplace(&mut self, threshold: impl Into<Option<f64>>) {
274 let threshold = threshold.into();
275 let [harmonic1, harmonic2] = [&self.curve, &self.pose].map(|efd| {
276 let lut = efd.coeffs_iter().map(|m| m.map(util::pow2).sum()).collect();
277 fourier_power_anaysis(lut, threshold)
278 });
279 self.set_harmonic(harmonic1.max(harmonic2));
280 }
281
282 /// Set the harmonic number of the coefficients.
283 ///
284 /// See also [`Efd::set_harmonic()`].
285 ///
286 /// # Panics
287 /// Panics if the harmonic is zero.
288 pub fn set_harmonic(&mut self, harmonic: usize) {
289 self.curve.set_harmonic(harmonic);
290 self.pose.set_harmonic(harmonic);
291 }
292
293 /// Consume self and return the parts of this type. The first is the curve
294 /// coefficients, and the second is the pose coefficients.
295 ///
296 /// See also [`PosedEfd::from_parts_unchecked()`].
297 pub fn into_inner(self) -> (Efd<D>, Efd<D>) {
298 (self.curve, self.pose)
299 }
300
301 /// Get the reference of the curve coefficients.
302 ///
303 /// **Note**: There is no mutable reference, please use
304 /// [`PosedEfd::into_inner()`] instead.
305 /// ```
306 /// # let curve = efd::tests::CURVE2D_POSE;
307 /// # let angles = efd::tests::ANGLE2D_POSE;
308 /// let efd = efd::PosedEfd2::from_angles(curve, angles, true);
309 /// let curve_efd = efd.as_curve();
310 /// let (mut curve_efd, _) = efd.into_inner();
311 /// ```
312 /// See also [`PosedEfd::as_pose()`].
313 pub fn as_curve(&self) -> &Efd<D> {
314 &self.curve
315 }
316
317 /// Get the reference of the pose coefficients.
318 ///
319 /// **Note**: There is no mutable reference, please use
320 /// [`PosedEfd::into_inner()`] instead.
321 /// ```
322 /// # let curve = efd::tests::CURVE2D_POSE;
323 /// # let angles = efd::tests::ANGLE2D_POSE;
324 /// let efd = efd::PosedEfd2::from_angles(curve, angles, true);
325 /// let pose_efd = efd.as_pose();
326 /// let (_, mut pose_efd) = efd.into_inner();
327 /// ```
328 /// See also [`PosedEfd::as_curve()`].
329 pub fn as_pose(&self) -> &Efd<D> {
330 &self.pose
331 }
332
333 /// Check if the descibed motion is open.
334 pub fn is_open(&self) -> bool {
335 self.curve.is_open()
336 }
337
338 /// Get the harmonic number of the coefficients.
339 ///
340 /// The curve and the pose coefficients are always have the same harmonic
341 /// number.
342 #[inline]
343 pub fn harmonic(&self) -> usize {
344 self.curve.harmonic()
345 }
346
347 /// Calculate the error between two [`PosedEfd`].
348 pub fn err(&self, rhs: &Self) -> f64 {
349 (2. * self.curve.err(&rhs.curve))
350 .max(self.pose.err(&rhs.pose))
351 .max((self.pose.as_geo().trans()).l2_err(rhs.pose.as_geo().trans()))
352 }
353
354 /// Calculate the error from a [`MotionSig`].
355 pub fn err_sig(&self, sig: &MotionSig<D>) -> f64 {
356 let curve =
357 zip(self.curve.recon_norm_by(&sig.t), &sig.curve).map(|(a, b)| 2. * a.l2_err(b));
358 let pose = zip(self.pose.recon_by(&sig.t), &sig.vectors).map(|(a, b)| a.l2_err(b));
359 curve.chain(pose).fold(0., f64::max)
360 }
361}