Curvature regulators #80
|
@ -166,18 +166,7 @@ pub fn AddRemove() -> View {
|
|||
button(
|
||||
on:click=|_| {
|
||||
let state = use_context::<AppState>();
|
||||
state.assembly.insert_new_element();
|
||||
|
||||
/* DEBUG */
|
||||
// print updated list of elements by identifier
|
||||
console::log_1(&JsValue::from("elements by identifier:"));
|
||||
for (id, key) in state.assembly.elements_by_id.get_clone().iter() {
|
||||
console::log_3(
|
||||
&JsValue::from(" "),
|
||||
&JsValue::from(id),
|
||||
&JsValue::from(*key)
|
||||
);
|
||||
}
|
||||
state.assembly.insert_new_sphere();
|
||||
}
|
||||
) { "+" }
|
||||
button(
|
||||
|
|
|
@ -10,9 +10,11 @@ use crate::{
|
|||
Q,
|
||||
local_unif_to_std,
|
||||
realize_gram,
|
||||
sphere,
|
||||
ConfigSubspace,
|
||||
ConstraintProblem
|
||||
},
|
||||
outline::OutlineItem,
|
||||
specified::SpecifiedValue
|
||||
};
|
||||
|
||||
|
@ -36,8 +38,8 @@ pub struct Element {
|
|||
pub color: ElementColor,
|
||||
pub representation: Signal<DVector<f64>>,
|
||||
|
||||
// All regulators with this element as a subject. The assembly owning
|
||||
// this element is responsible for keeping this set up to date.
|
||||
// the regulators this element is subject to. the assembly that owns the
|
||||
// element is responsible for keeping this set up to date
|
||||
pub regulators: Signal<BTreeSet<RegulatorKey>>,
|
||||
|
||||
// a serial number, assigned by `Element::new`, that uniquely identifies
|
||||
|
@ -132,7 +134,7 @@ impl Element {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait Regulator {
|
||||
pub trait Regulator: OutlineItem {
|
||||
glen marked this conversation as resolved
Outdated
|
||||
// get information
|
||||
fn subjects(&self) -> Vec<ElementKey>;
|
||||
fn measurement(&self) -> ReadSignal<f64>;
|
||||
|
@ -177,6 +179,39 @@ impl Regulator for ProductRegulator {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct HalfCurvatureRegulator {
|
||||
pub subject: ElementKey,
|
||||
pub measurement: ReadSignal<f64>,
|
||||
pub set_point: Signal<SpecifiedValue>
|
||||
}
|
||||
|
||||
impl Regulator for HalfCurvatureRegulator {
|
||||
fn subjects(&self) -> Vec<ElementKey> {
|
||||
vec![self.subject]
|
||||
}
|
||||
|
||||
fn measurement(&self) -> ReadSignal<f64> {
|
||||
self.measurement
|
||||
}
|
||||
|
||||
fn set_point(&self) -> Signal<SpecifiedValue> {
|
||||
self.set_point
|
||||
}
|
||||
|
||||
fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
|
||||
self.set_point.with_untracked(|set_pt| {
|
||||
if let Some(val) = set_pt.value {
|
||||
if let Some(col) = elts[self.subject].column_index {
|
||||
const CURVATURE_COMPONENT: usize = 3;
|
||||
problem.frozen.push(CURVATURE_COMPONENT, col, val);
|
||||
} else {
|
||||
panic!("Tried to write problem data from a regulator with an unindexed subject");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
At some point, it would be nice to have symbolic names for all of the components in some standard place and use them anywhere needed. I am not saying it has to be part of this PR. But it could be. Thoughts? At some point, it would be nice to have symbolic names for all of the components in some standard place and use them anywhere needed. I am not saying it has to be part of this PR. But it could be. Thoughts?
glen
commented
Since you have to make the other 3 the same as this 3, likely you may as well do centralized symbolic names for all the components right at the moment, although anything that makes the two 3s the same thing and not magic numbers is OK for the moment if you prefer not to do a full central enumeration of components. Since you have to make the _other_ 3 the same as this 3, likely you may as well do centralized symbolic names for all the components right at the moment, although anything that makes the two 3s the same thing and not magic numbers is OK for the moment if you prefer not to do a full central enumeration of components.
Vectornaut
commented
Done, by making Done, by making `CURVATURE_COMPONENT` an associated constant of the `Element` structure (commit 7f21e7e). When we introduce separate structures for different kinds of elements, this constant will go in the `Sphere` structure.
glen
commented
There's still pretty tight coupling between Elements and the Engine, because Elements are storing their own representations in Engine coordinates. So the Elements are not really abstract geometric elements, divorced from Engine details. Or put another way, an Engine change would entail deeper changes to the innards of Elements than might be ideal. But there's no real need to pre-worry about fixing that until we actually want to try another Engine. And the revised code does eliminate unlinked occurrences of the same magic number, so resolving this conversation. There's still pretty tight coupling between Elements and the Engine, because Elements are storing their own representations in Engine coordinates. So the Elements are not really abstract geometric elements, divorced from Engine details. Or put another way, an Engine change would entail deeper changes to the innards of Elements than might be ideal. But there's no real need to pre-worry about fixing that until we actually want to try another Engine. And the revised code does eliminate unlinked occurrences of the same magic number, so resolving this conversation.
|
||||
}
|
||||
|
||||
// the velocity is expressed in uniform coordinates
|
||||
pub struct ElementMotion<'a> {
|
||||
pub key: ElementKey,
|
||||
|
@ -223,23 +258,25 @@ impl Assembly {
|
|||
// insert an element into the assembly without checking whether we already
|
||||
// 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
|
||||
fn insert_element_unchecked(&self, elt: Element) {
|
||||
fn insert_element_unchecked(&self, elt: Element) -> ElementKey {
|
||||
let id = elt.id.clone();
|
||||
let key = self.elements.update(|elts| elts.insert(elt));
|
||||
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key));
|
||||
key
|
||||
}
|
||||
|
||||
pub fn try_insert_element(&self, elt: Element) -> bool {
|
||||
pub fn try_insert_element(&self, elt: Element) -> Option<ElementKey> {
|
||||
let can_insert = self.elements_by_id.with_untracked(
|
||||
|elts_by_id| !elts_by_id.contains_key(&elt.id)
|
||||
);
|
||||
if can_insert {
|
||||
self.insert_element_unchecked(elt);
|
||||
Some(self.insert_element_unchecked(elt))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
can_insert
|
||||
}
|
||||
|
||||
pub fn insert_new_element(&self) {
|
||||
pub fn insert_new_sphere(self) {
|
||||
// find the next unused identifier in the default sequence
|
||||
let mut id_num = 1;
|
||||
let mut id = format!("sphere{}", id_num);
|
||||
|
@ -250,15 +287,18 @@ impl Assembly {
|
|||
id = format!("sphere{}", id_num);
|
||||
}
|
||||
|
||||
// create and insert a new element
|
||||
self.insert_element_unchecked(
|
||||
// create and insert a sphere
|
||||
let key = self.insert_element_unchecked(
|
||||
Element::new(
|
||||
id,
|
||||
format!("Sphere {}", id_num),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5])
|
||||
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>) {
|
||||
|
@ -274,26 +314,6 @@ impl Assembly {
|
|||
for regulators in subject_regulators {
|
||||
regulators.update(|regs| regs.insert(key));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) {
|
||||
// create and insert a new product regulator
|
||||
let measurement = self.elements.map(
|
||||
move |elts| {
|
||||
let representations = subjects.map(|subj| elts[subj].representation);
|
||||
representations[0].with(|rep_0|
|
||||
representations[1].with(|rep_1|
|
||||
rep_0.dot(&(&*Q * rep_1))
|
||||
)
|
||||
)
|
||||
}
|
||||
);
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
self.insert_regulator(Rc::new(ProductRegulator {
|
||||
subjects: subjects,
|
||||
measurement: measurement,
|
||||
set_point: set_point
|
||||
}));
|
||||
|
||||
/* DEBUG */
|
||||
// print an updated list of regulators
|
||||
|
@ -316,6 +336,26 @@ impl Assembly {
|
|||
)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) {
|
||||
// create and insert a new product regulator
|
||||
let measurement = self.elements.map(
|
||||
move |elts| {
|
||||
let representations = subjects.map(|subj| elts[subj].representation);
|
||||
representations[0].with(|rep_0|
|
||||
representations[1].with(|rep_1|
|
||||
rep_0.dot(&(&*Q * rep_1))
|
||||
)
|
||||
)
|
||||
}
|
||||
);
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
self.insert_regulator(Rc::new(ProductRegulator {
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
I figured out what's bothering me about this code. It's connected with my feeling that these two methods (insert_new_product_regulator and insert_new_half_curvature_regulator) should either be the same or be more highly factored than they are now. It's that there's a de-localization of concerns at the moment. It appears that the definition of the measurement of a product regulator or a half-curvature regulator currently "lives" in these insertion methods. But such code should be part of the code for that flavor of regulator, not in an "outside" method like this insertion gadget. The current code structure suggests that it would be perfectly legitimate to insert a half-curvature regulator that measures the mean of the first three coordinates. It would not be. In other words, we don't have ideal encapsulation of regulators at the moment. Please refactor; and hopefully, after refactoring, these two functions will be identical or very thin veneer over something identical. Thanks! I figured out what's bothering me about this code. It's connected with my feeling that these two methods (insert_new_product_regulator and insert_new_half_curvature_regulator) should either be the same or be more highly factored than they are now. It's that there's a de-localization of concerns at the moment. It appears that the definition of the measurement of a product regulator or a half-curvature regulator currently "lives" in these insertion methods. But such code should be part of the code for that flavor of regulator, not in an "outside" method like this insertion gadget. The current code structure suggests that it would be perfectly legitimate to insert a half-curvature regulator that measures the mean of the first three coordinates. It would not be.
In other words, we don't have ideal encapsulation of regulators at the moment. Please refactor; and hopefully, after refactoring, these two functions will be identical or very thin veneer over something identical. Thanks!
Vectornaut
commented
I think that's a great idea. I propose moving the insertion code into a method of the > But such code should be part of the code for that flavor of regulator, not in an "outside" method like this insertion gadget.
I think that's a great idea. I propose moving the insertion code into a method of the `Regulator` trait, analogous to how the code that writes constraint problem data is kept in the `pose` method of the `ProblemPoser` trait. If you'd like, we could try to make a supertrait for anything that can be inserted into an assembly, which would be even more analogous to the `ProblemPoser` trait, but I don't think that's called for yet.
glen
commented
yes a method of regulator trait sounds good, no not necessary to generalize into an assembly-inserting supertrait, at least not until we have something else that would also use that supertrait, i.e. no need for premature abstraction. Thanks! yes a method of regulator trait sounds good, no not necessary to generalize into an assembly-inserting supertrait, at least not until we have something else that would also use that supertrait, i.e. no need for premature abstraction. Thanks!
Vectornaut
commented
|
||||
subjects: subjects,
|
||||
measurement: measurement,
|
||||
set_point: set_point
|
||||
}));
|
||||
|
||||
// update the realization when the regulator becomes a constraint, or is
|
||||
// edited while acting as a constraint
|
||||
|
@ -329,6 +369,64 @@ impl Assembly {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn insert_new_half_curvature_regulator(self, subject: ElementKey) {
|
||||
// create and insert a new half-curvature regulator
|
||||
let measurement = self.elements.map(
|
||||
move |elts| elts[subject].representation.with(|rep| rep[3])
|
||||
);
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
self.insert_regulator(Rc::new(HalfCurvatureRegulator {
|
||||
subject: subject,
|
||||
measurement: measurement,
|
||||
set_point: set_point
|
||||
}));
|
||||
|
||||
// update the realization when the regulator becomes a constraint, or is
|
||||
// edited while acting as a constraint
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
Aha, this 3 is the same 3 as that other one I recently commented about. They need to be instances of the same symbol. Otherwise, they're just magic numbers that may leave a maintainer wondering whether and why they need to be the same. Thanks for fixing. Aha, this 3 is the _same_ 3 as that other one I recently commented about. They need to be instances of the same symbol. Otherwise, they're just magic numbers that may leave a maintainer wondering whether and why they need to be the same. Thanks for fixing.
|
||||
create_effect(move || {
|
||||
console::log_1(&JsValue::from(
|
||||
format!("Updated regulator with subjects [{}]", subject)
|
||||
));
|
||||
if let Some(half_curv) = set_point.with(|set_pt| set_pt.value) {
|
||||
let representation = self.elements.with(
|
||||
|elts| elts[subject].representation
|
||||
);
|
||||
representation.update(|rep| {
|
||||
// set the sphere's half-curvature to the desired value
|
||||
rep[3] = half_curv;
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
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.
Vectornaut
commented
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
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.
Vectornaut
commented
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:
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.
Vectornaut
commented
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 ( > 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.
Vectornaut
commented
I've adopted this approach in commit > 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.
|
||||
|
||||
// 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))
|
||||
)
|
||||
));
|
||||
});
|
||||
self.realize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- realization ---
|
||||
|
||||
pub fn realize(&self) {
|
||||
|
|
|
@ -10,7 +10,13 @@ use web_sys::{
|
|||
use crate::{
|
||||
AppState,
|
||||
assembly,
|
||||
assembly::{ElementKey, Regulator, RegulatorKey},
|
||||
assembly::{
|
||||
ElementKey,
|
||||
HalfCurvatureRegulator,
|
||||
ProductRegulator,
|
||||
Regulator,
|
||||
RegulatorKey
|
||||
},
|
||||
specified::SpecifiedValue
|
||||
};
|
||||
|
||||
|
@ -84,27 +90,53 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait OutlineItem {
|
||||
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View;
|
||||
}
|
||||
|
||||
impl OutlineItem for ProductRegulator {
|
||||
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View {
|
||||
let state = use_context::<AppState>();
|
||||
let other_subject = if self.subjects[0] == element_key {
|
||||
self.subjects[1]
|
||||
} else {
|
||||
self.subjects[0]
|
||||
};
|
||||
let other_subject_label = state.assembly.elements.with(
|
||||
|elts| elts[other_subject].label.clone()
|
||||
);
|
||||
view! {
|
||||
li(class="regulator") {
|
||||
div(class="regulator-label") { (other_subject_label) }
|
||||
div(class="regulator-type") { "Inversive distance" }
|
||||
RegulatorInput(regulator=self)
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineItem for HalfCurvatureRegulator {
|
||||
fn outline_item(self: Rc<Self>, _element_key: ElementKey) -> View {
|
||||
view! {
|
||||
li(class="regulator") {
|
||||
div(class="regulator-label") // for spacing
|
||||
div(class="regulator-type") { "Half-curvature" }
|
||||
RegulatorInput(regulator=self)
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a list item that shows a regulator in an outline view of an element
|
||||
#[component(inline_props)]
|
||||
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = &state.assembly;
|
||||
let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone());
|
||||
let subjects = regulator.subjects();
|
||||
let other_subject = if subjects[0] == element_key {
|
||||
subjects[1]
|
||||
} else {
|
||||
subjects[0]
|
||||
};
|
||||
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
|
||||
view! {
|
||||
li(class="regulator") {
|
||||
div(class="regulator-label") { (other_subject_label) }
|
||||
div(class="regulator-type") { "Inversive distance" }
|
||||
RegulatorInput(regulator=regulator)
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
let regulator = state.assembly.regulators.with(
|
||||
|regs| regs[regulator_key].clone()
|
||||
);
|
||||
regulator.outline_item(element_key)
|
||||
}
|
||||
|
||||
// a list item that shows an element in an outline view of an assembly
|
||||
|
@ -127,7 +159,15 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
|||
};
|
||||
let regulated = element.regulators.map(|regs| regs.len() > 0);
|
||||
let regulator_list = element.regulators.map(
|
||||
|regs| regs.clone().into_iter().collect()
|
||||
move |elt_reg_keys| elt_reg_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.sorted_by_key(
|
||||
|®_key| state.assembly.regulators.with(
|
||||
|regs| regs[reg_key].subjects().len()
|
||||
)
|
||||
)
|
||||
.collect()
|
||||
);
|
||||
let details_node = create_node_ref();
|
||||
view! {
|
||||
|
|
The check here and the potential panic if it fails (and analogous code in two other ProblemPoser implementations) is evidence of misfactoring: in fact, the ProblemPosers only ever get to execute when every element has just been assigned a column index. So there is no need to have such a check.
I am open to any reasonable way to factor these checks out. One is to insist that all elements in an assembly always have a column index. As far as I can see, the only thing that None column indices are currently used for is a transient period between an Element being added to an Assembly and the next call to that Assembly being realized. That period could be eliminated by simply assigning each Element the next available column index and immediately calling realize. Or there could be a method (likely in engine?) that takes a realized assembly and a new completely unconstrained element and updates the realization to include the unconstrained element -- that should presumably be far less work than a full call to realize. As far as I can tell, the only consistency bit that needs to be kept up is the tangent space bases in the realization. But I suppose it should be possible to calculate the new tangent space basis matriceso directly in the case of a new unconstrained element added to a realized assembly.
Or we can insist that elements always have an index by assigning indices on creation, and dropping the invariant that an element with an index has to have tangent motions in the corresponding column of the tangent space basis matrices. That might not be so terrible -- if an element's column is out of range for the tangent space basis matrices, that must mean it is totally unconstrained, and so we can actually deform it freely, and so just switch to simpler deformation code for that case.
Another possibility is to initially use column index -1 for a new element, and not worry about any -1s in the problem posing because the code is structured such that they never occur when a problem is being posed; we only need to worry about the -1s when deforming, and handle the case there.
Another possibility (if I understand Rust and the Option type properly) is to simply use unwrap in these posers. It nominally could panic, but we know it won't because problems are only posed just after every element has been assigned an index. This route seems the least satisfying because we still have all of the baggage of Option even though the other possibilities make it clear that Option isn't filling a particularly critical need for the column indices of elements. But this route also might be the path of least resistance, so I'm OK if that's the way you want to go here.
Or there might be some other mechanism whereby the data structure passed to a problem poser is something of a slightly different type that definitely has a column for each element. (Perhaps this mild redundant-checking issue is even evidence that elements shouldn't store their column indices; conceptually, a column index is more of a property of an Element as a part of an Assembly, rather than intrinsically of the Element itself. So maybe instead the Assembly has some record of the column index for each Element, where the indices are plain ol' integers, and that record is provided to problem posers. But I am definitely not mandating a refactor of that scope as part of this PR.)
What doesn't seem good is writing out checks and manual panic messages in three places, for panics that can literally never occur because of the structure of the code. That redundant checking is what I mean by "evidence of misfactoring".
I think there are two problems here—a narrow one that falls within the scope of this pull request, and a broader one that deserves its own pull request.
Narrow: semantics issue
The conditional panic doesn't do a great job of communicating that we expect every element to have a column index when
pose
is called. Unwrapping theOption
, as you point out, has the semantics we want, and that's what we do on the main branch (commitb86f176
). However, unwrapping doesn't provide an informative panic message, which would make it harder to debug an invariant violation.I've switched to the
expect
method, which has the same semantics, and allows us to provide our own panic message (commit5eeb093
). I've also rewritten the panic messages in the style recommended by theexpect
documentation.Broad: factoring issue
I agree that we should eventually rewrite the column index system to avoid these checks entirely. I think that deserves its own pull request, since this pull request didn't create and doesn't modify the column index system.