233 lines
8.1 KiB
Rust
233 lines
8.1 KiB
Rust
use nalgebra::{DMatrix, DVector};
|
|
use rustc_hash::FxHashMap;
|
|
use slab::Slab;
|
|
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
|
|
use sycamore::prelude::*;
|
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
|
|
|
use crate::engine::{realize_gram, PartialMatrix};
|
|
|
|
// the types of the keys we use to access an assembly's elements and constraints
|
|
pub type ElementKey = usize;
|
|
pub type ConstraintKey = usize;
|
|
|
|
pub type ElementColor = [f32; 3];
|
|
|
|
/* KLUDGE */
|
|
// we should reconsider this design when we build a system for switching between
|
|
// assemblies. at that point, we might want to switch to hierarchical keys,
|
|
// where each each element has a key that identifies it within its assembly and
|
|
// each assembly has a key that identifies it within the sesssion
|
|
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub struct Element {
|
|
pub id: String,
|
|
pub label: String,
|
|
pub color: ElementColor,
|
|
pub representation: Signal<DVector<f64>>,
|
|
pub constraints: Signal<BTreeSet<ConstraintKey>>,
|
|
|
|
// a serial number, assigned by `Element::new`, that uniquely identifies
|
|
// each element (until `NEXT_ELEMENT_SERIAL` wraps around)
|
|
pub serial: u64,
|
|
|
|
// the configuration matrix column index that was assigned to this element
|
|
// last time the assembly was realized
|
|
column_index: usize
|
|
}
|
|
|
|
impl Element {
|
|
pub fn new(
|
|
id: String,
|
|
label: String,
|
|
color: ElementColor,
|
|
representation: DVector<f64>
|
|
) -> Element {
|
|
// take the next serial number, panicking if that was the last number we
|
|
// had left. the technique we use to panic on overflow is taken from
|
|
// _Rust Atomics and Locks_, by Mara Bos
|
|
//
|
|
// https://marabos.nl/atomics/atomics.html#example-handle-overflow
|
|
//
|
|
let serial = NEXT_ELEMENT_SERIAL.fetch_update(
|
|
Ordering::SeqCst, Ordering::SeqCst,
|
|
|serial| serial.checked_add(1)
|
|
).expect("Out of serial numbers for elements");
|
|
|
|
Element {
|
|
id: id,
|
|
label: label,
|
|
color: color,
|
|
representation: create_signal(representation),
|
|
constraints: create_signal(BTreeSet::default()),
|
|
serial: serial,
|
|
column_index: 0
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Clone)]
|
|
pub struct Constraint {
|
|
pub subjects: (ElementKey, ElementKey),
|
|
pub lorentz_prod: Signal<f64>,
|
|
pub lorentz_prod_text: Signal<String>,
|
|
pub lorentz_prod_valid: Signal<bool>,
|
|
pub active: Signal<bool>
|
|
}
|
|
|
|
// a complete, view-independent description of an assembly
|
|
#[derive(Clone)]
|
|
pub struct Assembly {
|
|
// elements and constraints
|
|
pub elements: Signal<Slab<Element>>,
|
|
pub constraints: Signal<Slab<Constraint>>,
|
|
|
|
// indexing
|
|
pub elements_by_id: Signal<FxHashMap<String, ElementKey>>
|
|
}
|
|
|
|
impl Assembly {
|
|
pub fn new() -> Assembly {
|
|
Assembly {
|
|
elements: create_signal(Slab::new()),
|
|
constraints: create_signal(Slab::new()),
|
|
elements_by_id: create_signal(FxHashMap::default())
|
|
}
|
|
}
|
|
|
|
// --- inserting elements and constraints ---
|
|
|
|
// 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) {
|
|
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));
|
|
}
|
|
|
|
pub fn try_insert_element(&self, elt: Element) -> bool {
|
|
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);
|
|
}
|
|
can_insert
|
|
}
|
|
|
|
pub fn insert_new_element(&self) {
|
|
// find the next unused identifier in the default sequence
|
|
let mut id_num = 1;
|
|
let mut id = format!("sphere{}", id_num);
|
|
while self.elements_by_id.with_untracked(
|
|
|elts_by_id| elts_by_id.contains_key(&id)
|
|
) {
|
|
id_num += 1;
|
|
id = format!("sphere{}", id_num);
|
|
}
|
|
|
|
// create and insert a new element
|
|
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])
|
|
)
|
|
);
|
|
}
|
|
|
|
pub fn insert_constraint(&self, constraint: Constraint) {
|
|
let subjects = constraint.subjects;
|
|
let key = self.constraints.update(|csts| csts.insert(constraint));
|
|
let subject_constraints = self.elements.with(
|
|
|elts| (elts[subjects.0].constraints, elts[subjects.1].constraints)
|
|
);
|
|
subject_constraints.0.update(|csts| csts.insert(key));
|
|
subject_constraints.1.update(|csts| csts.insert(key));
|
|
}
|
|
|
|
// --- realization ---
|
|
|
|
pub fn realize(&self) {
|
|
// index the elements
|
|
self.elements.update_silent(|elts| {
|
|
for (index, (_, elt)) in elts.into_iter().enumerate() {
|
|
elt.column_index = index;
|
|
}
|
|
});
|
|
|
|
// 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();
|
|
self.constraints.with_untracked(|csts| {
|
|
for (_, cst) in csts {
|
|
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() {
|
|
let subjects = cst.subjects;
|
|
let row = elts[subjects.0].column_index;
|
|
let col = elts[subjects.1].column_index;
|
|
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked());
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
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)
|
|
});
|
|
|
|
/* DEBUG */
|
|
// log the Gram matrix
|
|
console::log_1(&JsValue::from("Gram matrix:"));
|
|
gram.log_to_console();
|
|
|
|
/* DEBUG */
|
|
// log the initial configuration matrix
|
|
console::log_1(&JsValue::from("Old configuration:"));
|
|
for j in 0..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());
|
|
}
|
|
console::log_1(&JsValue::from(row_str));
|
|
}
|
|
|
|
// look for a configuration with the given Gram matrix
|
|
let (config, success, history) = realize_gram(
|
|
&gram, guess, &[],
|
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
|
);
|
|
|
|
/* DEBUG */
|
|
// report the outcome of the search
|
|
console::log_1(&JsValue::from(
|
|
if success {
|
|
"Target accuracy achieved!"
|
|
} else {
|
|
"Failed to reach target accuracy"
|
|
}
|
|
));
|
|
console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1));
|
|
console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap()));
|
|
|
|
if success {
|
|
// read out the solution
|
|
for (_, elt) in self.elements.get_clone_untracked() {
|
|
elt.representation.update(
|
|
|rep| rep.set_column(0, &config.column(elt.column_index))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} |