Simplify the realization triggering system (#105)

Simplifies the system that reactively triggers realizations, at the cost of removing the preconditioning step described in issue #101 and doing unnecessary realizations after certain kinds of updates.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#105
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
This commit is contained in:
Vectornaut 2025-07-31 22:21:32 +00:00 committed by Glen Whitney
parent 0801200210
commit 2eba80fb69
4 changed files with 48 additions and 104 deletions

View file

@ -16,7 +16,6 @@ use crate::{
components::{display::DisplayItem, outline::OutlineItem}, components::{display::DisplayItem, outline::OutlineItem},
engine::{ engine::{
Q, Q,
change_half_curvature,
local_unif_to_std, local_unif_to_std,
point, point,
project_point_to_normalized, project_point_to_normalized,
@ -358,16 +357,6 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>; fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>; fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>; fn set_point(&self) -> Signal<SpecifiedValue>;
// this method is used to responsively precondition the assembly for
// realization when the regulator becomes a constraint, or is edited while
// acting as a constraint. it should track the set point, do any desired
// preconditioning when the set point is present, and use its return value
// to report whether the set is present. the default implementation does no
// preconditioning
fn try_activate(&self) -> bool {
self.set_point().with(|set_pt| set_pt.is_present())
}
} }
impl Hash for dyn Regulator { impl Hash for dyn Regulator {
@ -488,18 +477,6 @@ impl Regulator for HalfCurvatureRegulator {
fn set_point(&self) -> Signal<SpecifiedValue> { fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point self.set_point
} }
fn try_activate(&self) -> bool {
match self.set_point.with(|set_pt| set_pt.value) {
Some(half_curv) => {
self.subject.representation().update(
|rep| change_half_curvature(rep, half_curv)
);
true
}
None => false
}
}
} }
impl Serial for HalfCurvatureRegulator { impl Serial for HalfCurvatureRegulator {
@ -552,8 +529,7 @@ pub struct Assembly {
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>, pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization control // realization control
pub keep_realized: Signal<bool>, pub realization_trigger: Signal<()>,
pub needs_realization: Signal<bool>,
// realization diagnostics // realization diagnostics
pub realization_status: Signal<Result<(), String>>, pub realization_status: Signal<Result<(), String>>,
@ -568,21 +544,23 @@ impl Assembly {
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)), tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(BTreeMap::default()), elements_by_id: create_signal(BTreeMap::default()),
keep_realized: create_signal(true), realization_trigger: create_signal(()),
needs_realization: create_signal(false),
realization_status: create_signal(Ok(())), realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new()) descent_history: create_signal(DescentHistory::new())
}; };
// realize the assembly whenever it becomes simultaneously true that // realize the assembly whenever the element list, the regulator list,
// we're trying to keep it realized and it needs realization // a regulator's set point, or the realization trigger is updated
let assembly_for_effect = assembly.clone(); let assembly_for_effect = assembly.clone();
create_effect(move || { create_effect(move || {
let should_realize = assembly_for_effect.keep_realized.get() assembly_for_effect.elements.track();
&& assembly_for_effect.needs_realization.get(); assembly_for_effect.regulators.with(
if should_realize { |regs| for reg in regs {
assembly_for_effect.realize(); reg.set_point().track();
} }
);
assembly_for_effect.realization_trigger.track();
assembly_for_effect.realize();
}); });
assembly assembly
@ -646,19 +624,6 @@ impl Assembly {
regulators.update(|regs| regs.insert(regulator.clone())); regulators.update(|regs| regs.insert(regulator.clone()));
} }
// request a realization when the regulator becomes a constraint, or is
// edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || {
/* DEBUG */
// log the regulator update
console_log!("Updated regulator with subjects {:?}", regulator.subjects());
if regulator.try_activate() {
self_for_effect.needs_realization.set(true);
}
});
/* DEBUG */ /* DEBUG */
// print an updated list of regulators // print an updated list of regulators
console_log!("Regulators:"); console_log!("Regulators:");
@ -726,8 +691,10 @@ impl Assembly {
} else { } else {
console_log!("✅️ Target accuracy achieved!"); console_log!("✅️ Target accuracy achieved!");
} }
console_log!("Steps: {}", history.scaled_loss.len() - 1); if history.scaled_loss.len() > 0 {
console_log!("Loss: {}", history.scaled_loss.last().unwrap()); console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
}
// report the loss history // report the loss history
self.descent_history.set(history); self.descent_history.set(history);
@ -750,9 +717,6 @@ impl Assembly {
// save the tangent space // save the tangent space
self.tangent.set_silent(tangent); self.tangent.set_silent(tangent);
// clear the realization request flag
self.needs_realization.set(false);
}, },
Err(message) => { Err(message) => {
// report the realization status. the `Err(message)` we're // report the realization status. the `Err(message)` we're
@ -848,10 +812,10 @@ impl Assembly {
}); });
} }
// request a realization to bring the configuration back onto the // trigger a realization to bring the configuration back onto the
// solution variety. this also gets the elements' column indices and the // solution variety. this also gets the elements' column indices and the
// saved tangent space back in sync // saved tangent space back in sync
self.needs_realization.set(true); self.realization_trigger.set(());
} }
} }

View file

@ -14,7 +14,22 @@ pub fn AddRemove() -> View {
button( button(
on:click=|_| { on:click=|_| {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
state.assembly.insert_element_default::<Sphere>(); batch(|| {
// this call is batched to avoid redundant realizations.
// it updates the element list and the regulator list,
// which are both tracked by the realization effect
/* TO DO */
// it would make more to do the batching inside
// `insert_element_default`, but that will have to wait
// until Sycamore handles nested batches correctly.
//
// https://github.com/sycamore-rs/sycamore/issues/802
//
// the nested batch issue is relevant here because the
// assembly loaders in the test assembly chooser use
// `insert_element_default` within larger batches
state.assembly.insert_element_default::<Sphere>();
});
} }
) { "Add sphere" } ) { "Add sphere" }
button( button(

View file

@ -900,9 +900,6 @@ pub fn TestAssemblyChooser() -> View {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let assembly = &state.assembly; let assembly = &state.assembly;
// pause realization
assembly.keep_realized.set(false);
// clear state // clear state
assembly.regulators.update(|regs| regs.clear()); assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear()); assembly.elements.update(|elts| elts.clear());
@ -923,9 +920,6 @@ pub fn TestAssemblyChooser() -> View {
"irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly),
_ => () _ => ()
}; };
// resume realization
assembly.keep_realized.set(true);
}); });
}); });

View file

@ -1,7 +1,6 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter}; use std::fmt::{Display, Error, Formatter};
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
// --- elements --- // --- elements ---
@ -50,40 +49,6 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
rep.scale_mut(0.5 / rep[3]); 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
pub fn change_half_curvature(rep: &mut DVector<f64>, half_curv: f64) {
// set the sphere's half-curvature to the desired value
rep[3] = half_curv;
// restore normalization by contracting toward the curvature axis
const SIZE_THRESHOLD: f64 = 1e-9;
let half_q_lt = -2.0 * half_curv * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let mut spatial = rep.fixed_rows_mut::<3>(0);
let q_sp = spatial.norm_squared();
if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD {
spatial.copy_from_slice(
&[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()]
);
} else {
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
spatial.scale_mut(1.0 / scaling);
rep[4] /= scaling;
}
/* DEBUG */
// verify normalization
let rep_for_debug = rep.clone();
console::log_1(&JsValue::from(
format!(
"Sphere self-product after curvature change: {}",
rep_for_debug.dot(&(&*Q * &rep_for_debug))
)
));
}
// --- partial matrices --- // --- partial matrices ---
pub struct MatrixEntry { pub struct MatrixEntry {
@ -199,13 +164,6 @@ impl ConfigSubspace {
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
); );
/* DEBUG */
// print the eigenvalues
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
console::log_1(&JsValue::from(
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
));
// express the basis in the standard coordinates // express the basis in the standard coordinates
let basis_std = proj_to_std * &basis_proj; let basis_std = proj_to_std * &basis_proj;
@ -425,9 +383,22 @@ pub fn realize_gram(
// start the descent history // start the descent history
let mut history = DescentHistory::new(); let mut history = DescentHistory::new();
// handle the case where the assembly is empty. our general realization
// routine can't handle this case because it builds the Hessian using
// `DMatrix::from_columns`, which panics when the list of columns is empty
let assembly_dim = guess.ncols();
if assembly_dim == 0 {
let result = Ok(
ConfigNeighborhood {
config: guess.clone(),
nbhd: ConfigSubspace::zero(0)
}
);
return Realization { result, history }
}
// find the dimension of the search space // find the dimension of the search space
let element_dim = guess.nrows(); let element_dim = guess.nrows();
let assembly_dim = guess.ncols();
let total_dim = element_dim * assembly_dim; let total_dim = element_dim * assembly_dim;
// scale the tolerance // scale the tolerance