problemreductions/models/misc/
minimum_discrete_planar_inverse_kinematics.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
10use crate::traits::Problem;
11use crate::types::Min;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "MinimumDiscretePlanarInverseKinematics",
17 display_name: "Minimum Discrete Planar Inverse Kinematics",
18 aliases: &[],
19 dimensions: &[],
20 category: crate::registry::ProblemCategory::Misc,
21 module_path: module_path!(),
22 description: "Pick one sampled absolute orientation per link, subject to consecutive-pair feasibility constraints, to minimize the squared distance from the end-effector to a target point",
23 fields: &[
24 FieldInfo {
25 name: "link_lengths",
26 type_name: "Vec<f64>",
27 description: "Positive link lengths l_1, ..., l_n",
28 },
29 FieldInfo {
30 name: "target_point",
31 type_name: "(f64, f64)",
32 description: "Target point g = (g_x, g_y) in R^2",
33 },
34 FieldInfo {
35 name: "orientation_samples",
36 type_name: "Vec<Vec<f64>>",
37 description: "Sampled absolute orientations Phi_j for each link j",
38 },
39 FieldInfo {
40 name: "allowed_pairs",
41 type_name: "Vec<Vec<(usize, usize)>>",
42 description: "Admissible (a_{j-1}, a_j) pair sets A_j for j = 2, ..., n",
43 },
44 ],
45 }
46}
47
48#[derive(Debug, Clone, Serialize)]
59pub struct MinimumDiscretePlanarInverseKinematics {
60 link_lengths: Vec<f64>,
61 target_point: (f64, f64),
62 orientation_samples: Vec<Vec<f64>>,
63 allowed_pairs: Vec<Vec<(usize, usize)>>,
64}
65
66#[derive(Deserialize)]
67struct MinimumDiscretePlanarInverseKinematicsData {
68 link_lengths: Vec<f64>,
69 target_point: (f64, f64),
70 orientation_samples: Vec<Vec<f64>>,
71 allowed_pairs: Vec<Vec<(usize, usize)>>,
72}
73
74impl<'de> Deserialize<'de> for MinimumDiscretePlanarInverseKinematics {
75 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76 where
77 D: serde::Deserializer<'de>,
78 {
79 let data = MinimumDiscretePlanarInverseKinematicsData::deserialize(deserializer)?;
80 Self::new(
81 data.link_lengths,
82 data.target_point,
83 data.orientation_samples,
84 data.allowed_pairs,
85 )
86 .map_err(serde::de::Error::custom)
87 }
88}
89
90impl MinimumDiscretePlanarInverseKinematics {
91 pub fn new(
94 link_lengths: Vec<f64>,
95 target_point: (f64, f64),
96 orientation_samples: Vec<Vec<f64>>,
97 allowed_pairs: Vec<Vec<(usize, usize)>>,
98 ) -> Result<Self, crate::registry::ConstructionError> {
99 let n = link_lengths.len();
100 if n == 0 {
101 return Err("MinimumDiscretePlanarInverseKinematics requires at least one link".into());
102 }
103 for (index, &length) in link_lengths.iter().enumerate() {
104 if !length.is_finite() || length <= 0.0 {
105 return Err(
106 format!("link length at index {index} must be positive and finite").into(),
107 );
108 }
109 }
110 if !target_point.0.is_finite() || !target_point.1.is_finite() {
111 return Err("target point coordinates must be finite".into());
112 }
113 if orientation_samples.len() != n {
114 return Err("orientation_samples must have one entry per link".into());
115 }
116 for (link, samples) in orientation_samples.iter().enumerate() {
117 if samples.is_empty() {
118 return Err(
119 format!("link {link} must have at least one candidate orientation").into(),
120 );
121 }
122 for (sample, &angle) in samples.iter().enumerate() {
123 if !angle.is_finite() {
124 return Err(format!(
125 "orientation sample {sample} for link {link} must be finite"
126 )
127 .into());
128 }
129 }
130 }
131 if allowed_pairs.len() != n - 1 {
132 return Err("allowed_pairs must have one entry per junction".into());
133 }
134 for (j_minus_1, pairs) in allowed_pairs.iter().enumerate() {
135 let m_prev = orientation_samples[j_minus_1].len();
136 let m_curr = orientation_samples[j_minus_1 + 1].len();
137 for &(a_prev, a_curr) in pairs {
138 if a_prev >= m_prev || a_curr >= m_curr {
139 return Err(format!(
140 "allowed pair ({a_prev}, {a_curr}) at junction {j_minus_1} is out of range"
141 )
142 .into());
143 }
144 }
145 }
146 Ok(Self {
147 link_lengths,
148 target_point,
149 orientation_samples,
150 allowed_pairs,
151 })
152 }
153
154 pub fn link_lengths(&self) -> &[f64] {
156 &self.link_lengths
157 }
158
159 pub fn target_point(&self) -> (f64, f64) {
161 self.target_point
162 }
163
164 pub fn orientation_samples(&self) -> &[Vec<f64>] {
166 &self.orientation_samples
167 }
168
169 pub fn allowed_pairs(&self) -> &[Vec<(usize, usize)>] {
171 &self.allowed_pairs
172 }
173
174 pub fn num_links(&self) -> usize {
176 self.link_lengths.len()
177 }
178
179 pub fn num_orientation_samples(&self) -> usize {
183 self.orientation_samples.iter().map(Vec::len).sum()
184 }
185
186 pub fn is_feasible(&self, config: &[usize]) -> bool {
189 let n = self.num_links();
190 if config.len() != n {
191 return false;
192 }
193 for (j, &a) in config.iter().enumerate() {
194 if a >= self.orientation_samples[j].len() {
195 return false;
196 }
197 }
198 for j in 1..n {
199 let pair = (config[j - 1], config[j]);
200 if !self.allowed_pairs[j - 1].contains(&pair) {
201 return false;
202 }
203 }
204 true
205 }
206
207 pub fn end_effector(
210 &self,
211 config: &[usize],
212 ) -> Result<Option<(f64, f64)>, crate::traits::EvaluationError> {
213 if !self.is_feasible(config) {
214 return Ok(None);
215 }
216 let mut x = 0.0_f64;
217 let mut y = 0.0_f64;
218 for (j, &a) in config.iter().enumerate() {
219 let phi = self.orientation_samples[j][a];
220 let next_x = x + self.link_lengths[j] * phi.cos();
221 let next_y = y + self.link_lengths[j] * phi.sin();
222 if !next_x.is_finite() || !next_y.is_finite() {
223 return Err(crate::traits::EvaluationError::NonFiniteResult(
224 "computing the inverse-kinematics end-effector position".into(),
225 ));
226 }
227 x = next_x;
228 y = next_y;
229 }
230 Ok(Some((x, y)))
231 }
232
233 pub fn squared_distance(
236 &self,
237 config: &[usize],
238 ) -> Result<Option<f64>, crate::traits::EvaluationError> {
239 let Some((x, y)) = self.end_effector(config)? else {
240 return Ok(None);
241 };
242 let dx = x - self.target_point.0;
243 let dy = y - self.target_point.1;
244 let squared_distance = dx * dx + dy * dy;
245 if !squared_distance.is_finite() {
246 return Err(crate::traits::EvaluationError::NonFiniteResult(
247 "computing the inverse-kinematics squared distance".into(),
248 ));
249 }
250 Ok(Some(squared_distance))
251 }
252
253 pub fn is_valid_solution(&self, config: &[usize]) -> bool {
255 self.is_feasible(config)
256 }
257}
258
259impl Problem for MinimumDiscretePlanarInverseKinematics {
260 const NAME: &'static str = "MinimumDiscretePlanarInverseKinematics";
261 type Solution = Vec<usize>;
262 type Value = Min<f64>;
263
264 crate::problem_parameters![
265 ("num_links", num_links),
266 ("num_orientation_samples", num_orientation_samples),
267 ];
268
269 fn variant() -> Vec<(&'static str, &'static str)> {
270 crate::variant_params![]
271 }
272
273 fn evaluate(
274 &self,
275 config: &Self::Solution,
276 ) -> Result<Min<f64>, crate::traits::EvaluationError> {
277 if config.len() != self.num_links() {
278 return Err(crate::traits::EvaluationError::InvalidConfiguration(
279 "orientation assignment length does not match the links".into(),
280 ));
281 }
282 if config
283 .iter()
284 .enumerate()
285 .any(|(link, &orientation)| orientation >= self.orientation_samples[link].len())
286 {
287 return Err(crate::traits::EvaluationError::InvalidConfiguration(
288 "orientation assignment contains an out-of-range sample".into(),
289 ));
290 }
291 Ok({
292 match self.squared_distance(config)? {
293 Some(value) => Min(Some(value)),
294 None => Min(None),
295 }
296 })
297 }
298}
299
300impl crate::solvers::BruteForceProblem for MinimumDiscretePlanarInverseKinematics {
301 fn dimensions(&self) -> Vec<usize> {
302 self.orientation_samples
303 .iter()
304 .map(|samples| samples.len())
305 .collect()
306 }
307}
308
309crate::declare_variants! {
310 default MinimumDiscretePlanarInverseKinematics => "(num_orientation_samples / num_links)^num_links",
312}
313
314crate::register_brute_force! {
315 MinimumDiscretePlanarInverseKinematics,
316}
317
318#[cfg(feature = "example-db")]
319pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
320 use std::f64::consts::FRAC_PI_2;
321 vec![crate::example_db::specs::ModelExampleSpec {
322 id: "minimum_discrete_planar_inverse_kinematics",
323 instance: Box::new(
324 MinimumDiscretePlanarInverseKinematics::new(
325 vec![2.0, 1.0],
326 (2.0, 1.0),
327 vec![vec![0.0, FRAC_PI_2], vec![0.0, FRAC_PI_2]],
328 vec![vec![(0, 0), (0, 1), (1, 1)]],
329 )
330 .unwrap(),
331 ),
332 optimal_config: serde_json::json!(vec![0, 1]),
333 optimal_value: serde_json::json!(0.0),
334 }]
335}
336
337#[cfg(test)]
338#[path = "../../unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs"]
339mod tests;