forked from StudioInfinity/dyna3
feat: Curvature regulators (#80)
Prior to this commit, there's only one kind of regulator: the one that regulates the inversive distance between two spheres (or, more generally, the Lorentz product between two element representation vectors). Adds a new kind of regulator, which regulates the curvature of a sphere (issue #55). In the process, introduces a general framework based on new traits for organizing and sharing code between different kinds of regulators. Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: StudioInfinity/dyna3#80 Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
This commit is contained in:
parent
23ba5acad7
commit
360ce12d8b
6 changed files with 640 additions and 331 deletions
|
@ -1,12 +1,21 @@
|
|||
use nalgebra::{DMatrix, DVector, DVectorView, Vector3};
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
|
||||
use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}};
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||
|
||||
use crate::{
|
||||
engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix},
|
||||
engine::{
|
||||
Q,
|
||||
change_half_curvature,
|
||||
local_unif_to_std,
|
||||
realize_gram,
|
||||
sphere,
|
||||
ConfigSubspace,
|
||||
ConstraintProblem
|
||||
},
|
||||
outline::OutlineItem,
|
||||
specified::SpecifiedValue
|
||||
};
|
||||
|
||||
|
@ -23,6 +32,10 @@ pub type ElementColor = [f32; 3];
|
|||
// each assembly has a key that identifies it within the sesssion
|
||||
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub trait ProblemPoser {
|
||||
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Element {
|
||||
pub id: String,
|
||||
|
@ -30,8 +43,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
|
||||
|
@ -45,6 +58,8 @@ pub struct Element {
|
|||
}
|
||||
|
||||
impl Element {
|
||||
const CURVATURE_COMPONENT: usize = 3;
|
||||
|
||||
pub fn new(
|
||||
id: String,
|
||||
label: String,
|
||||
|
@ -117,13 +132,148 @@ impl Element {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Regulator {
|
||||
pub subjects: (ElementKey, ElementKey),
|
||||
impl ProblemPoser for Element {
|
||||
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Element>) {
|
||||
let index = self.column_index.expect(
|
||||
format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||
);
|
||||
problem.gram.push_sym(index, index, 1.0);
|
||||
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Regulator: ProblemPoser + OutlineItem {
|
||||
fn subjects(&self) -> Vec<ElementKey>;
|
||||
fn measurement(&self) -> ReadSignal<f64>;
|
||||
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, _assembly: &Assembly) -> bool {
|
||||
self.set_point().with(|set_pt| set_pt.is_present())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InversiveDistanceRegulator {
|
||||
pub subjects: [ElementKey; 2],
|
||||
pub measurement: ReadSignal<f64>,
|
||||
pub set_point: Signal<SpecifiedValue>
|
||||
}
|
||||
|
||||
impl InversiveDistanceRegulator {
|
||||
pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator {
|
||||
let measurement = assembly.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());
|
||||
|
||||
InversiveDistanceRegulator { subjects, measurement, set_point }
|
||||
}
|
||||
}
|
||||
|
||||
impl Regulator for InversiveDistanceRegulator {
|
||||
fn subjects(&self) -> Vec<ElementKey> {
|
||||
self.subjects.into()
|
||||
}
|
||||
|
||||
fn measurement(&self) -> ReadSignal<f64> {
|
||||
self.measurement
|
||||
}
|
||||
|
||||
fn set_point(&self) -> Signal<SpecifiedValue> {
|
||||
self.set_point
|
||||
}
|
||||
}
|
||||
|
||||
impl ProblemPoser for InversiveDistanceRegulator {
|
||||
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
|
||||
self.set_point.with_untracked(|set_pt| {
|
||||
if let Some(val) = set_pt.value {
|
||||
let [row, col] = self.subjects.map(
|
||||
|subj| elts[subj].column_index.expect(
|
||||
"Subjects should be indexed before inversive distance regulator writes problem data"
|
||||
)
|
||||
);
|
||||
problem.gram.push_sym(row, col, val);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HalfCurvatureRegulator {
|
||||
pub subject: ElementKey,
|
||||
pub measurement: ReadSignal<f64>,
|
||||
pub set_point: Signal<SpecifiedValue>
|
||||
}
|
||||
|
||||
impl HalfCurvatureRegulator {
|
||||
pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator {
|
||||
let measurement = assembly.elements.map(
|
||||
move |elts| elts[subject].representation.with(
|
||||
|rep| rep[Element::CURVATURE_COMPONENT]
|
||||
)
|
||||
);
|
||||
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
|
||||
HalfCurvatureRegulator { subject, measurement, set_point }
|
||||
}
|
||||
}
|
||||
|
||||
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 try_activate(&self, assembly: &Assembly) -> bool {
|
||||
match self.set_point.with(|set_pt| set_pt.value) {
|
||||
Some(half_curv) => {
|
||||
let representation = assembly.elements.with_untracked(
|
||||
|elts| elts[self.subject].representation
|
||||
);
|
||||
representation.update(
|
||||
|rep| change_half_curvature(rep, half_curv)
|
||||
);
|
||||
true
|
||||
}
|
||||
None => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProblemPoser for HalfCurvatureRegulator {
|
||||
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
|
||||
self.set_point.with_untracked(|set_pt| {
|
||||
if let Some(val) = set_pt.value {
|
||||
let col = elts[self.subject].column_index.expect(
|
||||
"Subject should be indexed before half-curvature regulator writes problem data"
|
||||
);
|
||||
problem.frozen.push(Element::CURVATURE_COMPONENT, col, val);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// the velocity is expressed in uniform coordinates
|
||||
pub struct ElementMotion<'a> {
|
||||
pub key: ElementKey,
|
||||
|
@ -137,7 +287,7 @@ type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
|
|||
pub struct Assembly {
|
||||
// elements and regulators
|
||||
pub elements: Signal<Slab<Element>>,
|
||||
pub regulators: Signal<Slab<Regulator>>,
|
||||
pub regulators: Signal<Slab<Rc<dyn Regulator>>>,
|
||||
|
||||
// solution variety tangent space. the basis vectors are stored in
|
||||
// configuration matrix format, ordered according to the elements' column
|
||||
|
@ -167,26 +317,33 @@ impl Assembly {
|
|||
|
||||
// --- 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
|
||||
// same identifier will get kicked out of the `elements_by_id` index
|
||||
fn insert_element_unchecked(&self, elt: Element) {
|
||||
fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey {
|
||||
// insert the sphere
|
||||
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));
|
||||
|
||||
// regulate the sphere's curvature
|
||||
self.insert_regulator(HalfCurvatureRegulator::new(key, &self));
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn try_insert_element(&self, elt: Element) -> bool {
|
||||
pub fn try_insert_sphere(&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_sphere_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);
|
||||
|
@ -197,70 +354,69 @@ impl Assembly {
|
|||
id = format!("sphere{}", id_num);
|
||||
}
|
||||
|
||||
// create and insert a new element
|
||||
self.insert_element_unchecked(
|
||||
// create and insert a sphere
|
||||
let _ = self.insert_sphere_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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_regulator(&self, regulator: Regulator) {
|
||||
let subjects = regulator.subjects;
|
||||
let key = self.regulators.update(|regs| regs.insert(regulator));
|
||||
let subject_regulators = self.elements.with(
|
||||
|elts| (elts[subjects.0].regulators, elts[subjects.1].regulators)
|
||||
pub fn insert_regulator<T: Regulator + 'static>(&self, regulator: T) {
|
||||
// add the regulator to the assembly's regulator list
|
||||
let regulator_rc = Rc::new(regulator);
|
||||
let key = self.regulators.update(
|
||||
|regs| regs.insert(regulator_rc.clone())
|
||||
);
|
||||
subject_regulators.0.update(|regs| regs.insert(key));
|
||||
subject_regulators.1.update(|regs| regs.insert(key));
|
||||
}
|
||||
|
||||
pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) {
|
||||
// create and insert a new regulator
|
||||
let measurement = self.elements.map(
|
||||
move |elts| {
|
||||
let reps = (
|
||||
elts[subjects.0].representation.get_clone(),
|
||||
elts[subjects.1].representation.get_clone()
|
||||
);
|
||||
reps.0.dot(&(&*Q * reps.1))
|
||||
|
||||
// add the regulator to each subject's regulator list
|
||||
let subjects = regulator_rc.subjects();
|
||||
let subject_regulators: Vec<_> = self.elements.with_untracked(
|
||||
|elts| subjects.into_iter().map(
|
||||
|subj| elts[subj].regulators
|
||||
).collect()
|
||||
);
|
||||
for regulators in subject_regulators {
|
||||
regulators.update(|regs| regs.insert(key));
|
||||
}
|
||||
|
||||
// update the 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_1(&JsValue::from(
|
||||
format!("Updated regulator with subjects {:?}", regulator_rc.subjects())
|
||||
));
|
||||
|
||||
if regulator_rc.try_activate(&self_for_effect) {
|
||||
self_for_effect.realize();
|
||||
}
|
||||
);
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
self.insert_regulator(Regulator {
|
||||
subjects: subjects,
|
||||
measurement: measurement,
|
||||
set_point: set_point
|
||||
});
|
||||
|
||||
/* DEBUG */
|
||||
// print an updated list of regulators
|
||||
console::log_1(&JsValue::from("Regulators:"));
|
||||
self.regulators.with(|regs| {
|
||||
self.regulators.with_untracked(|regs| {
|
||||
for (_, reg) in regs.into_iter() {
|
||||
console::log_5(
|
||||
&JsValue::from(" "),
|
||||
&JsValue::from(reg.subjects.0),
|
||||
&JsValue::from(reg.subjects.1),
|
||||
&JsValue::from(":"),
|
||||
®.set_point.with_untracked(
|
||||
|set_pt| JsValue::from(set_pt.spec.as_str())
|
||||
console::log_1(&JsValue::from(format!(
|
||||
" {:?}: {}",
|
||||
reg.subjects(),
|
||||
reg.set_point().with_untracked(
|
||||
|set_pt| {
|
||||
let spec = &set_pt.spec;
|
||||
if spec.is_empty() {
|
||||
"__".to_string()
|
||||
} else {
|
||||
spec.clone()
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// update the realization when the regulator becomes a constraint, or is
|
||||
// edited while acting as a constraint
|
||||
create_effect(move || {
|
||||
console::log_1(&JsValue::from(
|
||||
format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1)
|
||||
));
|
||||
if set_point.with(|set_pt| set_pt.is_present()) {
|
||||
self.realize();
|
||||
)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -275,55 +431,39 @@ impl Assembly {
|
|||
}
|
||||
});
|
||||
|
||||
// set up the Gram matrix and the initial configuration matrix
|
||||
let (gram, guess) = self.elements.with_untracked(|elts| {
|
||||
// set up the off-diagonal part of the Gram matrix
|
||||
let mut gram_to_be = PartialMatrix::new();
|
||||
// set up the constraint problem
|
||||
let problem = self.elements.with_untracked(|elts| {
|
||||
let mut problem = ConstraintProblem::new(elts.len());
|
||||
for (_, elt) in elts {
|
||||
elt.pose(&mut problem, elts);
|
||||
}
|
||||
self.regulators.with_untracked(|regs| {
|
||||
for (_, reg) in regs {
|
||||
reg.set_point.with_untracked(|set_pt| {
|
||||
if let Some(val) = set_pt.value {
|
||||
let subjects = reg.subjects;
|
||||
let row = elts[subjects.0].column_index.unwrap();
|
||||
let col = elts[subjects.1].column_index.unwrap();
|
||||
gram_to_be.push_sym(row, col, val);
|
||||
}
|
||||
});
|
||||
reg.pose(&mut problem, elts);
|
||||
}
|
||||
});
|
||||
|
||||
// set up the initial configuration matrix and the diagonal of the
|
||||
// Gram matrix
|
||||
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
|
||||
for (_, elt) in elts {
|
||||
let index = elt.column_index.unwrap();
|
||||
gram_to_be.push_sym(index, index, 1.0);
|
||||
guess_to_be.set_column(index, &elt.representation.get_clone_untracked());
|
||||
}
|
||||
|
||||
(gram_to_be, guess_to_be)
|
||||
problem
|
||||
});
|
||||
|
||||
/* DEBUG */
|
||||
// log the Gram matrix
|
||||
console::log_1(&JsValue::from("Gram matrix:"));
|
||||
gram.log_to_console();
|
||||
problem.gram.log_to_console();
|
||||
|
||||
/* DEBUG */
|
||||
// log the initial configuration matrix
|
||||
console::log_1(&JsValue::from("Old configuration:"));
|
||||
for j in 0..guess.nrows() {
|
||||
for j in 0..problem.guess.nrows() {
|
||||
let mut row_str = String::new();
|
||||
for k in 0..guess.ncols() {
|
||||
row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str());
|
||||
for k in 0..problem.guess.ncols() {
|
||||
row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str());
|
||||
}
|
||||
console::log_1(&JsValue::from(row_str));
|
||||
}
|
||||
|
||||
// look for a configuration with the given Gram matrix
|
||||
let (config, tangent, success, history) = realize_gram(
|
||||
&gram, guess, &[],
|
||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||
);
|
||||
|
||||
/* DEBUG */
|
||||
|
@ -458,4 +598,48 @@ impl Assembly {
|
|||
// sync
|
||||
self.realize();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")]
|
||||
fn unindexed_element_test() {
|
||||
let _ = create_root(|| {
|
||||
Element::new(
|
||||
"sphere".to_string(),
|
||||
"Sphere".to_string(),
|
||||
[1.0_f32, 1.0_f32, 1.0_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||
).pose(&mut ConstraintProblem::new(1), &Slab::new());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
|
||||
fn unindexed_subject_test_inversive_distance() {
|
||||
let _ = create_root(|| {
|
||||
let mut elts = Slab::new();
|
||||
let subjects = [0, 1].map(|k| {
|
||||
elts.insert(
|
||||
Element::new(
|
||||
format!("sphere{k}"),
|
||||
format!("Sphere {k}"),
|
||||
[1.0_f32, 1.0_f32, 1.0_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||
)
|
||||
)
|
||||
});
|
||||
elts[subjects[0]].column_index = Some(0);
|
||||
InversiveDistanceRegulator {
|
||||
subjects: subjects,
|
||||
measurement: create_memo(|| 0.0),
|
||||
set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap())
|
||||
}.pose(&mut ConstraintProblem::new(2), &elts);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue