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>, pub constraints: Signal>, // 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 ) -> 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, pub lorentz_prod_text: Signal, pub lorentz_prod_valid: Signal, pub active: Signal } // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { // elements and constraints pub elements: Signal>, pub constraints: Signal>, // indexing pub elements_by_id: Signal> } 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::::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::::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)) ); } } } }