feat: Point coordinate regulators #118

Merged
Vectornaut merged 9 commits from glen/dyna3:pointCoordRegulators into main 2025-10-13 22:52:03 +00:00
2 changed files with 33 additions and 54 deletions
Showing only changes of commit 50c51ca08f - Show all commits

View file

@ -31,14 +31,11 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) 3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html)
4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) 4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/)
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool 5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool
- In the future, `trunk` can be updated with the same command. You may - In the future, `trunk` can be updated with the same command. You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`.
need the `--locked` flag if your ambient version of `rustc` does not
match that required by `trunk`.
6. Add the `.cargo/bin` folder in your home directory to your executable search path 6. Add the `.cargo/bin` folder in your home directory to your executable search path
- This lets you call Trunk, and other tools installed by Cargo, without specifying their paths - This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
- On POSIX systems, the search path is stored in the `PATH` environment variable - On POSIX systems, the search path is stored in the `PATH` environment variable
- Alternatively, if you don't want to adjust your `PATH`, you can install - Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk`
`trunk` in another directory `DIR` via `cargo install --root DIR trunk`
### Play with the prototype ### Play with the prototype

View file

@ -86,6 +86,14 @@ impl Ord for dyn Serial {
} }
} }
// Small helper function to generate consistent errors when there
// are indexing issues in a ProblemPoser
fn indexing_error(item: &str, name: &str, actor: &str) -> String {
format!(
"{item} \"{name}\" must be indexed before {actor} writes problem data"
)
}
pub trait ProblemPoser { pub trait ProblemPoser {
fn pose(&self, problem: &mut ConstraintProblem); fn pose(&self, problem: &mut ConstraintProblem);
} }
@ -248,19 +256,12 @@ impl Serial for Sphere {
} }
} }
fn index_message(thing: &str, name: &str, actor: &str) -> String {
format!(
"{thing} \"{name}\" must be indexed before {actor} writes problem data"
)
}
impl ProblemPoser for Sphere { impl ProblemPoser for Sphere {
fn pose(&self, problem: &mut ConstraintProblem) { fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect( let index = self.column_index().expect(
index_message("Sphere", &self.id, "it").as_str()); indexing_error("Sphere", &self.id, "it").as_str());
problem.gram.push_sym(index, index, 1.0); problem.gram.push_sym(index, index, 1.0);
problem.guess.set_column( problem.guess.set_column(index, &self.representation.get_clone_untracked());
index, &self.representation.get_clone_untracked());
} }
} }
@ -363,11 +364,10 @@ impl Serial for Point {
impl ProblemPoser for Point { impl ProblemPoser for Point {
fn pose(&self, problem: &mut ConstraintProblem) { fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect( let index = self.column_index().expect(
index_message("Point", &self.id, "it").as_str()); indexing_error("Point", &self.id, "it").as_str());
problem.gram.push_sym(index, index, 0.0); problem.gram.push_sym(index, index, 0.0);
problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5);
problem.guess.set_column( problem.guess.set_column(index, &self.representation.get_clone_untracked());
index, &self.representation.get_clone_untracked());
} }
} }
@ -412,8 +412,7 @@ pub struct InversiveDistanceRegulator {
impl InversiveDistanceRegulator { impl InversiveDistanceRegulator {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self { pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self {
let representations = subjects.each_ref().map( let representations = subjects.each_ref().map(|subj| subj.representation());
|subj| subj.representation());
let measurement = create_memo(move || { let measurement = create_memo(move || {
representations[0].with(|rep_0| representations[0].with(|rep_0|
representations[1].with(|rep_1| representations[1].with(|rep_1|
@ -455,7 +454,7 @@ impl ProblemPoser for InversiveDistanceRegulator {
if let Some(val) = set_pt.value { if let Some(val) = set_pt.value {
let [row, col] = self.subjects.each_ref().map( let [row, col] = self.subjects.each_ref().map(
|subj| subj.column_index().expect( |subj| subj.column_index().expect(
index_message("Subject", subj.id(), indexing_error("Subject", subj.id(),
"inversive distance regulator").as_str()) "inversive distance regulator").as_str())
); );
problem.gram.push_sym(row, col, val); problem.gram.push_sym(row, col, val);
@ -509,7 +508,7 @@ impl ProblemPoser for HalfCurvatureRegulator {
self.set_point.with_untracked(|set_pt| { self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value { if let Some(val) = set_pt.value {
let col = self.subject.column_index().expect( let col = self.subject.column_index().expect(
index_message("Subject", &self.subject.id, indexing_error("Subject", &self.subject.id,
"half-curvature regulator").as_str()); "half-curvature regulator").as_str());
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
} }
@ -546,9 +545,7 @@ impl PointCoordinateRegulator {
move |rep| rep[axis as usize] move |rep| rep[axis as usize]
); );
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
Self { Self { subject, axis, measurement, set_point, serial: Self::next_serial() }
subject, axis, measurement, set_point, serial: Self::next_serial()
}
} }
} }
@ -567,7 +564,7 @@ impl ProblemPoser for PointCoordinateRegulator {
self.set_point.with_untracked(|set_pt| { self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value { if let Some(val) = set_pt.value {
let col = self.subject.column_index().expect( let col = self.subject.column_index().expect(
index_message("Subject", &self.subject.id, indexing_error("Subject", &self.subject.id,
"point-coordinate regulator").as_str()); "point-coordinate regulator").as_str());
problem.frozen.push(self.axis as usize, col, val); problem.frozen.push(self.axis as usize, col, val);
// If all three of the subject's spatial coordinates have been // If all three of the subject's spatial coordinates have been
@ -583,9 +580,7 @@ impl ProblemPoser for PointCoordinateRegulator {
if nset == Axis::CARDINALITY { if nset == Axis::CARDINALITY {
let [x, y, z] = coords; let [x, y, z] = coords;
problem.frozen.push( problem.frozen.push(
Point::NORM_COMPONENT, Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]);
col,
point(x,y,z)[Point::NORM_COMPONENT]);
} }
} }
}); });
@ -684,8 +679,7 @@ impl Assembly {
let id = elt.id().clone(); let id = elt.id().clone();
let elt_rc = Rc::new(elt); let elt_rc = Rc::new(elt);
self.elements.update(|elts| elts.insert(elt_rc.clone())); self.elements.update(|elts| elts.insert(elt_rc.clone()));
self.elements_by_id.update( self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
// create and insert the element's default regulators // create and insert the element's default regulators
for reg in elt_rc.default_regulators() { for reg in elt_rc.default_regulators() {
@ -694,9 +688,7 @@ impl Assembly {
} }
pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
let can_insert = self.elements_by_id.with_untracked( let can_insert = self.elements_by_id.with_untracked(|elts_by_id| !elts_by_id.contains_key(elt.id()));
|elts_by_id| !elts_by_id.contains_key(elt.id())
);
if can_insert { if can_insert {
self.insert_element_unchecked(elt); self.insert_element_unchecked(elt);
} }
@ -761,8 +753,7 @@ impl Assembly {
pub fn load_config(&self, config: &DMatrix<f64>) { pub fn load_config(&self, config: &DMatrix<f64>) {
for elt in self.elements.get_clone_untracked() { for elt in self.elements.get_clone_untracked() {
elt.representation().update( elt.representation().update(
|rep| rep.set_column( |rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
0, &config.column(elt.column_index().unwrap()))
); );
} }
} }
@ -907,8 +898,7 @@ impl Assembly {
if column_index < realized_dim { if column_index < realized_dim {
// this element had a column index when we started, so by // this element had a column index when we started, so by
// invariant (1), it's reflected in the tangent space // invariant (1), it's reflected in the tangent space
let mut target_columns = let mut target_columns = motion_proj.columns_mut(0, realized_dim);
motion_proj.columns_mut(0, realized_dim);
target_columns += self.tangent.with( target_columns += self.tangent.with(
|tan| tan.proj(&elt_motion.velocity, column_index) |tan| tan.proj(&elt_motion.velocity, column_index)
); );
@ -916,10 +906,9 @@ impl Assembly {
// this element didn't have a column index when we started, so // this element didn't have a column index when we started, so
// by invariant (2), it's unconstrained // by invariant (2), it's unconstrained
let mut target_column = motion_proj.column_mut(column_index); let mut target_column = motion_proj.column_mut(column_index);
let unif_to_std = elt_motion let unif_to_std = elt_motion.element.representation().with_untracked(
.element.representation() |rep| local_unif_to_std(rep.as_view())
.with_untracked( );
|rep| local_unif_to_std(rep.as_view()));
target_column += unif_to_std * elt_motion.velocity; target_column += unif_to_std * elt_motion.velocity;
} }
} }
@ -936,9 +925,7 @@ impl Assembly {
elt.project_to_normalized(rep); elt.project_to_normalized(rep);
}, },
None => { None => {
console_log!( console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
"No velocity to unpack for fresh element \"{}\"",
elt.id())
}, },
}; };
}); });
@ -973,15 +960,13 @@ inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() { fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| { let _ = create_root(|| {
Vectornaut marked this conversation as resolved

When I originally wrote this test, it bothered me that the identifiers sphere0 and sphere1 were assigned but never tested. The new, more informative indexing error message makes them part of the test, which is very satisfying.

When I originally wrote this test, it bothered me that the identifiers `sphere0` and `sphere1` were assigned but never tested. The new, more informative indexing error message makes them part of the test, which is very satisfying.
let subjects = [0, 1].map( let subjects = [0, 1].map(
|k| Rc::new(Sphere::default( |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc<dyn Element>
format!("sphere{k}"), k)) as Rc<dyn Element>
); );
subjects[0].set_column_index(0); subjects[0].set_column_index(0);
InversiveDistanceRegulator { InversiveDistanceRegulator {
subjects: subjects, subjects: subjects,
measurement: create_memo(|| 0.0), measurement: create_memo(|| 0.0),
set_point: create_signal( set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()),
SpecifiedValue::try_from("0.0".to_string()).unwrap()),
serial: InversiveDistanceRegulator::next_serial() serial: InversiveDistanceRegulator::next_serial()
}.pose(&mut ConstraintProblem::new(2)); }.pose(&mut ConstraintProblem::new(2));
}); });
@ -1010,10 +995,8 @@ inversive distance regulator writes problem data")]
// nudge the sphere repeatedly along the `z` axis // nudge the sphere repeatedly along the `z` axis
const STEP_SIZE: f64 = 0.0025; const STEP_SIZE: f64 = 0.0025;
const STEP_CNT: usize = 400; const STEP_CNT: usize = 400;
let sphere = assembly.elements_by_id.with( let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone());
|elts_by_id| elts_by_id[sphere_id].clone()); let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]);
let velocity =
DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]);
for _ in 0..STEP_CNT { for _ in 0..STEP_CNT {
assembly.deform( assembly.deform(
vec![ vec![
@ -1031,8 +1014,7 @@ inversive distance regulator writes problem data")]
let final_half_curv = sphere.representation().with_untracked( let final_half_curv = sphere.representation().with_untracked(
|rep| rep[Sphere::CURVATURE_COMPONENT] |rep| rep[Sphere::CURVATURE_COMPONENT]
); );
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
< DRIFT_TOL);
}); });
} }
} }