Curvature regulators #80

Merged
glen merged 21 commits from Vectornaut/dyna3:curvature-regulators into main 2025-04-21 23:40:43 +00:00
2 changed files with 34 additions and 30 deletions
Showing only changes of commit 81e423fcbe - Show all commits

View file

@ -11,7 +11,7 @@ use crate::{
// load an example assembly for testing. this code will be removed once we've // load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system // built a more formal test assembly system
fn load_gen_assemb(assembly: &Assembly) { fn load_gen_assemb(assembly: &Assembly) {
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("gemini_a"), String::from("gemini_a"),
String::from("Castor"), String::from("Castor"),
@ -19,7 +19,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(0.5, 0.5, 0.0, 1.0) engine::sphere(0.5, 0.5, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("gemini_b"), String::from("gemini_b"),
String::from("Pollux"), String::from("Pollux"),
@ -27,7 +27,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(-0.5, -0.5, 0.0, 1.0) engine::sphere(-0.5, -0.5, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("ursa_major"), String::from("ursa_major"),
String::from("Ursa major"), String::from("Ursa major"),
@ -35,7 +35,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(-0.5, 0.5, 0.0, 0.75) engine::sphere(-0.5, 0.5, 0.0, 0.75)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("ursa_minor"), String::from("ursa_minor"),
String::from("Ursa minor"), String::from("Ursa minor"),
@ -43,7 +43,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(0.5, -0.5, 0.0, 0.5) engine::sphere(0.5, -0.5, 0.0, 0.5)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("moon_deimos"), String::from("moon_deimos"),
String::from("Deimos"), String::from("Deimos"),
@ -51,7 +51,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(0.0, 0.15, 1.0, 0.25) engine::sphere(0.0, 0.15, 1.0, 0.25)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("moon_phobos"), String::from("moon_phobos"),
String::from("Phobos"), String::from("Phobos"),
@ -66,7 +66,7 @@ fn load_gen_assemb(assembly: &Assembly) {
// built a more formal test assembly system // built a more formal test assembly system
fn load_low_curv_assemb(assembly: &Assembly) { fn load_low_curv_assemb(assembly: &Assembly) {
let a = 0.75_f64.sqrt(); let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"central".to_string(), "central".to_string(),
"Central".to_string(), "Central".to_string(),
@ -74,7 +74,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere(0.0, 0.0, 0.0, 1.0) engine::sphere(0.0, 0.0, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"assemb_plane".to_string(), "assemb_plane".to_string(),
"Assembly plane".to_string(), "Assembly plane".to_string(),
@ -82,7 +82,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"side1".to_string(), "side1".to_string(),
"Side 1".to_string(), "Side 1".to_string(),
@ -90,7 +90,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"side2".to_string(), "side2".to_string(),
"Side 2".to_string(), "Side 2".to_string(),
@ -98,7 +98,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"side3".to_string(), "side3".to_string(),
"Side 3".to_string(), "Side 3".to_string(),
@ -106,7 +106,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"corner1".to_string(), "corner1".to_string(),
"Corner 1".to_string(), "Corner 1".to_string(),
@ -114,7 +114,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
"corner2".to_string(), "corner2".to_string(),
"Corner 2".to_string(), "Corner 2".to_string(),
@ -122,7 +122,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_sphere(
Element::new( Element::new(
String::from("corner3"), String::from("corner3"),
String::from("Corner 3"), String::from("Corner 3"),

View file

@ -261,28 +261,33 @@ impl Assembly {
// --- inserting elements and regulators --- // --- inserting elements and regulators ---
// insert an element into the assembly without checking whether we already // insert a sphere into the assembly without checking whether we already
// have an element with the same identifier. any element that does have the // have an element with the same identifier. any element that does have the
// same identifier will get kicked out of the `elements_by_id` index // same identifier will get kicked out of the `elements_by_id` index
fn insert_element_unchecked(&self, elt: Element) -> ElementKey { fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey {
// insert the sphere
let id = elt.id.clone(); let id = elt.id.clone();
let key = self.elements.update(|elts| elts.insert(elt)); let key = self.elements.update(|elts| elts.insert(elt));
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key));
// regulate the sphere's curvature
self.insert_new_half_curvature_regulator(key);
key key
} }
pub fn try_insert_element(&self, elt: Element) -> Option<ElementKey> { pub fn try_insert_sphere(&self, elt: Element) -> Option<ElementKey> {
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 {
Some(self.insert_element_unchecked(elt)) Some(self.insert_sphere_unchecked(elt))
} else { } else {
None None
} }
} }
pub fn insert_new_sphere(self) { pub fn insert_new_sphere(&self) {
// find the next unused identifier in the default sequence // find the next unused identifier in the default sequence
let mut id_num = 1; let mut id_num = 1;
let mut id = format!("sphere{}", id_num); let mut id = format!("sphere{}", id_num);
@ -294,7 +299,7 @@ impl Assembly {
} }
// create and insert a sphere // create and insert a sphere
let key = self.insert_element_unchecked( let _ = self.insert_sphere_unchecked(
Element::new( Element::new(
id, id,
format!("Sphere {}", id_num), format!("Sphere {}", id_num),
@ -302,9 +307,6 @@ impl Assembly {
sphere(0.0, 0.0, 0.0, 1.0) sphere(0.0, 0.0, 0.0, 1.0)
) )
); );
// create and insert a curvature regulator
self.insert_new_half_curvature_regulator(key);
} }
fn insert_regulator(&self, regulator: Rc<dyn Regulator>) { fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
@ -312,7 +314,7 @@ impl Assembly {
let key = self.regulators.update( let key = self.regulators.update(
|regs| regs.insert(regulator) |regs| regs.insert(regulator)
); );
let subject_regulators: Vec<_> = self.elements.with( let subject_regulators: Vec<_> = self.elements.with_untracked(
|elts| subjects.into_iter().map( |elts| subjects.into_iter().map(
|subj| elts[subj].regulators |subj| elts[subj].regulators
).collect() ).collect()
@ -324,7 +326,7 @@ impl Assembly {
/* DEBUG */ /* DEBUG */
// print an updated list of regulators // print an updated list of regulators
console::log_1(&JsValue::from("Regulators:")); console::log_1(&JsValue::from("Regulators:"));
self.regulators.with(|regs| { self.regulators.with_untracked(|regs| {
for (_, reg) in regs.into_iter() { for (_, reg) in regs.into_iter() {
console::log_1(&JsValue::from(format!( console::log_1(&JsValue::from(format!(
" {:?}: {}", " {:?}: {}",
@ -344,7 +346,7 @@ impl Assembly {
}); });
} }
pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) { pub fn insert_new_product_regulator(&self, subjects: [ElementKey; 2]) {
// create and insert a new product regulator // create and insert a new product regulator
let measurement = self.elements.map( let measurement = self.elements.map(
move |elts| { move |elts| {
@ -365,17 +367,18 @@ impl Assembly {
// update the realization when the regulator becomes a constraint, or is // update the realization when the regulator becomes a constraint, or is
// edited while acting as a constraint // edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || { create_effect(move || {
console::log_1(&JsValue::from( console::log_1(&JsValue::from(
format!("Updated regulator with subjects {:?}", subjects) format!("Updated regulator with subjects {:?}", subjects)
)); ));
if set_point.with(|set_pt| set_pt.is_present()) { if set_point.with(|set_pt| set_pt.is_present()) {
self.realize(); self_for_effect.realize();
} }
}); });
} }
pub fn insert_new_half_curvature_regulator(self, subject: ElementKey) { pub fn insert_new_half_curvature_regulator(&self, subject: ElementKey) {
// create and insert a new half-curvature regulator // create and insert a new half-curvature regulator
let measurement = self.elements.map( let measurement = self.elements.map(
move |elts| elts[subject].representation.with(|rep| rep[3]) move |elts| elts[subject].representation.with(|rep| rep[3])
@ -389,12 +392,13 @@ impl Assembly {
// update the realization when the regulator becomes a constraint, or is // update the realization when the regulator becomes a constraint, or is
// edited while acting as a constraint // edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || { create_effect(move || {
glen marked this conversation as resolved Outdated

This function evokes a few questions:

A) Seems like there is some duplication at least of structure/behavior here with the insert_new_product_regulator; is there anything that can be profitable factored out (some common insert_regulator common functionality that both of these can use)?

B) Why do you have to do so much work updating the guess (I think that's what's going on) when you start regulating curvature, but it seems like you don't do much of anything when you start regulating an inversive distance?

C) This very much has the look of engine code that has leaked into the assembly: nitty gritty dealing with the internal coordinates of an element. Any chance it could be implemented in the engine and just called here, or perhaps even just deferred until a pre-processing step when the realization is called? Either way, it also might make the code between insert curvature regulator and insert product regulator more similar and easier to share between the two.

This function evokes a few questions: A) Seems like there is some duplication at least of structure/behavior here with the insert_new_product_regulator; is there anything that can be profitable factored out (some common insert_regulator common functionality that both of these can use)? B) Why do you have to do so much work updating the guess (I think that's what's going on) when you start regulating curvature, but it seems like you don't do much of anything when you start regulating an inversive distance? C) This very much has the look of engine code that has leaked into the assembly: nitty gritty dealing with the internal coordinates of an element. Any chance it could be implemented in the engine and just called here, or perhaps even just deferred until a pre-processing step when the realization is called? Either way, it also might make the code between insert curvature regulator and insert product regulator more similar and easier to share between the two.

A) Seems like there is some duplication at least of structure/behavior here with the insert_new_product_regulator; is there anything that can be profitable factored out (some common insert_regulator common functionality that both of these can use)?

I agree that the code is organized similarly, but I haven't found anything we can usefully factor out. For example, I don't see a way to put the shared organization in a shared constructor, because ProductRegulator and HalfCurvatureRegulator are different structures. The only thing that seems really straightforwardly shared is initializing the set point to

create_signal(SpecifiedValue::from_empty_spec()),

which seems like too little code to factor out profitably.

> A) Seems like there is some duplication at least of structure/behavior here with the insert_new_product_regulator; is there anything that can be profitable factored out (some common insert_regulator common functionality that both of these can use)? I agree that the code is organized similarly, but I haven't found anything we can usefully factor out. For example, I don't see a way to put the shared organization in a shared constructor, because `ProductRegulator` and `HalfCurvatureRegulator` are different structures. The only thing that seems really straightforwardly shared is initializing the set point to ```create_signal(SpecifiedValue::from_empty_spec())```, which seems like too little code to factor out profitably.

B) Why do you have to do so much work updating the guess (I think that's what's going on) when you start regulating curvature, but it seems like you don't do much of anything when you start regulating an inversive distance?

We don't have to be so careful about updating the guess. My first prototype of the curvature regulator didn't do anything special when it started regulating; the curvature component of the representation vector would just get overwritten with the desired value at the beginning of the engine's realization routine. That seemed to work fine in the examples I played with.

I felt compelled to be careful about updating the guess for three related reasons:

  1. Just overwriting the curvature component of the representation vector messes up the vector's normalization. I worried that starting from a non-normalized guess could make the engine more likely to stall, and could make the assembly change especially erratically when the curvature regulator is turned on.
  2. I could imagine using a curvature regulator to make extreme curvature changes. For example, someone who's doing conformal geometry on the boundary of hyperbolic 3-space might switch between the Poincaré model and the upper half-space model by switching the curvature of the boundary sphere between 1 and 0. I worried that if we just took whatever realization the engine happened to find, extreme curvature changes might throw the sphere far outside the field of view.
  3. For a moderately curved sphere with nothing constrained except its curvature, one might expect that changing the curvature shouldn't affect the center much. I wanted to demonstrate that we could meet this expectation, in case you thought it was important.

I'd be open to also choosing the initial guess more carefully when we start regulating an inversive distance. That seems tricker to me, though, and I haven't felt a need for it.

> B) Why do you have to do so much work updating the guess (I think that's what's going on) when you start regulating curvature, but it seems like you don't do much of anything when you start regulating an inversive distance? We don't *have* to be so careful about updating the guess. My first prototype of the curvature regulator didn't do anything special when it started regulating; the curvature component of the representation vector would just get overwritten with the desired value at the beginning of the engine's realization routine. That seemed to work fine in the examples I played with. I felt compelled to be careful about updating the guess for three related reasons: 1. Just overwriting the curvature component of the representation vector messes up the vector's normalization. I worried that starting from a non-normalized guess could make the engine more likely to stall, and could make the assembly change especially erratically when the curvature regulator is turned on. 2. I could imagine using a curvature regulator to make extreme curvature changes. For example, someone who's doing conformal geometry on the boundary of hyperbolic 3-space might switch between the Poincaré model and the upper half-space model by switching the curvature of the boundary sphere between 1 and 0. I worried that if we just took whatever realization the engine happened to find, extreme curvature changes might throw the sphere far outside the field of view. 3. For a moderately curved sphere with nothing constrained except its curvature, one might expect that changing the curvature shouldn't affect the center much. I wanted to demonstrate that we could meet this expectation, in case you thought it was important. I'd be open to also choosing the initial guess more carefully when we start regulating an inversive distance. That seems tricker to me, though, and I haven't felt a need for it.

C) This very much has the look of engine code that has leaked into the assembly: nitty gritty dealing with the internal coordinates of an element.

Yes, I think this code is on the border between the assembly module's responsibilities and the engine's responsibilities. I decided to put it in the assembly module because its core purpose is to decide how an otherwise unconstrained sphere should behave when you change its curvature. That seems more like user interaction than like constraint solving to me.

On the other hand, the engine does provide representation-specific implementations of other user-facing, mostly-representation-agnostic tasks, like creating a sphere from a center and a radius (sphere) or a direction, an offset, and a curvature (sphere_with_offset). If we can find a mostly-representation-agnostic way to describe how we update the curvature, maybe it would feel similar in spirit to those tasks.

> C) This very much has the look of engine code that has leaked into the assembly: nitty gritty dealing with the internal coordinates of an element. Yes, I think this code is on the border between the assembly module's responsibilities and the engine's responsibilities. I decided to put it in the assembly module because its core purpose is to decide how an otherwise unconstrained sphere should behave when you change its curvature. That seems more like user interaction than like constraint solving to me. On the other hand, the engine does provide representation-specific implementations of other user-facing, mostly-representation-agnostic tasks, like creating a sphere from a center and a radius (`sphere`) or a direction, an offset, and a curvature (`sphere_with_offset`). If we can find a mostly-representation-agnostic way to describe how we update the curvature, maybe it would feel similar in spirit to those tasks.

On the other hand, the engine does provide representation-specific implementations of other user-facing, mostly-representation-agnostic tasks […]. If we can find a mostly-representation-agnostic way to describe how we update the curvature, maybe it would feel similar in spirit to those tasks.

I've adopted this approach in commit 4654bf0 by moving the half-curvature change routine into the engine module. I haven't come up with a representation-agnostic description of how the sphere is supposed to change, so I'm just calling the routine change_half_curvature for now.

> On the other hand, the engine does provide representation-specific implementations of other user-facing, mostly-representation-agnostic tasks […]. If we can find a mostly-representation-agnostic way to describe how we update the curvature, maybe it would feel similar in spirit to those tasks. I've adopted this approach in commit 4654bf0 by moving the half-curvature change routine into the engine module. I haven't come up with a representation-agnostic description of how the sphere is supposed to change, so I'm just calling the routine `change_half_curvature` for now.
console::log_1(&JsValue::from( console::log_1(&JsValue::from(
format!("Updated regulator with subjects [{}]", subject) format!("Updated regulator with subjects [{}]", subject)
)); ));
if let Some(half_curv) = set_point.with(|set_pt| set_pt.value) { if let Some(half_curv) = set_point.with(|set_pt| set_pt.value) {
let representation = self.elements.with( let representation = self_for_effect.elements.with_untracked(
|elts| elts[subject].representation |elts| elts[subject].representation
); );
representation.update(|rep| { representation.update(|rep| {
@ -428,7 +432,7 @@ impl Assembly {
) )
)); ));
}); });
self.realize(); self_for_effect.realize();
} }
}); });
} }