Trigger realization more directly

Simplify 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.

The new system should trigger a realization after any update that could
affect the assembly's deformation space. For simplicity, any update to
the regulator list triggers an update, even if it doesn't affect the set
of constraints. In particular, adding a regulator triggers an
unnecessary realization.
This commit is contained in:
Aaron Fenyes 2025-07-24 14:59:21 -07:00
parent 0801200210
commit c73008d702
4 changed files with 36 additions and 96 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,12 @@ 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
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

@ -50,40 +50,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 {
@ -425,9 +391,20 @@ pub fn realize_gram(
// start the descent history // start the descent history
let mut history = DescentHistory::new(); let mut history = DescentHistory::new();
// handle the empty-assembly case
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