Generalize constraints to observables #48

Merged
glen merged 25 commits from Vectornaut/dyna3:observables_on_main into main 2025-03-10 23:43:25 +00:00
4 changed files with 87 additions and 87 deletions
Showing only changes of commit 24139ad5e9 - Show all commits

View file

@ -78,12 +78,12 @@ summary.selected {
background-color: var(--selection-highlight); background-color: var(--selection-highlight);
} }
summary > div, .observable { summary > div, .regulator {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
} }
.element, .observable { .element, .regulator {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
padding-left: 8px; padding-left: 8px;
@ -108,7 +108,7 @@ details[open]:has(li) .element-switch::after {
flex-grow: 1; flex-grow: 1;
} }
.observable-label { .regulator-label {
flex-grow: 1; flex-grow: 1;
} }
@ -124,37 +124,37 @@ details[open]:has(li) .element-switch::after {
width: 56px; width: 56px;
} }
.observable { .regulator {
font-style: italic; font-style: italic;
} }
.observable-type { .regulator-type {
padding: 2px 8px 0px 8px; padding: 2px 8px 0px 8px;
font-size: 10pt; font-size: 10pt;
} }
.observable.invalid-constraint { .regulator.invalid-constraint {
color: var(--text-invalid); color: var(--text-invalid);
} }
.observable > input { .regulator > input {
color: inherit; color: inherit;
background-color: inherit; background-color: inherit;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 2px; border-radius: 2px;
} }
.observable > input::placeholder { .regulator > input::placeholder {
color: inherit; color: inherit;
opacity: 54%; opacity: 54%;
font-style: italic; font-style: italic;
} }
.observable.valid-constraint > input { .regulator.valid-constraint > input {
background-color: var(--display-background); background-color: var(--display-background);
} }
.observable.invalid-constraint > input { .regulator.invalid-constraint > input {
border-color: var(--border-invalid); border-color: var(--border-invalid);
} }

View file

@ -6,8 +6,8 @@ use crate::{
AppState, AppState,
assembly::{ assembly::{
Assembly, Assembly,
glen marked this conversation as resolved
Review

In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line, like

    assembly::{Assembly, Element}

whereas here this has been spread across three lines. They should be consistent in format. If it's the same to you, I prefer a formats that balances the desire for fewer linebreaks (to keep a good amount of information visible on screen at one time) with the need for clear organization and readability (which too few linebreaks can engender). That is, I prefer the assembly.rs format.

In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line, like ``` assembly::{Assembly, Element} ``` whereas here this has been spread across three lines. They should be consistent in format. If it's the same to you, I prefer a formats that balances the desire for fewer linebreaks (to keep a good amount of information visible on screen at one time) with the need for clear organization and readability (which too few linebreaks can engender). That is, I prefer the assembly.rs format.
Review

In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line […], whereas here this has been spread across three lines. They should be consistent in format.

Good catch. My convention has been to start with each use declaration on one line. If that line gets too long, I switch the outermost braced list from space-separated to newline-separated. Then I do the same thing recursively at lower list levels. The use declarations you noticed break this convention; I've corrected them In commit b9db7a5. I think the convention is followed everywhere else, but I'll keep an eye on it whenever I revise use declarations in future commits.

> In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line […], whereas here this has been spread across three lines. They should be consistent in format. Good catch. My convention has been to start with each `use` declaration on one line. If that line gets too long, I switch the outermost braced list from space-separated to newline-separated. Then I do the same thing recursively at lower list levels. The `use` declarations you noticed break this convention; I've corrected them In commit b9db7a5. I think the convention is followed everywhere else, but I'll keep an eye on it whenever I revise `use` declarations in future commits.
Observable, Regulator,
ObservableRole, RegulatorRole,
Element Element
}, },
engine::Q engine::Q
@ -210,8 +210,8 @@ pub fn AddRemove() -> View {
} }
); );
let desired = create_signal(0.0); let desired = create_signal(0.0);
let role = create_signal(ObservableRole::Measurement); let role = create_signal(RegulatorRole::Measurement);
state.assembly.insert_observable(Observable { state.assembly.insert_regulator(Regulator {
subjects: subjects, subjects: subjects,
measured: measured, measured: measured,
desired: desired, desired: desired,
@ -221,22 +221,22 @@ pub fn AddRemove() -> View {
state.selection.update(|sel| sel.clear()); state.selection.update(|sel| sel.clear());
/* DEBUG */ /* DEBUG */
// print updated observable list // print updated regulator list
console::log_1(&JsValue::from("Observables:")); console::log_1(&JsValue::from("Regulators:"));
state.assembly.observables.with(|obsls| { state.assembly.regulators.with(|regs| {
for (_, obs) in obsls.into_iter() { for (_, reg) in regs.into_iter() {
console::log_5( console::log_5(
&JsValue::from(" "), &JsValue::from(" "),
&JsValue::from(obs.subjects.0), &JsValue::from(reg.subjects.0),
&JsValue::from(obs.subjects.1), &JsValue::from(reg.subjects.1),
&JsValue::from(":"), &JsValue::from(":"),
&JsValue::from(obs.desired.get_untracked()) &JsValue::from(reg.desired.get_untracked())
); );
} }
}); });
// update the realization when the observable becomes // update the realization when the regulator becomes
// constrained, or is edited while constrained // a constraint, or is edited while acting as a constraint
create_effect(move || { create_effect(move || {
console::log_1(&JsValue::from( console::log_1(&JsValue::from(
format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1)

View file

@ -7,9 +7,9 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix};
// the types of the keys we use to access an assembly's elements and observables // the types of the keys we use to access an assembly's elements and regulators
pub type ElementKey = usize; pub type ElementKey = usize;
pub type ObservableKey = usize; pub type RegulatorKey = usize;
pub type ElementColor = [f32; 3]; pub type ElementColor = [f32; 3];
@ -26,7 +26,7 @@ pub struct Element {
pub label: String, pub label: String,
pub color: ElementColor, pub color: ElementColor,
pub representation: Signal<DVector<f64>>, pub representation: Signal<DVector<f64>>,
pub observables: Signal<BTreeSet<ObservableKey>>, pub regulators: Signal<BTreeSet<RegulatorKey>>,
// a serial number, assigned by `Element::new`, that uniquely identifies // a serial number, assigned by `Element::new`, that uniquely identifies
// each element // each element
@ -61,7 +61,7 @@ impl Element {
label: label, label: label,
color: color, color: color,
representation: create_signal(representation), representation: create_signal(representation),
observables: create_signal(BTreeSet::default()), regulators: create_signal(BTreeSet::default()),
serial: serial, serial: serial,
column_index: None column_index: None
} }
@ -111,30 +111,30 @@ impl Element {
} }
} }
pub enum ObservableRole { pub enum RegulatorRole {
Measurement, Measurement,
Constraint(bool) Constraint(bool)
} }
impl ObservableRole { impl RegulatorRole {
pub fn is_valid_constraint(&self) -> bool { pub fn is_valid_constraint(&self) -> bool {
match self { match self {
ObservableRole::Measurement => false, RegulatorRole::Measurement => false,
ObservableRole::Constraint(valid) => *valid RegulatorRole::Constraint(valid) => *valid
} }
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Observable { pub struct Regulator {
pub subjects: (ElementKey, ElementKey), pub subjects: (ElementKey, ElementKey),
pub measured: ReadSignal<f64>, pub measured: ReadSignal<f64>,
pub desired: Signal<f64>, pub desired: Signal<f64>,
pub desired_text: Signal<String>, pub desired_text: Signal<String>,
pub role: Signal<ObservableRole> pub role: Signal<RegulatorRole>
} }
impl Observable { impl Regulator {
fn role_is_valid_constraint_untracked(&self) -> bool { fn role_is_valid_constraint_untracked(&self) -> bool {
self.role.with_untracked(|role| role.is_valid_constraint()) self.role.with_untracked(|role| role.is_valid_constraint())
} }
@ -151,9 +151,9 @@ type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
// a complete, view-independent description of an assembly // a complete, view-independent description of an assembly
#[derive(Clone)] #[derive(Clone)]
pub struct Assembly { pub struct Assembly {
// elements and observables // elements and regulators
pub elements: Signal<Slab<Element>>, pub elements: Signal<Slab<Element>>,
pub observables: Signal<Slab<Observable>>, pub regulators: Signal<Slab<Regulator>>,
// solution variety tangent space. the basis vectors are stored in // solution variety tangent space. the basis vectors are stored in
// configuration matrix format, ordered according to the elements' column // configuration matrix format, ordered according to the elements' column
@ -175,13 +175,13 @@ impl Assembly {
pub fn new() -> Assembly { pub fn new() -> Assembly {
Assembly { Assembly {
elements: create_signal(Slab::new()), elements: create_signal(Slab::new()),
observables: create_signal(Slab::new()), regulators: create_signal(Slab::new()),
tangent: create_signal(ConfigSubspace::zero(0)), tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(FxHashMap::default()) elements_by_id: create_signal(FxHashMap::default())
} }
} }
// --- inserting elements and observables --- // --- inserting elements and regulators ---
// insert an element into the assembly without checking whether we already // insert an element 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
@ -224,14 +224,14 @@ impl Assembly {
); );
} }
pub fn insert_observable(&self, observable: Observable) { pub fn insert_regulator(&self, regulator: Regulator) {
let subjects = observable.subjects; let subjects = regulator.subjects;
let key = self.observables.update(|obsls| obsls.insert(observable)); let key = self.regulators.update(|regs| regs.insert(regulator));
let subject_observables = self.elements.with( let subject_regulators = self.elements.with(
|elts| (elts[subjects.0].observables, elts[subjects.1].observables) |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators)
); );
subject_observables.0.update(|obsls| obsls.insert(key)); subject_regulators.0.update(|regs| regs.insert(key));
subject_observables.1.update(|obsls| obsls.insert(key)); subject_regulators.1.update(|regs| regs.insert(key));
} }
// --- realization --- // --- realization ---
@ -248,13 +248,13 @@ impl Assembly {
let (gram, guess) = self.elements.with_untracked(|elts| { let (gram, guess) = self.elements.with_untracked(|elts| {
// set up the off-diagonal part of the Gram matrix // set up the off-diagonal part of the Gram matrix
let mut gram_to_be = PartialMatrix::new(); let mut gram_to_be = PartialMatrix::new();
self.observables.with_untracked(|obsls| { self.regulators.with_untracked(|regs| {
for (_, obs) in obsls { for (_, reg) in regs {
if obs.role_is_valid_constraint_untracked() { if reg.role_is_valid_constraint_untracked() {
let subjects = obs.subjects; let subjects = reg.subjects;
let row = elts[subjects.0].column_index.unwrap(); let row = elts[subjects.0].column_index.unwrap();
let col = elts[subjects.1].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, obs.desired.get_untracked()); gram_to_be.push_sym(row, col, reg.desired.get_untracked());
} }
} }
}); });

View file

@ -12,33 +12,33 @@ use crate::{
AppState, AppState,
assembly, assembly,
assembly::{ assembly::{
Observable, Regulator,
ObservableKey, RegulatorKey,
ObservableRole::*, RegulatorRole::*,
ElementKey ElementKey
} }
}; };
// an editable view of the Lorentz product representing an observable // an editable view of a regulator
#[component(inline_props)] #[component(inline_props)]
fn ObservableInput(observable: Observable) -> View { fn RegulatorInput(regulator: Regulator) -> View {
view! { view! {
input( input(
r#type="text", r#type="text",
placeholder=observable.measured.with(|result| result.to_string()), placeholder=regulator.measured.with(|result| result.to_string()),
bind:value=observable.desired_text, bind:value=regulator.desired_text,
on:change=move |event: Event| { on:change=move |event: Event| {
let target: HtmlInputElement = event.target().unwrap().unchecked_into(); let target: HtmlInputElement = event.target().unwrap().unchecked_into();
let value = target.value(); let value = target.value();
if value.is_empty() { if value.is_empty() {
observable.role.set(Measurement); regulator.role.set(Measurement);
} else { } else {
match target.value().parse::<f64>() { match target.value().parse::<f64>() {
Ok(desired) => batch(|| { Ok(desired) => batch(|| {
observable.desired.set(desired); regulator.desired.set(desired);
observable.role.set(Constraint(true)); regulator.role.set(Constraint(true));
}), }),
Err(_) => observable.role.set(Constraint(false)) Err(_) => regulator.role.set(Constraint(false))
glen marked this conversation as resolved
Review

Please could you explain to me the three moves in the three closures in this view! macro, the one that specifies the class of the input element, the one that specifies its change handler, and the one that specifies its keydown handler?

If I am understanding Rust correctly, the move annotation specifies that the closure should take ownership of any free variables that appear therein. So that would mean that the function that computes the class of the input takes ownership of the regulator. Why is that desired? Is it because the input element may not actually be realized until after the RegulatorInput function returns, and the ownership transfer extends the lifetime of the regulator until the closure has a chance to run and grab the info it needs to set the class (or perhaps actually, it has to persist that regulator indefinitely and then it can react to all future changes to the regulator)? The class-calculating closure of the input element seems like a slightly odd place for the long-term ownership of the regulator to reside, but maybe it doesn't really matter what owns it, as long as it persists indefinitely?

But it seems my suggestions in the previous paragraph can't possibly be correct, because regulator also occurs free in the closure passed as on:change, but the ownership of one entity can't exist in two places, if I am understanding correctly. So I guess please just explain to me what's going on here.

And finally, the third move seems the most confusing. For the life of me, I can't see any free variables in the closure passed as on:keydown, so there doesn't seem to be anything to move, and so the move shouldn't be there.

Thanks in advance for illuminating these things for me.

Please could you explain to me the three `move`s in the three closures in this `view!` macro, the one that specifies the class of the input element, the one that specifies its change handler, and the one that specifies its keydown handler? If I am understanding Rust correctly, the `move` annotation specifies that the closure should take ownership of any free variables that appear therein. So that would mean that the function that computes the class of the input takes ownership of the regulator. Why is that desired? Is it because the input element may not actually be realized until after the RegulatorInput function returns, and the ownership transfer extends the lifetime of the regulator until the closure has a chance to run and grab the info it needs to set the class (or perhaps actually, it has to persist that regulator indefinitely and then it can react to all future changes to the regulator)? The class-calculating closure of the input element seems like a slightly odd place for the long-term ownership of the regulator to reside, but maybe it doesn't really matter what owns it, as long as it persists indefinitely? But it seems my suggestions in the previous paragraph can't possibly be correct, because `regulator` also occurs free in the closure passed as `on:change`, but the ownership of one entity can't exist in two places, if I am understanding correctly. So I guess please just explain to me what's going on here. And finally, the third `move` seems the most confusing. For the life of me, I can't see _any_ free variables in the closure passed as `on:keydown`, so there doesn't seem to be anything _to_ move, and so the `move` shouldn't be there. Thanks in advance for illuminating these things for me.
Review

Replying to your latest post here, to keep it in this resolvable conversation. Ah, the fact that Signal has Copy semantics makes this code much clearer. I didn't know that, and it seems a weird quirk of Rust to me that with ownership such a crucial concept to Rust, it is not clear whether copy or ownership transfer is in effect and I don't see any way you can know without consulting the source/docs for Signal.

Anyhow, I now get the first two moves. And now I see that reset_value is just a local closure, and that's what's being moved by the third move (for the keydown event). So that's good. Resolving.

Replying to your latest post here, to keep it in this resolvable conversation. Ah, the fact that Signal has Copy semantics makes this code much clearer. I didn't know that, and it seems a weird quirk of Rust to me that with ownership such a crucial concept to Rust, it is not clear whether copy or ownership transfer is in effect and I don't see any way you can know without consulting the source/docs for Signal. Anyhow, I now get the first two `move`s. And now I see that `reset_value` is just a local closure, and that's what's being `move`d by the third `move` (for the keydown event). So that's good. Resolving.
}; };
} }
} }
@ -46,30 +46,30 @@ fn ObservableInput(observable: Observable) -> View {
} }
} }
// a list item that shows an observable in an outline view of an element // a list item that shows a regulator in an outline view of an element
#[component(inline_props)] #[component(inline_props)]
fn ObservableOutlineItem(observable_key: ObservableKey, element_key: ElementKey) -> View { fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let assembly = &state.assembly; let assembly = &state.assembly;
let observable = assembly.observables.with(|obsls| obsls[observable_key].clone()); let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone());
let other_subject = if observable.subjects.0 == element_key { let other_subject = if regulator.subjects.0 == element_key {
observable.subjects.1 regulator.subjects.1
} else { } else {
observable.subjects.0 regulator.subjects.0
}; };
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
let class = observable.role.map( let class = regulator.role.map(
|role| match role { |role| match role {
Measurement => "observable", Measurement => "regulator",
Constraint(true) => "observable valid-constraint", Constraint(true) => "regulator valid-constraint",
Constraint(false) => "observable invalid-constraint" Constraint(false) => "regulator invalid-constraint"
} }
); );
view! { view! {
li(class=class.get()) { li(class=class.get()) {
div(class="observable-label") { (other_subject_label) } div(class="regulator-label") { (other_subject_label) }
div(class="observable-type") { "Inversive distance" } div(class="regulator-type") { "Inversive distance" }
ObservableInput(observable=observable) RegulatorInput(regulator=regulator)
div(class="status") div(class="status")
} }
} }
@ -93,9 +93,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
).collect::<Vec<_>>() ).collect::<Vec<_>>()
) )
}; };
let observed = element.observables.map(|obsls| obsls.len() > 0); let regulated = element.regulators.map(|regs| regs.len() > 0);
let observable_list = element.observables.map( let regulator_list = element.regulators.map(
|obsls| obsls.clone().into_iter().collect() |regs| regs.clone().into_iter().collect()
); );
let details_node = create_node_ref(); let details_node = create_node_ref();
view! { view! {
@ -110,7 +110,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
state.select(key, event.shift_key()); state.select(key, event.shift_key());
event.prevent_default(); event.prevent_default();
}, },
"ArrowRight" if observed.get() => { "ArrowRight" if regulated.get() => {
let _ = details_node let _ = details_node
.get() .get()
.unchecked_into::<web_sys::Element>() .unchecked_into::<web_sys::Element>()
@ -157,16 +157,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
div(class="status") div(class="status")
} }
} }
ul(class="observables") { ul(class="regulators") {
Keyed( Keyed(
list=observable_list, list=regulator_list,
view=move |obs_key| view! { view=move |reg_key| view! {
ObservableOutlineItem( RegulatorOutlineItem(
observable_key=obs_key, regulator_key=reg_key,
element_key=key element_key=key
) )
}, },
key=|obs_key| obs_key.clone() key=|reg_key| reg_key.clone()
) )
} }
} }
@ -174,9 +174,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
} }
} }
// a component that lists the elements of the current assembly, showing the // a component that lists the elements of the current assembly, showing each
// observables associated with each element as a collapsible sub-list. its // element's regulators in a collapsible sub-list. its implementation is based
// implementation is based on Kate Morley's HTML + CSS tree views: // on Kate Morley's HTML + CSS tree views:
// //
// https://iamkate.com/code/tree-views/ // https://iamkate.com/code/tree-views/
// //