Skip to main content

geop_core_math/solvers/system/
structure.rs

1//! [`System::structure`]: how a system falls apart for a solve — into
2//! groups no term ties together, and within each, the blocks that only hang
3//! off the others.
4//!
5//! A screw mated to a bracket, a bracket fastened to a link: the screw's
6//! constraints read the screw and the bracket, and decide nothing about the
7//! bracket — whatever the bracket does, the screw can follow, and how it
8//! does is the screw's own business. Such a block is a **pendant** on the
9//! blocks its terms read besides it: it is solved after them, alone, with
10//! them where they ended up, and enclosed after them, over their boxes. An
11//! assembly of a chain of links with hundreds of parts fastened and mated
12//! to them is then one small system of the links, and hundreds of systems
13//! of one part each, worked on in lockstep layer by layer — not one dense
14//! system of every part, whose solve costs the cube of its size.
15//!
16//! A block `b` hangs off the blocks `S` its remaining terms read besides it
17//! when, where the blocks are:
18//!
19//! - nothing pulls it, and no limit reads it: either could hold back what
20//!   it hangs off, a limit only where it is reached;
21//! - its terms hold: it is where they put it, given `S`;
22//! - its terms' rows have the same rank by its own steps as by all of `S`'s
23//!   and its own: they constrain nothing of `S`, to first order — whatever
24//!   `S` does, it can follow. (A part mated to a link and to the ground is
25//!   no pendant: its rows also hold the link.)
26//! - held where it is ([`Mobility::Held`]), it can be carried along by one
27//!   of `S` ([`Variable::carried`]): a solve starts it there. And every
28//!   value of `S` that only follows ([`Mobility::Follows`]) is still read by
29//!   some other term: it was decided by those, not by where `b` was held. A
30//!   joint's coordinate only `b`'s joint reads would otherwise be held
31//!   where it was, and `b` turned with the link it hangs off, where the
32//!   whole solve held `b` and turned the joint.
33//!
34//! Blocks are taken off one after another, each pendant's terms with it, as
35//! long as one qualifies: a screw comes off its bracket, then the bracket
36//! off its link. What is left of each group is its core, solved as before.
37//!
38//! What changes is only what a block held where it was holds back: a
39//! pendant moves with what it hangs off, and of its own freedom stays as
40//! near where it was as its terms let it (see [`System::solve`]); but it no
41//! longer holds back what it hangs off. A link carrying a hundred screws is
42//! as easy to move as one carrying none.
43
44use crate::{
45    scalars::Scalar,
46    solvers::{linear::eliminate, system::tolerance},
47    union_find::UnionFind,
48};
49
50use super::{Bound, Equations, Group, Linearized, Mobility, System, Term, Variable, every_term};
51
52/// The groups of a system's blocks a solve works on (see the module docs),
53/// in layers: every group's [`Group::on`] blocks are in groups of earlier
54/// layers.
55#[derive(Clone, Debug, Default, PartialEq, Eq)]
56pub struct Structure {
57    pub groups: Vec<Group>,
58    /// Per group no term ties to another — before any block was taken off
59    /// it — its blocks and terms: what a solve falls back to solving as one
60    /// where a pendant cannot follow what it hangs off after all.
61    pub components: Vec<Group>,
62}
63
64impl Structure {
65    /// Whether it can be of a system of `blocks` blocks, `constraints`
66    /// constraints and `preferences` preferences: every block in one of its
67    /// components, and every term it names one of the system's.
68    pub fn fits(&self, blocks: usize, (constraints, preferences): (usize, usize)) -> bool {
69        let covered: usize = self.components.iter().map(|c| c.blocks.len()).sum();
70        let within = |g: &Group| {
71            g.blocks.iter().chain(&g.on).all(|&b| b < blocks)
72                && g.constraints.iter().all(|&c| c < constraints)
73                && g.preferences.iter().all(|&p| p < preferences)
74        };
75        covered == blocks && self.groups.iter().chain(&self.components).all(within)
76    }
77
78    /// How many layers its groups are in.
79    pub fn layers(&self) -> usize {
80        self.groups.iter().map(|g| g.layer + 1).max().unwrap_or(0)
81    }
82
83    /// The groups of layer `layer`, by index.
84    pub fn layer(&self, layer: usize) -> Vec<usize> {
85        (0..self.groups.len())
86            .filter(|&g| self.groups[g].layer == layer)
87            .collect()
88    }
89}
90
91/// A term as the structure is found from: what it is, the blocks it reads,
92/// and its rows where the blocks are.
93struct Read {
94    term: Term,
95    blocks: Vec<usize>,
96    made: Linearized,
97}
98
99/// The terms' rows over a box of steps around where the blocks are (see
100/// [`System::structure`]), made the first time one is asked: one more
101/// evaluation of every term, with every block moved — which a system
102/// whose blocks hang off nothing never needs.
103struct Over<'s, 'e, E: Equations> {
104    system: &'s System<'e, E>,
105    at: &'s [E::Value],
106    terms: Vec<Term>,
107    rows: std::cell::OnceCell<Vec<Option<Linearized>>>,
108}
109
110impl<E: Equations> Over<'_, '_, E> {
111    /// The rows of the term `k` over the box, as slopes by the steps from
112    /// where the blocks are; none where it cannot be made over it.
113    fn get(&self, k: usize) -> Option<&Linearized> {
114        let rows = self.rows.get_or_init(|| {
115            let Some((boxed, differentials)) = self.system.around(self.at) else {
116                return vec![None; self.terms.len()];
117            };
118            (self
119                .system
120                .equations
121                .evaluate(&boxed, &self.terms)
122                .into_iter())
123            .map(|made| {
124                let mut made = made?;
125                for (block, rows) in &mut made.jacobian {
126                    if let Some(Some(d)) = differentials.get(*block) {
127                        for row in rows.iter_mut() {
128                            *row = super::compose(row, d);
129                        }
130                    }
131                }
132                Some(made)
133            })
134            .collect()
135        });
136        rows.get(k)?.as_ref()
137    }
138}
139
140impl<E: Equations> System<'_, E> {
141    /// The structure of the system where the blocks are at `at`: its
142    /// groups, the pendants taken off them, in layers (see the module
143    /// docs). A term that reads no block, or cannot be made at `at`, is in
144    /// none: nothing can be done about it.
145    ///
146    /// Whether a block's rows constrain what it hangs off is a question of
147    /// rank, decided over a box of steps around `at` as wide as the
148    /// solver's tolerance, as the enclosure decides its rows (see
149    /// [`System::enclose`]): where the blocks are, rounding leaves a hair of
150    /// a row that depends on the others, which no box holds apart from
151    /// zero.
152    pub fn structure(&self, at: &[E::Value]) -> Structure {
153        let n = self.mobility.len();
154        let terms = every_term(self.equations.count());
155        let made = self.equations.evaluate(at, &terms);
156        let mut reads: Vec<Read> = Vec::new();
157        for (&term, made) in terms.iter().zip(made) {
158            let Some(made) = made else {
159                continue;
160            };
161            let mut blocks: Vec<usize> = (made.jacobian.iter())
162                .map(|(block, _)| *block)
163                .filter(|&b| b < n)
164                .collect();
165            blocks.sort_unstable();
166            blocks.dedup();
167            if !blocks.is_empty() {
168                reads.push(Read { term, blocks, made });
169            }
170        }
171        let components = grouped(n, reads.iter().map(|r| (r.term, r.blocks.as_slice())));
172        let Ok(tolerance) = tolerance(self.scale) else {
173            return Structure {
174                groups: components.clone(),
175                components,
176            };
177        };
178        // Per block, the terms still on it, by index into `reads`.
179        let mut on: Vec<Vec<usize>> = vec![Vec::new(); n];
180        for (k, read) in reads.iter().enumerate() {
181            for &b in &read.blocks {
182                on[b].push(k);
183            }
184        }
185        let over = Over {
186            system: self,
187            at,
188            terms: reads.iter().map(|r| r.term).collect(),
189            rows: Default::default(),
190        };
191        let mut alive = vec![true; reads.len()];
192        let mut peeled: Vec<Option<Pendant>> = vec![None; n];
193        let mut order = Vec::new();
194        let mut waiting: std::collections::BTreeSet<usize> = (0..n).collect();
195        while let Some(b) = waiting.pop_first() {
196            if peeled[b].is_some() {
197                continue;
198            }
199            let own: Vec<usize> = on[b].iter().copied().filter(|&k| alive[k]).collect();
200            let Some(pendant) = self.hangs_off(at, b, own, &reads, &over, &alive, &on, tolerance)
201            else {
202                continue;
203            };
204            for &k in &pendant.terms {
205                alive[k] = false;
206            }
207            waiting.extend(pendant.on.iter().copied());
208            peeled[b] = Some(pendant);
209            order.push(b);
210        }
211        // The cores: what is left of each component.
212        let cores = grouped(
213            n,
214            (reads.iter().zip(&alive))
215                .filter(|(_, alive)| **alive)
216                .map(|(r, _)| (r.term, r.blocks.as_slice())),
217        );
218        let component_of =
219            |block: usize| (components.iter()).position(|g| g.blocks.binary_search(&block).is_ok());
220        let mut groups: Vec<Group> = Vec::new();
221        let mut group_of: Vec<Option<usize>> = vec![None; n];
222        for mut core in cores {
223            // A block taken off leaves a core of nothing.
224            core.blocks.retain(|&b| peeled[b].is_none());
225            if core.blocks.is_empty() {
226                continue;
227            }
228            core.component = component_of(core.blocks[0]).unwrap_or(0);
229            for &b in &core.blocks {
230                group_of[b] = Some(groups.len());
231            }
232            groups.push(core);
233        }
234        // Pendants after what they hang off: the last taken off first.
235        for &b in order.iter().rev() {
236            let Some(pendant) = &peeled[b] else {
237                continue;
238            };
239            let layer = (pendant.on.iter())
240                .filter_map(|&s| group_of[s].map(|g| groups[g].layer + 1))
241                .max()
242                .unwrap_or(0);
243            let mut group = Group {
244                blocks: vec![b],
245                on: pendant.on.clone(),
246                layer,
247                component: component_of(b).unwrap_or(0),
248                own: pendant.own,
249                ..Default::default()
250            };
251            for &k in &pendant.terms {
252                match reads[k].term {
253                    Term::Constraint(c) => group.constraints.push(c),
254                    Term::Preference(p) => group.preferences.push(p),
255                }
256            }
257            group_of[b] = Some(groups.len());
258            groups.push(group);
259        }
260        groups.sort_by_key(|g| g.layer);
261        Structure { groups, components }
262    }
263
264    /// The blocks over the box of steps as wide as the solver's tolerance
265    /// around `at`, and per block the differential of its retraction there
266    /// (see [`Variable::differential`]); none where one cannot be made.
267    #[allow(clippy::type_complexity)]
268    fn around(&self, at: &[E::Value]) -> Option<(Vec<E::Value>, Vec<Option<Vec<Vec<Scalar>>>>)> {
269        let radius = tolerance(self.scale).ok()?;
270        let mut boxed = Vec::with_capacity(at.len());
271        let mut differentials = Vec::with_capacity(at.len());
272        for (value, mobility) in at.iter().zip(&self.mobility) {
273            let step = match mobility.free() {
274                true => vec![radius.neg().union(radius); value.dimension()],
275                false => vec![Scalar::ZERO; value.dimension()],
276            };
277            boxed.push(value.retract_over(&step).ok()?);
278            differentials.push(value.differential(&step).ok()?);
279        }
280        Some((boxed, differentials))
281    }
282
283    /// What the block `b` hangs off, if it is a pendant (see the module
284    /// docs): `own` the terms still on it, by index into `reads`.
285    #[allow(clippy::too_many_arguments)]
286    fn hangs_off(
287        &self,
288        at: &[E::Value],
289        b: usize,
290        own: Vec<usize>,
291        reads: &[Read],
292        over: &Over<E>,
293        alive: &[bool],
294        on: &[Vec<usize>],
295        tolerance: Scalar,
296    ) -> Option<Pendant> {
297        let mobility = self.mobility[b];
298        if own.is_empty() || !mobility.free() {
299            return None;
300        }
301        let mut hangs: Vec<usize> = Vec::new();
302        for &k in &own {
303            let read = &reads[k];
304            let Term::Constraint(c) = read.term else {
305                return None;
306            };
307            let bound = self.equations.bound(c);
308            if bound != Bound::Zero
309                || !(read.made.values.iter()).all(|&r| bound.holds(r, tolerance))
310            {
311                return None;
312            }
313            hangs.extend(read.blocks.iter().copied().filter(|&o| o != b));
314        }
315        hangs.sort_unstable();
316        hangs.dedup();
317        // The rows constrain nothing of what it hangs off.
318        let mut columns = vec![b];
319        columns.extend(&hangs);
320        let mut offsets = Vec::with_capacity(columns.len());
321        let mut width = 0;
322        for &c in &columns {
323            offsets.push(width);
324            width += at[c].dimension();
325        }
326        let own_width = at[b].dimension();
327        let mut rows: Vec<Vec<Scalar>> = Vec::new();
328        for &k in &own {
329            // Hanging off nothing, it constrains nothing else: its rows
330            // only count its own freedom, which they do where it is.
331            let made = match hangs.is_empty() {
332                true => &reads[k].made,
333                false => over.get(k)?,
334            };
335            for r in 0..made.values.len() {
336                let mut row = vec![Scalar::ZERO; width];
337                for (block, derivatives) in &made.jacobian {
338                    let Some(at) = columns.iter().position(|c| c == block) else {
339                        continue;
340                    };
341                    let Some(local) = derivatives.get(r) else {
342                        continue;
343                    };
344                    for (slot, &d) in row.iter_mut().skip(offsets[at]).zip(local) {
345                        *slot = slot.add(d);
346                    }
347                }
348                rows.push(row);
349            }
350        }
351        // Eliminated by its own columns only — the others go along — every
352        // row left over could be zero: it says nothing its own steps do not.
353        // As many pivots as its own columns at most, so the cost is the
354        // rows' size, not its square.
355        let reduced = eliminate(rows, own_width);
356        let constrained = reduced.pivots.len();
357        let rest = (reduced.rows.iter()).skip(constrained);
358        if !rest.flatten().all(|v| v.could_be_equal(Scalar::ZERO)) {
359            return None;
360        }
361        if mobility == Mobility::Held {
362            let carried = (hangs.iter()).any(|&s| at[b].carried(&at[s], &at[s]).is_some());
363            if !hangs.is_empty() && !carried {
364                return None;
365            }
366            let decided = |f: usize| {
367                (on[f].iter()).any(|&k| {
368                    alive[k]
369                        && !own.contains(&k)
370                        && matches!(reads[k].term, Term::Constraint(c)
371                            if self.equations.bound(c) == Bound::Zero)
372                })
373            };
374            if (hangs.iter()).any(|&f| self.mobility[f] == Mobility::Follows && !decided(f)) {
375                return None;
376            }
377        }
378        Some(Pendant {
379            on: hangs,
380            own: own_width - constrained,
381            terms: own,
382        })
383    }
384}
385
386/// A block taken off as a pendant: what it hangs off, how many ways its
387/// terms leave it free to move with that held, and its terms, by index.
388#[derive(Clone)]
389struct Pendant {
390    on: Vec<usize>,
391    own: usize,
392    terms: Vec<usize>,
393}
394
395/// The groups of `n` blocks the terms `reads` — each with the blocks it
396/// reads — tie together, each with its terms, in order of their first
397/// blocks.
398fn grouped<'r>(n: usize, reads: impl Iterator<Item = (Term, &'r [usize])> + Clone) -> Vec<Group> {
399    let mut sets = UnionFind::new(n);
400    for (_, blocks) in reads.clone() {
401        for pair in blocks.windows(2) {
402            sets.union(pair[0], pair[1]);
403        }
404    }
405    let mut by_root: std::collections::BTreeMap<usize, Group> = Default::default();
406    for block in 0..n {
407        by_root
408            .entry(sets.find(block))
409            .or_default()
410            .blocks
411            .push(block);
412    }
413    for (term, blocks) in reads {
414        let Some(group) = by_root.get_mut(&sets.find(blocks[0])) else {
415            continue;
416        };
417        match term {
418            Term::Constraint(c) => group.constraints.push(c),
419            Term::Preference(p) => group.preferences.push(p),
420        }
421    }
422    by_root.into_values().collect()
423}