Dispatch normalization routines correctly #87

Merged
glen merged 4 commits from Vectornaut/dyna3:dispatch-normalization into main 2025-06-04 21:01:14 +00:00
3 changed files with 112 additions and 52 deletions

View file

@ -46,6 +46,13 @@ features = [
dyna3 = { path = ".", default-features = false, features = ["dev"] }
wasm-bindgen-test = "0.3.34"
# turn off spurious warnings about the custom config that Sycamore uses
#
glen marked this conversation as resolved Outdated

Do Cargo.toml files allow comments? If so, please comment the meaning/purpose of this new block, it's a bit inscrutable. Something like "previously we used server-side rendering, but we switched to BLAH because BLEE, so warn if SSR is turned on". Of course I may totally have misunderstood what's going on here, that's just meant as an example comment.

Do Cargo.toml files allow comments? If so, please comment the meaning/purpose of this new block, it's a bit inscrutable. Something like "previously we used server-side rendering, but we switched to BLAH because BLEE, so warn if SSR is turned on". Of course I may totally have misunderstood what's going on here, that's just meant as an example comment.

Done in commit e19792d (previously 8732cb7)!

Done in commit e19792d (previously 8732cb7)!
# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr
#
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] }
[profile.release]
opt-level = "s" # optimize for small code size
debug = true # include debug symbols

View file

@ -1,6 +1,5 @@
use nalgebra::{DMatrix, DVector, DVectorView};
use std::{
any::{Any, TypeId},
cell::Cell,
collections::{BTreeMap, BTreeSet},
cmp::Ordering,
@ -20,6 +19,8 @@ use crate::{
change_half_curvature,
local_unif_to_std,
point,
project_point_to_normalized,
project_sphere_to_normalized,
realize_gram,
sphere,
ConfigSubspace,
@ -107,6 +108,10 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
// element is responsible for keeping this set up to date
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
glen marked this conversation as resolved Outdated

Do you like the word "normalize" or "canonicalize"? I feel like the latter more explicitly means "choose the favored representative of an equivalence class" (which I think is mainly what's going on here) and the former can have broader meanings in which the entity is actually changed, like when you find a unit vector in the same direction -- I think that's sometimes called normalization. Only make a change if you feel like "canonicalize" would be a better/clearer choice.

Do you like the word "normalize" or "canonicalize"? I feel like the latter more explicitly means "choose the favored representative of an equivalence class" (which I think is mainly what's going on here) and the former can have broader meanings in which the entity is actually changed, like when you find a unit vector in the same direction -- I think that's sometimes called normalization. Only make a change if you feel like "canonicalize" would be a better/clearer choice.

In my mind, an element is represented by a line through the origin in \mathbb{R}^{4,1}, so choosing a favored representative is equivalent to choosing a particular scaling. That's why I used the word "normalization." However, the implementation of normalize_rep_mut for spheres actually changes the equivalence class. That's because the only thing we use normalize_rep_mut for right now is to keep elements normalized during nudging, and normalizing in a way that preserves the equivalence class makes nudging less stable. If both "normalize" and "canonicalize" imply staying within an equivalence class for you, then maybe we need another term entirely.

In my mind, an element is represented by a line through the origin in $\mathbb{R}^{4,1}$, so choosing a favored representative is equivalent to choosing a particular scaling. That's why I used the word "normalization." However, the implementation of `normalize_rep_mut` for spheres actually changes the equivalence class. That's because the only thing we use `normalize_rep_mut` for right now is to keep elements normalized during nudging, and normalizing in a way that preserves the equivalence class makes nudging less stable. If both "normalize" and "canonicalize" imply staying within an equivalence class for you, then maybe we need another term entirely.

Ah, hmm, I hadn't appreciated that the result of normalize_rep_mut might be in a different equivalence class. "Normalize" does not by itself connote "stay in the same equivalence class, but "normalize representation" does seem to imply strongly that we are just choosing a different representation of the same entity. So I would agree the name needs to change -- but just "normalize" without the "representation" would be OK, since we're not just picking a different representation of the same geometric entity. If you don't like that, perhaps "stabilize" or "standardize" or "choose_canonical" or "nearby_canonical" or something?

Ah, hmm, I hadn't appreciated that the result of normalize_rep_mut might be in a different equivalence class. "Normalize" does not by itself connote "stay in the same equivalence class, but "normalize representation" does seem to imply strongly that we are just choosing a different representation of the same entity. So I would agree the name needs to change -- but just "normalize" without the "representation" would be OK, since we're not just picking a different representation of the same geometric entity. If you don't like that, perhaps "stabilize" or "standardize" or "choose_canonical" or "nearby_canonical" or something?

How about something like proj_to_normalized, to communicate that we're projecting the given representation vector to the normalized configuration variety for the given element type?

So I would agree the name needs to change -- but just "normalize" without the "representation" would be OK, since we're not just picking a different representation of the same geometric entity.

The rep isn't working as intended in the name normalize_rep_mut anyway. I added it to emphasize that this method acts on the representation vector provided as an argument, not the vector representing the element the method is called from. This makes it different from the normalize_mut methods for nalgebra matrices.

(Using an associated function instead of a method would be the best way to clarify this, but I don't think it's possible to call associated functions from trait objects. This complaint about the limitation and this unsuccessful attempt to lift the limitation seem to demonstrate that it exists, or at least used to exist. If you want, though, I can try again to use an associated function and look at exactly what compiler error it produced.)

How about something like `proj_to_normalized`, to communicate that we're projecting the given representation vector to the normalized configuration variety for the given element type? > So I would agree the name needs to change -- but just "normalize" without the "representation" would be OK, since we're not just picking a different representation of the same geometric entity. The `rep` isn't working as intended in the name `normalize_rep_mut` anyway. I added it to emphasize that this method acts on the representation vector provided as an argument, not the vector representing the element the method is called from. This makes it different from the [`normalize_mut`](https://docs.rs/nalgebra/latest/nalgebra/base/type.DMatrix.html#method.normalize_mut) methods for nalgebra matrices. (Using an associated function instead of a method would be the best way to clarify this, but I don't think it's possible to call associated functions from trait objects. [This complaint](https://users.rust-lang.org/t/trait-object-safety-still-confuses-me/39699/4) about the limitation and [this unsuccessful attempt](https://github.com/rust-lang/rfcs/pull/2886) to lift the limitation seem to demonstrate that it exists, or at least used to exist. If you want, though, I can try again to use an associated function and look at exactly what compiler error it produced.)

Sure, any of project_to_canonical or project_to_normal or project_to_normalized or canonical_projection or normal_projection would be fine, whatever seems simplest/clearest to you. I'd suggest that with a name that's already this long, there's no point in abbreviating with proj -- just use whatever full word proj would be an abbreviation for. Using a method is fine, don't worry about the associated function thing.

Sure, any of `project_to_canonical` or `project_to_normal` or `project_to_normalized` or `canonical_projection` or `normal_projection` would be fine, whatever seems simplest/clearest to you. I'd suggest that with a name that's already this long, there's no point in abbreviating with `proj` -- just use whatever full word `proj` would be an abbreviation for. Using a method is fine, don't worry about the associated function thing.

I just pushed a commit with better names (54b34e0582). My look-over before pushing was a bit cursory; I'll look at it harder later today.

I just pushed a commit with better names (`54b34e0582`). My look-over before pushing was a bit cursory; I'll look at it harder later today.

I've looked at the renaming commit again and I still think it's fine, so let me know what you think!

I've looked at the renaming commit again and I still think it's fine, so let me know what you think!
// project a representation vector for this kind of element onto its
// normalization variety
fn project_to_normalized(&self, rep: &mut DVector<f64>);
// the configuration matrix column index that was assigned to the element
// last time the assembly was realized, or `None` if the element has never
// been through a realization
@ -221,6 +226,10 @@ impl Element for Sphere {
self.regulators
}
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
project_sphere_to_normalized(rep);
}
fn column_index(&self) -> Option<usize> {
self.column_index.get()
}
@ -313,6 +322,10 @@ impl Element for Point {
self.regulators
}
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
project_point_to_normalized(rep);
}
fn column_index(&self) -> Option<usize> {
self.column_index.get()
}
@ -611,9 +624,7 @@ impl Assembly {
create_effect(move || {
/* DEBUG */
// log the regulator update
console::log_1(&JsValue::from(
format!("Updated regulator with subjects {:?}", regulator.subjects())
));
console_log!("Updated regulator with subjects {:?}", regulator.subjects());
if regulator.try_activate() {
self_for_effect.realize();
@ -622,10 +633,10 @@ impl Assembly {
/* DEBUG */
// print an updated list of regulators
console::log_1(&JsValue::from("Regulators:"));
console_log!("Regulators:");
self.regulators.with_untracked(|regs| {
for reg in regs.into_iter() {
console::log_1(&JsValue::from(format!(
console_log!(
" {:?}: {}",
reg.subjects(),
reg.set_point().with_untracked(
@ -638,7 +649,7 @@ impl Assembly {
}
}
)
)));
);
}
});
}
@ -669,19 +680,11 @@ impl Assembly {
/* DEBUG */
// log the Gram matrix
console::log_1(&JsValue::from("Gram matrix:"));
problem.gram.log_to_console();
console_log!("Gram matrix:\n{}", problem.gram);
/* DEBUG */
// log the initial configuration matrix
console::log_1(&JsValue::from("Old configuration:"));
for j in 0..problem.guess.nrows() {
let mut row_str = String::new();
for k in 0..problem.guess.ncols() {
row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str());
}
console::log_1(&JsValue::from(row_str));
}
console_log!("Old configuration:{:>8.3}", problem.guess);
// look for a configuration with the given Gram matrix
let (config, tangent, success, history) = realize_gram(
@ -690,16 +693,14 @@ impl Assembly {
/* DEBUG */
// report the outcome of the search
console::log_1(&JsValue::from(
if success {
"Target accuracy achieved!"
} else {
"Failed to reach target accuracy"
}
));
console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1));
console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap()));
console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim()));
if success {
console_log!("Target accuracy achieved!")
} else {
console_log!("Failed to reach target accuracy")
}
console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", *history.scaled_loss.last().unwrap());
console_log!("Tangent dimension: {}", tangent.dim());
if success {
// read out the solution
@ -782,29 +783,17 @@ impl Assembly {
// step the assembly along the deformation. this changes the elements'
// normalizations, so we restore those afterward
/* KLUDGE */
// for now, we only restore the normalizations of spheres
for elt in self.elements.get_clone_untracked() {
elt.representation().update_silent(|rep| {
match elt.column_index() {
Some(column_index) => {
// step the assembly along the deformation
// step the element along the deformation and then
// restore its normalization
*rep += motion_proj.column(column_index);
if elt.type_id() == TypeId::of::<Sphere>() {
// restore normalization by contracting toward the
// last coordinate axis
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
let half_q_lt = -2.0 * rep[3] * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
}
elt.project_to_normalized(rep);
},
None => {
console::log_1(&JsValue::from(
format!("No velocity to unpack for fresh element \"{}\"", elt.id())
))
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
}
};
});
@ -821,6 +810,8 @@ impl Assembly {
mod tests {
use super::*;
use crate::engine;
#[test]
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
fn unindexed_element_test() {
@ -846,4 +837,50 @@ mod tests {
}.pose(&mut ConstraintProblem::new(2));
});
}
#[test]
fn curvature_drift_test() {
const INITIAL_RADIUS: f64 = 0.25;
let _ = create_root(|| {
// set up an assembly containing a single sphere centered at the
// origin
let assembly = Assembly::new();
let sphere_id = "sphere0";
let _ = assembly.try_insert_element(
// we create the sphere by hand for two reasons: to choose the
// curvature (which can affect drift rate) and to make the test
// independent of `Sphere::default`
Sphere::new(
String::from(sphere_id),
String::from("Sphere 0"),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS)
)
);
// 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]);
for _ in 0..STEP_CNT {
assembly.deform(
vec![
ElementMotion {
element: sphere.clone(),
velocity: velocity.as_view()
}
]
);
}
// check how much the sphere's curvature has drifted
const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS;
const DRIFT_TOL: f64 = 0.015;
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);
});
}
}

View file

@ -1,5 +1,6 @@
use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter};
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
// --- elements ---
@ -34,6 +35,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6
])
}
// project a sphere's representation vector to the normalization variety by
// contracting toward the last coordinate axis
pub fn project_sphere_to_normalized(rep: &mut DVector<f64>) {
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
let half_q_lt = -2.0 * rep[3] * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
}
// normalize a point's representation vector by scaling
pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
rep.scale_mut(0.5 / rep[3]);
}
// given a sphere's representation vector, change the sphere's half-curvature to
// `half-curv` and then restore normalization by contracting the representation
// vector toward the curvature axis
@ -94,15 +110,6 @@ impl PartialMatrix {
}
}
/* DEBUG */
pub fn log_to_console(&self) {
for &MatrixEntry { index: (row, col), value } in self {
console::log_1(&JsValue::from(
format!(" {} {} {}", row, col, value)
));
}
}
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = a.clone();
for &MatrixEntry { index, value } in self {
@ -128,6 +135,15 @@ impl PartialMatrix {
}
}
impl Display for PartialMatrix {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
for &MatrixEntry { index: (row, col), value } in self {
writeln!(f, " {row} {col} {value}")?;
}
Ok(())
}
}
impl IntoIterator for PartialMatrix {
type Item = MatrixEntry;
type IntoIter = std::vec::IntoIter<Self::Item>;