feat: Point coordinate regulators #118
2 changed files with 64 additions and 34 deletions
|
@ -31,9 +31,14 @@ 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)
|
||||
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
|
||||
- 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`.
|
||||
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
|
||||
- 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
|
||||
`trunk` in another directory `DIR` via `cargo install --root DIR trunk`
|
||||
|
||||
### Play with the prototype
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
cmp::Ordering,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt,
|
||||
fmt::{Debug, Formatter},
|
||||
fmt::{Debug, Display, Formatter},
|
||||
hash::{Hash, Hasher},
|
||||
rc::Rc,
|
||||
sync::{atomic, atomic::AtomicU64},
|
||||
|
@ -128,7 +128,7 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
|
|||
|
||||
impl Debug for dyn Element {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
self.id().fmt(f)
|
||||
Debug::fmt(&self.id(), f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,13 +248,19 @@ 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 {
|
||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||
let index = self.column_index().expect(
|
||||
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||
);
|
||||
index_message("Sphere", &self.id, "it").as_str());
|
||||
problem.gram.push_sym(index, index, 1.0);
|
||||
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
||||
problem.guess.set_column(
|
||||
index, &self.representation.get_clone_untracked());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,11 +363,11 @@ impl Serial for Point {
|
|||
impl ProblemPoser for Point {
|
||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||
let index = self.column_index().expect(
|
||||
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||
);
|
||||
index_message("Point", &self.id, "it").as_str());
|
||||
problem.gram.push_sym(index, index, 0.0);
|
||||
problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5);
|
||||
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
||||
problem.guess.set_column(
|
||||
index, &self.representation.get_clone_untracked());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -406,7 +412,8 @@ pub struct InversiveDistanceRegulator {
|
|||
|
||||
impl InversiveDistanceRegulator {
|
||||
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self {
|
||||
let representations = subjects.each_ref().map(|subj| subj.representation());
|
||||
let representations = subjects.each_ref().map(
|
||||
|subj| subj.representation());
|
||||
let measurement = create_memo(move || {
|
||||
representations[0].with(|rep_0|
|
||||
representations[1].with(|rep_1|
|
||||
|
@ -448,8 +455,8 @@ impl ProblemPoser for InversiveDistanceRegulator {
|
|||
if let Some(val) = set_pt.value {
|
||||
let [row, col] = self.subjects.each_ref().map(
|
||||
|subj| subj.column_index().expect(
|
||||
"Subjects should be indexed before inversive distance regulator writes problem data"
|
||||
)
|
||||
index_message("Subject", subj.id(),
|
||||
"inversive distance regulator").as_str())
|
||||
);
|
||||
problem.gram.push_sym(row, col, val);
|
||||
}
|
||||
|
@ -502,8 +509,8 @@ impl ProblemPoser for HalfCurvatureRegulator {
|
|||
self.set_point.with_untracked(|set_pt| {
|
||||
if let Some(val) = set_pt.value {
|
||||
let col = self.subject.column_index().expect(
|
||||
"Subject should be indexed before half-curvature regulator writes problem data"
|
||||
);
|
||||
index_message("Subject", &self.subject.id,
|
||||
"half-curvature regulator").as_str());
|
||||
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
|
||||
}
|
||||
});
|
||||
|
@ -519,7 +526,7 @@ impl Axis {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Axis {
|
||||
impl Display for Axis {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.name())
|
||||
}
|
||||
|
@ -539,7 +546,9 @@ impl PointCoordinateRegulator {
|
|||
move |rep| rep[axis as usize]
|
||||
);
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
Self { subject, axis, measurement, set_point, serial: Self::next_serial() }
|
||||
Self {
|
||||
subject, axis, measurement, set_point, serial: Self::next_serial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -558,10 +567,11 @@ impl ProblemPoser for PointCoordinateRegulator {
|
|||
self.set_point.with_untracked(|set_pt| {
|
||||
if let Some(val) = set_pt.value {
|
||||
let col = self.subject.column_index().expect(
|
||||
"Subject must be indexed before point-coordinate regulator poses.");
|
||||
index_message("Subject", &self.subject.id,
|
||||
"point-coordinate regulator").as_str());
|
||||
problem.frozen.push(self.axis as usize, col, val);
|
||||
// Check if all three spatial coordinates have been frozen, and if so,
|
||||
// freeze the norm component as well
|
||||
// If all three of the subject's spatial coordinates have been
|
||||
// frozen, then freeze its norm component:
|
||||
Vectornaut marked this conversation as resolved
|
||||
let mut coords = [0.0; Axis::CARDINALITY];
|
||||
let mut nset: usize = 0;
|
||||
for &MatrixEntry {index, value} in &(problem.frozen) {
|
||||
|
@ -573,7 +583,9 @@ impl ProblemPoser for PointCoordinateRegulator {
|
|||
if nset == Axis::CARDINALITY {
|
||||
let [x, y, z] = coords;
|
||||
problem.frozen.push(
|
||||
Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]);
|
||||
Point::NORM_COMPONENT,
|
||||
col,
|
||||
point(x,y,z)[Point::NORM_COMPONENT]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -672,7 +684,8 @@ impl Assembly {
|
|||
let id = elt.id().clone();
|
||||
let elt_rc = Rc::new(elt);
|
||||
self.elements.update(|elts| elts.insert(elt_rc.clone()));
|
||||
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
|
||||
self.elements_by_id.update(
|
||||
|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
|
||||
|
||||
// create and insert the element's default regulators
|
||||
for reg in elt_rc.default_regulators() {
|
||||
|
@ -748,7 +761,8 @@ impl Assembly {
|
|||
pub fn load_config(&self, config: &DMatrix<f64>) {
|
||||
for elt in self.elements.get_clone_untracked() {
|
||||
elt.representation().update(
|
||||
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
|
||||
|rep| rep.set_column(
|
||||
0, &config.column(elt.column_index().unwrap()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -893,7 +907,8 @@ impl Assembly {
|
|||
if column_index < realized_dim {
|
||||
// this element had a column index when we started, so by
|
||||
// invariant (1), it's reflected in the tangent space
|
||||
let mut target_columns = motion_proj.columns_mut(0, realized_dim);
|
||||
let mut target_columns =
|
||||
motion_proj.columns_mut(0, realized_dim);
|
||||
target_columns += self.tangent.with(
|
||||
|tan| tan.proj(&elt_motion.velocity, column_index)
|
||||
);
|
||||
|
@ -901,9 +916,10 @@ impl Assembly {
|
|||
// this element didn't have a column index when we started, so
|
||||
// by invariant (2), it's unconstrained
|
||||
let mut target_column = motion_proj.column_mut(column_index);
|
||||
let unif_to_std = elt_motion.element.representation().with_untracked(
|
||||
|rep| local_unif_to_std(rep.as_view())
|
||||
);
|
||||
let unif_to_std = elt_motion
|
||||
.element.representation()
|
||||
.with_untracked(
|
||||
|rep| local_unif_to_std(rep.as_view()));
|
||||
target_column += unif_to_std * elt_motion.velocity;
|
||||
}
|
||||
}
|
||||
|
@ -920,7 +936,9 @@ impl Assembly {
|
|||
elt.project_to_normalized(rep);
|
||||
},
|
||||
None => {
|
||||
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
|
||||
console_log!(
|
||||
"No velocity to unpack for fresh element \"{}\"",
|
||||
elt.id())
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -940,7 +958,8 @@ mod tests {
|
|||
use crate::engine;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
|
||||
#[should_panic(expected =
|
||||
Vectornaut marked this conversation as resolved
Vectornaut
commented
When I originally wrote this test, it bothered me that the identifiers 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.
|
||||
"Sphere \"sphere\" must be indexed before it writes problem data")]
|
||||
fn unindexed_element_test() {
|
||||
let _ = create_root(|| {
|
||||
let elt = Sphere::default("sphere".to_string(), 0);
|
||||
|
@ -949,17 +968,20 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
|
||||
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \
|
||||
inversive distance regulator writes problem data")]
|
||||
fn unindexed_subject_test_inversive_distance() {
|
||||
let _ = create_root(|| {
|
||||
let subjects = [0, 1].map(
|
||||
|k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc<dyn Element>
|
||||
|k| Rc::new(Sphere::default(
|
||||
format!("sphere{k}"), k)) as Rc<dyn Element>
|
||||
);
|
||||
subjects[0].set_column_index(0);
|
||||
InversiveDistanceRegulator {
|
||||
subjects: subjects,
|
||||
measurement: create_memo(|| 0.0),
|
||||
set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()),
|
||||
set_point: create_signal(
|
||||
SpecifiedValue::try_from("0.0".to_string()).unwrap()),
|
||||
serial: InversiveDistanceRegulator::next_serial()
|
||||
}.pose(&mut ConstraintProblem::new(2));
|
||||
});
|
||||
|
@ -988,8 +1010,10 @@ mod tests {
|
|||
// nudge the sphere repeatedly along the `z` axis
|
||||
const STEP_SIZE: f64 = 0.0025;
|
||||
const STEP_CNT: usize = 400;
|
||||
let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone());
|
||||
let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]);
|
||||
let sphere = assembly.elements_by_id.with(
|
||||
|elts_by_id| elts_by_id[sphere_id].clone());
|
||||
let velocity =
|
||||
DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]);
|
||||
for _ in 0..STEP_CNT {
|
||||
assembly.deform(
|
||||
vec![
|
||||
|
@ -1007,7 +1031,8 @@ mod tests {
|
|||
let final_half_curv = sphere.representation().with_untracked(
|
||||
|rep| rep[Sphere::CURVATURE_COMPONENT]
|
||||
);
|
||||
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
|
||||
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs()
|
||||
< DRIFT_TOL);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue
Flagging the inconsistent bracket formatting for later. Right now, this would be formatted as
{ index, value }
elsewhere, but during review we discussed potentially changing this convention in the future.It might shock you to know that I don't actually particularly care whether the braces-on-one-line spacing is uniform. If we both agree not to change them unless we touch a line, we could each just write this one the way we're comfortable writing. It's not like either way is particularly hard to read... Or we could decide to be uniform. It's all cool.