forked from glen/dyna3
185 lines
No EOL
6.1 KiB
Rust
185 lines
No EOL
6.1 KiB
Rust
use nalgebra::{DMatrix, DVector};
|
|
use rustc_hash::FxHashMap;
|
|
use slab::Slab;
|
|
use std::collections::BTreeSet;
|
|
use sycamore::prelude::*;
|
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
|
|
|
use crate::engine::{realize_gram, PartialMatrix};
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub struct Element {
|
|
pub id: String,
|
|
pub label: String,
|
|
pub color: [f32; 3],
|
|
pub rep: DVector<f64>,
|
|
pub constraints: BTreeSet<usize>,
|
|
|
|
// internal properties, not reflected in any view
|
|
pub index: usize
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Constraint {
|
|
pub args: (usize, usize),
|
|
pub rep: Signal<f64>,
|
|
pub rep_text: Signal<String>,
|
|
pub rep_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, usize>>
|
|
}
|
|
|
|
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 {
|
|
id: id,
|
|
label: format!("Sphere {}", id_num),
|
|
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
|
rep: DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]),
|
|
constraints: BTreeSet::default(),
|
|
index: 0
|
|
}
|
|
);
|
|
}
|
|
|
|
pub fn insert_constraint(&self, constraint: Constraint) {
|
|
let args = constraint.args;
|
|
let key = self.constraints.update(|csts| csts.insert(constraint));
|
|
self.elements.update(|elts| {
|
|
elts[args.0].constraints.insert(key);
|
|
elts[args.1].constraints.insert(key);
|
|
});
|
|
}
|
|
|
|
// --- realization ---
|
|
|
|
pub fn realize(&self) {
|
|
// index the elements
|
|
self.elements.update_silent(|elts| {
|
|
for (index, (_, elt)) in elts.into_iter().enumerate() {
|
|
elt.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.rep_valid.get_untracked() {
|
|
let args = cst.args;
|
|
let row = elts[args.0].index;
|
|
let col = elts[args.1].index;
|
|
gram_to_be.push_sym(row, col, cst.rep.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.index;
|
|
gram_to_be.push_sym(index, index, 1.0);
|
|
guess_to_be.set_column(index, &elt.rep);
|
|
}
|
|
|
|
(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
|
|
self.elements.update(|elts| {
|
|
for (_, elt) in elts.iter_mut() {
|
|
elt.rep.set_column(0, &config.column(elt.index));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} |