Compare commits
5 Commits
engine-pro
...
main
Author | SHA1 | Date | |
---|---|---|---|
e917272c60 | |||
65cee1ecc2 | |||
707618cdd3 | |||
86fa682b31 | |||
b92be312e8 |
4
app-proto/.gitignore
vendored
Normal file
4
app-proto/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
target
|
||||||
|
dist
|
||||||
|
profiling
|
||||||
|
Cargo.lock
|
44
app-proto/Cargo.toml
Normal file
44
app-proto/Cargo.toml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
[package]
|
||||||
|
name = "dyna3"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Aaron Fenyes", "Glen Whitney"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
itertools = "0.13.0"
|
||||||
|
js-sys = "0.3.70"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
nalgebra = "0.33.0"
|
||||||
|
rustc-hash = "2.0.0"
|
||||||
|
slab = "0.4.9"
|
||||||
|
sycamore = "0.9.0-beta.3"
|
||||||
|
|
||||||
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
|
# logging them with `console.error`. This is great for development, but requires
|
||||||
|
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||||
|
# code size when deploying.
|
||||||
|
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3.69"
|
||||||
|
features = [
|
||||||
|
'HtmlCanvasElement',
|
||||||
|
'HtmlInputElement',
|
||||||
|
'Performance',
|
||||||
|
'WebGl2RenderingContext',
|
||||||
|
'WebGlBuffer',
|
||||||
|
'WebGlProgram',
|
||||||
|
'WebGlShader',
|
||||||
|
'WebGlUniformLocation',
|
||||||
|
'WebGlVertexArrayObject'
|
||||||
|
]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen-test = "0.3.34"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s" # optimize for small code size
|
||||||
|
debug = true # include debug symbols
|
11
app-proto/index.html
Normal file
11
app-proto/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>dyna3</title>
|
||||||
|
<link data-trunk rel="css" href="main.css"/>
|
||||||
|
<link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
175
app-proto/main.css
Normal file
175
app-proto/main.css
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
:root {
|
||||||
|
--text: #fcfcfc; /* almost white */
|
||||||
|
--text-bright: white;
|
||||||
|
--text-invalid: #f58fc2; /* bright pink */
|
||||||
|
--border: #555; /* light gray */
|
||||||
|
--border-focus: #aaa; /* bright gray */
|
||||||
|
--border-invalid: #70495c; /* dusky pink */
|
||||||
|
--selection-highlight: #444; /* medium gray */
|
||||||
|
--page-background: #222; /* dark gray */
|
||||||
|
--display-background: #020202; /* almost black */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--page-background);
|
||||||
|
font-family: 'Fira Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sidebar */
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
float: left;
|
||||||
|
width: 450px;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
border-width: 0px 1px 0px 0px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* add-remove */
|
||||||
|
|
||||||
|
#add-remove {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-remove > button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KLUDGE */
|
||||||
|
/*
|
||||||
|
for convenience, we're using emoji as temporary icons for some buttons. these
|
||||||
|
buttons need to be displayed in an emoji font
|
||||||
|
*/
|
||||||
|
#add-remove > button.emoji {
|
||||||
|
font-family: 'Noto Emoji', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* outline */
|
||||||
|
|
||||||
|
#outline {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.selected {
|
||||||
|
color: var(--text-bright);
|
||||||
|
background-color: var(--selection-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary > div, .constraint {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element, .constraint {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-switch {
|
||||||
|
width: 18px;
|
||||||
|
padding-left: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
details:has(li) .element-switch::after {
|
||||||
|
content: '▸';
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open]:has(li) .element-switch::after {
|
||||||
|
content: '▾';
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-label {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-label {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-representation {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-representation > div {
|
||||||
|
padding: 2px 0px 0px 0px;
|
||||||
|
font-size: 10pt;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint.invalid {
|
||||||
|
color: var(--text-invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint > input[type=checkbox] {
|
||||||
|
margin: 0px 8px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint > input[type=text] {
|
||||||
|
color: inherit;
|
||||||
|
background-color: inherit;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint.invalid > input[type=text] {
|
||||||
|
border-color: var(--border-invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
width: 20px;
|
||||||
|
padding-left: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Noto Emoji';
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid > .status::after, details:has(.invalid):not([open]) .status::after {
|
||||||
|
content: '⚠';
|
||||||
|
color: var(--text-invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* display */
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
float: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: var(--display-background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas:focus {
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
}
|
8
app-proto/run-examples
Executable file
8
app-proto/run-examples
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
# based on "Enabling print statements in Cargo tests", by Jon Almeida
|
||||||
|
#
|
||||||
|
# https://jonalmeida.com/posts/2015/01/23/print-cargo/
|
||||||
|
#
|
||||||
|
|
||||||
|
cargo test -- --nocapture engine::tests::irisawa_hexlet_test
|
||||||
|
cargo test -- --nocapture engine::tests::three_spheres_example
|
||||||
|
cargo test -- --nocapture engine::tests::point_on_sphere_example
|
240
app-proto/src/add_remove.rs
Normal file
240
app-proto/src/add_remove.rs
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
use sycamore::prelude::*;
|
||||||
|
use web_sys::{console, wasm_bindgen::JsValue};
|
||||||
|
|
||||||
|
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// load an example assembly for testing. this code will be removed once we've
|
||||||
|
// built a more formal test assembly system
|
||||||
|
fn load_gen_assemb(assembly: &Assembly) {
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("gemini_a"),
|
||||||
|
String::from("Castor"),
|
||||||
|
[1.00_f32, 0.25_f32, 0.00_f32],
|
||||||
|
engine::sphere(0.5, 0.5, 0.0, 1.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("gemini_b"),
|
||||||
|
String::from("Pollux"),
|
||||||
|
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||||
|
engine::sphere(-0.5, -0.5, 0.0, 1.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("ursa_major"),
|
||||||
|
String::from("Ursa major"),
|
||||||
|
[0.25_f32, 0.00_f32, 1.00_f32],
|
||||||
|
engine::sphere(-0.5, 0.5, 0.0, 0.75)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("ursa_minor"),
|
||||||
|
String::from("Ursa minor"),
|
||||||
|
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||||
|
engine::sphere(0.5, -0.5, 0.0, 0.5)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("moon_deimos"),
|
||||||
|
String::from("Deimos"),
|
||||||
|
[0.75_f32, 0.75_f32, 0.00_f32],
|
||||||
|
engine::sphere(0.0, 0.15, 1.0, 0.25)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("moon_phobos"),
|
||||||
|
String::from("Phobos"),
|
||||||
|
[0.00_f32, 0.75_f32, 0.50_f32],
|
||||||
|
engine::sphere(0.0, -0.15, -1.0, 0.25)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// load an example assembly for testing. this code will be removed once we've
|
||||||
|
// built a more formal test assembly system
|
||||||
|
fn load_low_curv_assemb(assembly: &Assembly) {
|
||||||
|
let a = 0.75_f64.sqrt();
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"central".to_string(),
|
||||||
|
"Central".to_string(),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"assemb_plane".to_string(),
|
||||||
|
"Assembly plane".to_string(),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"side1".to_string(),
|
||||||
|
"Side 1".to_string(),
|
||||||
|
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||||
|
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"side2".to_string(),
|
||||||
|
"Side 2".to_string(),
|
||||||
|
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||||
|
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"side3".to_string(),
|
||||||
|
"Side 3".to_string(),
|
||||||
|
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||||
|
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"corner1".to_string(),
|
||||||
|
"Corner 1".to_string(),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
"corner2".to_string(),
|
||||||
|
"Corner 2".to_string(),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element::new(
|
||||||
|
String::from("corner3"),
|
||||||
|
String::from("Corner 3"),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AddRemove() -> View {
|
||||||
|
/* DEBUG */
|
||||||
|
let assembly_name = create_signal("general".to_string());
|
||||||
|
create_effect(move || {
|
||||||
|
// get name of chosen assembly
|
||||||
|
let name = assembly_name.get_clone();
|
||||||
|
console::log_1(
|
||||||
|
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
|
||||||
|
);
|
||||||
|
|
||||||
|
batch(|| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let assembly = &state.assembly;
|
||||||
|
|
||||||
|
// clear state
|
||||||
|
assembly.elements.update(|elts| elts.clear());
|
||||||
|
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
|
||||||
|
state.selection.update(|sel| sel.clear());
|
||||||
|
|
||||||
|
// load assembly
|
||||||
|
match name.as_str() {
|
||||||
|
"general" => load_gen_assemb(assembly),
|
||||||
|
"low-curv" => load_low_curv_assemb(assembly),
|
||||||
|
_ => ()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
div(id="add-remove") {
|
||||||
|
button(
|
||||||
|
on:click=|_| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
state.assembly.insert_new_element();
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// print updated list of elements by identifier
|
||||||
|
console::log_1(&JsValue::from("elements by identifier:"));
|
||||||
|
for (id, key) in state.assembly.elements_by_id.get_clone().iter() {
|
||||||
|
console::log_3(
|
||||||
|
&JsValue::from(" "),
|
||||||
|
&JsValue::from(id),
|
||||||
|
&JsValue::from(*key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { "+" }
|
||||||
|
button(
|
||||||
|
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
|
||||||
|
disabled={
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
state.selection.with(|sel| sel.len() != 2)
|
||||||
|
},
|
||||||
|
on:click=|_| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let subjects = state.selection.with(
|
||||||
|
|sel| {
|
||||||
|
let subject_vec: Vec<_> = sel.into_iter().collect();
|
||||||
|
(subject_vec[0].clone(), subject_vec[1].clone())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let lorentz_prod = create_signal(0.0);
|
||||||
|
let lorentz_prod_valid = create_signal(false);
|
||||||
|
let active = create_signal(true);
|
||||||
|
state.assembly.insert_constraint(Constraint {
|
||||||
|
subjects: subjects,
|
||||||
|
lorentz_prod: lorentz_prod,
|
||||||
|
lorentz_prod_text: create_signal(String::new()),
|
||||||
|
lorentz_prod_valid: lorentz_prod_valid,
|
||||||
|
active: active,
|
||||||
|
});
|
||||||
|
state.selection.update(|sel| sel.clear());
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// print updated constraint list
|
||||||
|
console::log_1(&JsValue::from("Constraints:"));
|
||||||
|
state.assembly.constraints.with(|csts| {
|
||||||
|
for (_, cst) in csts.into_iter() {
|
||||||
|
console::log_5(
|
||||||
|
&JsValue::from(" "),
|
||||||
|
&JsValue::from(cst.subjects.0),
|
||||||
|
&JsValue::from(cst.subjects.1),
|
||||||
|
&JsValue::from(":"),
|
||||||
|
&JsValue::from(cst.lorentz_prod.get_untracked())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the realization when the constraint becomes active
|
||||||
|
// and valid, or is edited while active and valid
|
||||||
|
create_effect(move || {
|
||||||
|
console::log_1(&JsValue::from(
|
||||||
|
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
|
||||||
|
));
|
||||||
|
lorentz_prod.track();
|
||||||
|
if active.get() && lorentz_prod_valid.get() {
|
||||||
|
state.assembly.realize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
) { "🔗" }
|
||||||
|
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
|
||||||
|
option(value="general") { "General" }
|
||||||
|
option(value="low-curv") { "Low-curvature" }
|
||||||
|
option(value="empty") { "Empty" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
233
app-proto/src/assembly.rs
Normal file
233
app-proto/src/assembly.rs
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
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
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
464
app-proto/src/display.rs
Normal file
464
app-proto/src/display.rs
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
use core::array;
|
||||||
|
use nalgebra::{DMatrix, Rotation3, Vector3};
|
||||||
|
use sycamore::{prelude::*, motion::create_raf};
|
||||||
|
use web_sys::{
|
||||||
|
console,
|
||||||
|
window,
|
||||||
|
KeyboardEvent,
|
||||||
|
WebGl2RenderingContext,
|
||||||
|
WebGlProgram,
|
||||||
|
WebGlShader,
|
||||||
|
WebGlUniformLocation,
|
||||||
|
wasm_bindgen::{JsCast, JsValue}
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
fn compile_shader(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
shader_type: u32,
|
||||||
|
source: &str,
|
||||||
|
) -> WebGlShader {
|
||||||
|
let shader = context.create_shader(shader_type).unwrap();
|
||||||
|
context.shader_source(&shader, source);
|
||||||
|
context.compile_shader(&shader);
|
||||||
|
shader
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_uniform_array_locations<const N: usize>(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
program: &WebGlProgram,
|
||||||
|
var_name: &str,
|
||||||
|
member_name_opt: Option<&str>
|
||||||
|
) -> [Option<WebGlUniformLocation>; N] {
|
||||||
|
array::from_fn(|n| {
|
||||||
|
let name = match member_name_opt {
|
||||||
|
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
|
||||||
|
None => format!("{var_name}[{n}]")
|
||||||
|
};
|
||||||
|
context.get_uniform_location(&program, name.as_str())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the given data into the vertex input of the given name
|
||||||
|
fn bind_vertex_attrib(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
index: u32,
|
||||||
|
size: i32,
|
||||||
|
data: &[f32]
|
||||||
|
) {
|
||||||
|
// create a data buffer and bind it to ARRAY_BUFFER
|
||||||
|
let buffer = context.create_buffer().unwrap();
|
||||||
|
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer));
|
||||||
|
|
||||||
|
// load the given data into the buffer. the function `Float32Array::view`
|
||||||
|
// creates a raw view into our module's `WebAssembly.Memory` buffer.
|
||||||
|
// allocating more memory will change the buffer, invalidating the view.
|
||||||
|
// that means we have to make sure we don't allocate any memory until the
|
||||||
|
// view is dropped
|
||||||
|
unsafe {
|
||||||
|
context.buffer_data_with_array_buffer_view(
|
||||||
|
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||||
|
&js_sys::Float32Array::view(&data),
|
||||||
|
WebGl2RenderingContext::STATIC_DRAW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow the target attribute to be used
|
||||||
|
context.enable_vertex_attrib_array(index);
|
||||||
|
|
||||||
|
// take whatever's bound to ARRAY_BUFFER---here, the data buffer created
|
||||||
|
// above---and bind it to the target attribute
|
||||||
|
//
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
|
||||||
|
//
|
||||||
|
context.vertex_attrib_pointer_with_i32(
|
||||||
|
index,
|
||||||
|
size,
|
||||||
|
WebGl2RenderingContext::FLOAT,
|
||||||
|
false, // don't normalize
|
||||||
|
0, // zero stride
|
||||||
|
0, // zero offset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Display() -> View {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
|
||||||
|
// canvas
|
||||||
|
let display = create_node_ref();
|
||||||
|
|
||||||
|
// navigation
|
||||||
|
let pitch_up = create_signal(0.0);
|
||||||
|
let pitch_down = create_signal(0.0);
|
||||||
|
let yaw_right = create_signal(0.0);
|
||||||
|
let yaw_left = create_signal(0.0);
|
||||||
|
let roll_ccw = create_signal(0.0);
|
||||||
|
let roll_cw = create_signal(0.0);
|
||||||
|
let zoom_in = create_signal(0.0);
|
||||||
|
let zoom_out = create_signal(0.0);
|
||||||
|
let turntable = create_signal(false); /* BENCHMARKING */
|
||||||
|
|
||||||
|
// change listener
|
||||||
|
let scene_changed = create_signal(true);
|
||||||
|
create_effect(move || {
|
||||||
|
state.assembly.elements.with(|elts| {
|
||||||
|
for (_, elt) in elts {
|
||||||
|
elt.representation.track();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state.selection.track();
|
||||||
|
scene_changed.set(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* INSTRUMENTS */
|
||||||
|
const SAMPLE_PERIOD: i32 = 60;
|
||||||
|
let mut last_sample_time = 0.0;
|
||||||
|
let mut frames_since_last_sample = 0;
|
||||||
|
let mean_frame_interval = create_signal(0.0);
|
||||||
|
|
||||||
|
on_mount(move || {
|
||||||
|
// timing
|
||||||
|
let mut last_time = 0.0;
|
||||||
|
|
||||||
|
// viewpoint
|
||||||
|
const ROT_SPEED: f64 = 0.4; // in radians per second
|
||||||
|
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
||||||
|
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
|
||||||
|
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
||||||
|
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||||
|
let mut location_z: f64 = 5.0;
|
||||||
|
|
||||||
|
// display parameters
|
||||||
|
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
||||||
|
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
||||||
|
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||||
|
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||||
|
|
||||||
|
/* INSTRUMENTS */
|
||||||
|
let performance = window().unwrap().performance().unwrap();
|
||||||
|
|
||||||
|
// get the display canvas
|
||||||
|
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||||
|
let ctx = canvas
|
||||||
|
.get_context("webgl2")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<WebGl2RenderingContext>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// compile and attach the vertex and fragment shaders
|
||||||
|
let vertex_shader = compile_shader(
|
||||||
|
&ctx,
|
||||||
|
WebGl2RenderingContext::VERTEX_SHADER,
|
||||||
|
include_str!("identity.vert"),
|
||||||
|
);
|
||||||
|
let fragment_shader = compile_shader(
|
||||||
|
&ctx,
|
||||||
|
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||||
|
include_str!("inversive.frag"),
|
||||||
|
);
|
||||||
|
let program = ctx.create_program().unwrap();
|
||||||
|
ctx.attach_shader(&program, &vertex_shader);
|
||||||
|
ctx.attach_shader(&program, &fragment_shader);
|
||||||
|
ctx.link_program(&program);
|
||||||
|
let link_status = ctx
|
||||||
|
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||||
|
.as_bool()
|
||||||
|
.unwrap();
|
||||||
|
let link_msg = if link_status {
|
||||||
|
"Linked successfully"
|
||||||
|
} else {
|
||||||
|
"Linking failed"
|
||||||
|
};
|
||||||
|
console::log_1(&JsValue::from(link_msg));
|
||||||
|
ctx.use_program(Some(&program));
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// print the maximum number of vectors that can be passed as
|
||||||
|
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
||||||
|
// requires this maximum to be at least 224, as discussed in the
|
||||||
|
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
|
||||||
|
// here:
|
||||||
|
//
|
||||||
|
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
|
||||||
|
//
|
||||||
|
// there are also other size limits. for example, on Aaron's
|
||||||
|
// machine, the the length of a float or genType array seems to be
|
||||||
|
// capped at 1024 elements
|
||||||
|
console::log_2(
|
||||||
|
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
||||||
|
&JsValue::from("uniform vectors available")
|
||||||
|
);
|
||||||
|
|
||||||
|
// find indices of vertex attributes and uniforms
|
||||||
|
const SPHERE_MAX: usize = 200;
|
||||||
|
let position_index = ctx.get_attrib_location(&program, "position") as u32;
|
||||||
|
let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt");
|
||||||
|
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "sphere_list", Some("sp")
|
||||||
|
);
|
||||||
|
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "sphere_list", Some("lt")
|
||||||
|
);
|
||||||
|
let color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "color_list", None
|
||||||
|
);
|
||||||
|
let highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "highlight_list", None
|
||||||
|
);
|
||||||
|
let resolution_loc = ctx.get_uniform_location(&program, "resolution");
|
||||||
|
let shortdim_loc = ctx.get_uniform_location(&program, "shortdim");
|
||||||
|
let opacity_loc = ctx.get_uniform_location(&program, "opacity");
|
||||||
|
let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold");
|
||||||
|
let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode");
|
||||||
|
|
||||||
|
// create a vertex array and bind it to the graphics context
|
||||||
|
let vertex_array = ctx.create_vertex_array().unwrap();
|
||||||
|
ctx.bind_vertex_array(Some(&vertex_array));
|
||||||
|
|
||||||
|
// set the vertex positions
|
||||||
|
const VERTEX_CNT: usize = 6;
|
||||||
|
let positions: [f32; 3*VERTEX_CNT] = [
|
||||||
|
// northwest triangle
|
||||||
|
-1.0, -1.0, 0.0,
|
||||||
|
-1.0, 1.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
// southeast triangle
|
||||||
|
-1.0, -1.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
1.0, -1.0, 0.0
|
||||||
|
];
|
||||||
|
bind_vertex_attrib(&ctx, position_index, 3, &positions);
|
||||||
|
|
||||||
|
// set up a repainting routine
|
||||||
|
let (_, start_animation_loop, _) = create_raf(move || {
|
||||||
|
// get the time step
|
||||||
|
let time = performance.now();
|
||||||
|
let time_step = 0.001*(time - last_time);
|
||||||
|
last_time = time;
|
||||||
|
|
||||||
|
// get the navigation state
|
||||||
|
let pitch_up_val = pitch_up.get();
|
||||||
|
let pitch_down_val = pitch_down.get();
|
||||||
|
let yaw_right_val = yaw_right.get();
|
||||||
|
let yaw_left_val = yaw_left.get();
|
||||||
|
let roll_ccw_val = roll_ccw.get();
|
||||||
|
let roll_cw_val = roll_cw.get();
|
||||||
|
let zoom_in_val = zoom_in.get();
|
||||||
|
let zoom_out_val = zoom_out.get();
|
||||||
|
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||||
|
|
||||||
|
// update the assembly's orientation
|
||||||
|
let ang_vel = {
|
||||||
|
let pitch = pitch_up_val - pitch_down_val;
|
||||||
|
let yaw = yaw_right_val - yaw_left_val;
|
||||||
|
let roll = roll_ccw_val - roll_cw_val;
|
||||||
|
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
|
||||||
|
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
|
||||||
|
} else {
|
||||||
|
Vector3::zeros()
|
||||||
|
}
|
||||||
|
} /* BENCHMARKING */ + if turntable_val {
|
||||||
|
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
|
||||||
|
} else {
|
||||||
|
Vector3::zeros()
|
||||||
|
};
|
||||||
|
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
|
||||||
|
rotation_sp.copy_from(
|
||||||
|
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
||||||
|
);
|
||||||
|
orientation = &rotation * &orientation;
|
||||||
|
|
||||||
|
// update the assembly's location
|
||||||
|
let zoom = zoom_out_val - zoom_in_val;
|
||||||
|
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||||
|
|
||||||
|
if scene_changed.get() {
|
||||||
|
/* INSTRUMENTS */
|
||||||
|
// measure mean frame interval
|
||||||
|
frames_since_last_sample += 1;
|
||||||
|
if frames_since_last_sample >= SAMPLE_PERIOD {
|
||||||
|
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
|
||||||
|
last_sample_time = time;
|
||||||
|
frames_since_last_sample = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the map from assembly space to world space
|
||||||
|
let location = {
|
||||||
|
let u = -location_z;
|
||||||
|
DMatrix::from_column_slice(5, 5, &[
|
||||||
|
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0, 0.0, u,
|
||||||
|
0.0, 0.0, 2.0*u, 1.0, u*u,
|
||||||
|
0.0, 0.0, 0.0, 0.0, 1.0
|
||||||
|
])
|
||||||
|
};
|
||||||
|
let assembly_to_world = &location * &orientation;
|
||||||
|
|
||||||
|
// get the assembly
|
||||||
|
let (
|
||||||
|
elt_cnt,
|
||||||
|
reps_world,
|
||||||
|
colors,
|
||||||
|
highlights
|
||||||
|
) = state.assembly.elements.with(|elts| {
|
||||||
|
(
|
||||||
|
// number of elements
|
||||||
|
elts.len() as i32,
|
||||||
|
|
||||||
|
// representation vectors in world coordinates
|
||||||
|
elts.iter().map(
|
||||||
|
|(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep)
|
||||||
|
).collect::<Vec<_>>(),
|
||||||
|
|
||||||
|
// colors
|
||||||
|
elts.iter().map(|(key, elt)| {
|
||||||
|
if state.selection.with(|sel| sel.contains(&key)) {
|
||||||
|
elt.color.map(|ch| 0.2 + 0.8*ch)
|
||||||
|
} else {
|
||||||
|
elt.color
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>(),
|
||||||
|
|
||||||
|
// highlight levels
|
||||||
|
elts.iter().map(|(key, _)| {
|
||||||
|
if state.selection.with(|sel| sel.contains(&key)) {
|
||||||
|
1.0_f32
|
||||||
|
} else {
|
||||||
|
HIGHLIGHT
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// set the resolution
|
||||||
|
let width = canvas.width() as f32;
|
||||||
|
let height = canvas.height() as f32;
|
||||||
|
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
||||||
|
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
||||||
|
|
||||||
|
// pass the assembly
|
||||||
|
ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt);
|
||||||
|
for n in 0..reps_world.len() {
|
||||||
|
let v = &reps_world[n];
|
||||||
|
ctx.uniform3f(
|
||||||
|
sphere_sp_locs[n].as_ref(),
|
||||||
|
v[0] as f32, v[1] as f32, v[2] as f32
|
||||||
|
);
|
||||||
|
ctx.uniform2f(
|
||||||
|
sphere_lt_locs[n].as_ref(),
|
||||||
|
v[3] as f32, v[4] as f32
|
||||||
|
);
|
||||||
|
ctx.uniform3fv_with_f32_array(
|
||||||
|
color_locs[n].as_ref(),
|
||||||
|
&colors[n]
|
||||||
|
);
|
||||||
|
ctx.uniform1f(
|
||||||
|
highlight_locs[n].as_ref(),
|
||||||
|
highlights[n]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass the display parameters
|
||||||
|
ctx.uniform1f(opacity_loc.as_ref(), OPACITY);
|
||||||
|
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||||
|
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||||
|
|
||||||
|
// draw the scene
|
||||||
|
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||||
|
|
||||||
|
// clear the scene change flag
|
||||||
|
scene_changed.set(
|
||||||
|
pitch_up_val != 0.0
|
||||||
|
|| pitch_down_val != 0.0
|
||||||
|
|| yaw_left_val != 0.0
|
||||||
|
|| yaw_right_val != 0.0
|
||||||
|
|| roll_cw_val != 0.0
|
||||||
|
|| roll_ccw_val != 0.0
|
||||||
|
|| zoom_in_val != 0.0
|
||||||
|
|| zoom_out_val != 0.0
|
||||||
|
|| turntable_val /* BENCHMARKING */
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
frames_since_last_sample = 0;
|
||||||
|
mean_frame_interval.set(-1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
start_animation_loop();
|
||||||
|
});
|
||||||
|
|
||||||
|
let set_nav_signal = move |event: KeyboardEvent, value: f64| {
|
||||||
|
let mut navigating = true;
|
||||||
|
let shift = event.shift_key();
|
||||||
|
match event.key().as_str() {
|
||||||
|
"ArrowUp" if shift => zoom_in.set(value),
|
||||||
|
"ArrowDown" if shift => zoom_out.set(value),
|
||||||
|
"ArrowUp" => pitch_up.set(value),
|
||||||
|
"ArrowDown" => pitch_down.set(value),
|
||||||
|
"ArrowRight" if shift => roll_cw.set(value),
|
||||||
|
"ArrowLeft" if shift => roll_ccw.set(value),
|
||||||
|
"ArrowRight" => yaw_right.set(value),
|
||||||
|
"ArrowLeft" => yaw_left.set(value),
|
||||||
|
_ => navigating = false
|
||||||
|
};
|
||||||
|
if navigating {
|
||||||
|
scene_changed.set(true);
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
/* TO DO */
|
||||||
|
// switch back to integer-valued parameters when that becomes possible
|
||||||
|
// again
|
||||||
|
canvas(
|
||||||
|
ref=display,
|
||||||
|
width="600",
|
||||||
|
height="600",
|
||||||
|
tabindex="0",
|
||||||
|
on:keydown=move |event: KeyboardEvent| {
|
||||||
|
if event.key() == "Shift" {
|
||||||
|
roll_cw.set(yaw_right.get());
|
||||||
|
roll_ccw.set(yaw_left.get());
|
||||||
|
zoom_in.set(pitch_up.get());
|
||||||
|
zoom_out.set(pitch_down.get());
|
||||||
|
yaw_right.set(0.0);
|
||||||
|
yaw_left.set(0.0);
|
||||||
|
pitch_up.set(0.0);
|
||||||
|
pitch_down.set(0.0);
|
||||||
|
} else {
|
||||||
|
if event.key() == "Enter" { /* BENCHMARKING */
|
||||||
|
turntable.set_fn(|turn| !turn);
|
||||||
|
scene_changed.set(true);
|
||||||
|
}
|
||||||
|
set_nav_signal(event, 1.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on:keyup=move |event: KeyboardEvent| {
|
||||||
|
if event.key() == "Shift" {
|
||||||
|
yaw_right.set(roll_cw.get());
|
||||||
|
yaw_left.set(roll_ccw.get());
|
||||||
|
pitch_up.set(zoom_in.get());
|
||||||
|
pitch_down.set(zoom_out.get());
|
||||||
|
roll_cw.set(0.0);
|
||||||
|
roll_ccw.set(0.0);
|
||||||
|
zoom_in.set(0.0);
|
||||||
|
zoom_out.set(0.0);
|
||||||
|
} else {
|
||||||
|
set_nav_signal(event, 0.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on:blur=move |_| {
|
||||||
|
pitch_up.set(0.0);
|
||||||
|
pitch_down.set(0.0);
|
||||||
|
yaw_right.set(0.0);
|
||||||
|
yaw_left.set(0.0);
|
||||||
|
roll_ccw.set(0.0);
|
||||||
|
roll_cw.set(0.0);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
540
app-proto/src/engine.rs
Normal file
540
app-proto/src/engine.rs
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use nalgebra::{Const, DMatrix, DVector, Dyn};
|
||||||
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||||
|
|
||||||
|
// --- elements ---
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn point(x: f64, y: f64, z: f64) -> DVector<f64> {
|
||||||
|
DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// the sphere with the given center and radius, with inward-pointing normals
|
||||||
|
pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector<f64> {
|
||||||
|
let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z;
|
||||||
|
DVector::from_column_slice(&[
|
||||||
|
center_x / radius,
|
||||||
|
center_y / radius,
|
||||||
|
center_z / radius,
|
||||||
|
0.5 / radius,
|
||||||
|
0.5 * (center_norm_sq / radius - radius)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// the sphere of curvature `curv` whose closest point to the origin has position
|
||||||
|
// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the
|
||||||
|
// curvature to zero gives a plane
|
||||||
|
pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector<f64> {
|
||||||
|
let norm_sp = 1.0 + off * curv;
|
||||||
|
DVector::from_column_slice(&[
|
||||||
|
norm_sp * dir_x,
|
||||||
|
norm_sp * dir_y,
|
||||||
|
norm_sp * dir_z,
|
||||||
|
0.5 * curv,
|
||||||
|
off * (1.0 + 0.5 * off * curv)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- partial matrices ---
|
||||||
|
|
||||||
|
struct MatrixEntry {
|
||||||
|
index: (usize, usize),
|
||||||
|
value: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PartialMatrix(Vec<MatrixEntry>);
|
||||||
|
|
||||||
|
impl PartialMatrix {
|
||||||
|
pub fn new() -> PartialMatrix {
|
||||||
|
PartialMatrix(Vec::<MatrixEntry>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
|
||||||
|
let PartialMatrix(entries) = self;
|
||||||
|
entries.push(MatrixEntry { index: (row, col), value: value });
|
||||||
|
if row != col {
|
||||||
|
entries.push(MatrixEntry { index: (col, row), value: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
pub fn log_to_console(&self) {
|
||||||
|
let PartialMatrix(entries) = self;
|
||||||
|
for ent in entries {
|
||||||
|
let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value);
|
||||||
|
console::log_1(&JsValue::from(ent_str.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
|
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
|
||||||
|
let PartialMatrix(entries) = self;
|
||||||
|
for ent in entries {
|
||||||
|
result[ent.index] = a[ent.index];
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
|
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
|
||||||
|
let PartialMatrix(entries) = self;
|
||||||
|
for ent in entries {
|
||||||
|
result[ent.index] = ent.value - rhs[ent.index];
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- descent history ---
|
||||||
|
|
||||||
|
pub struct DescentHistory {
|
||||||
|
pub config: Vec<DMatrix<f64>>,
|
||||||
|
pub scaled_loss: Vec<f64>,
|
||||||
|
pub neg_grad: Vec<DMatrix<f64>>,
|
||||||
|
pub min_eigval: Vec<f64>,
|
||||||
|
pub base_step: Vec<DMatrix<f64>>,
|
||||||
|
pub backoff_steps: Vec<i32>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DescentHistory {
|
||||||
|
fn new() -> DescentHistory {
|
||||||
|
DescentHistory {
|
||||||
|
config: Vec::<DMatrix<f64>>::new(),
|
||||||
|
scaled_loss: Vec::<f64>::new(),
|
||||||
|
neg_grad: Vec::<DMatrix<f64>>::new(),
|
||||||
|
min_eigval: Vec::<f64>::new(),
|
||||||
|
base_step: Vec::<DMatrix<f64>>::new(),
|
||||||
|
backoff_steps: Vec::<i32>::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- gram matrix realization ---
|
||||||
|
|
||||||
|
// the Lorentz form
|
||||||
|
lazy_static! {
|
||||||
|
static ref Q: DMatrix<f64> = DMatrix::from_row_slice(5, 5, &[
|
||||||
|
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0, 0.0, -2.0,
|
||||||
|
0.0, 0.0, 0.0, -2.0, 0.0
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchState {
|
||||||
|
config: DMatrix<f64>,
|
||||||
|
err_proj: DMatrix<f64>,
|
||||||
|
loss: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchState {
|
||||||
|
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
|
||||||
|
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
|
||||||
|
let loss = err_proj.norm_squared();
|
||||||
|
SearchState {
|
||||||
|
config: config,
|
||||||
|
err_proj: err_proj,
|
||||||
|
loss: loss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix<f64> {
|
||||||
|
let mut result = DMatrix::<f64>::zeros(nrows, ncols);
|
||||||
|
result[index] = 1.0;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// use backtracking line search to find a better configuration
|
||||||
|
fn seek_better_config(
|
||||||
|
gram: &PartialMatrix,
|
||||||
|
state: &SearchState,
|
||||||
|
base_step: &DMatrix<f64>,
|
||||||
|
base_target_improvement: f64,
|
||||||
|
min_efficiency: f64,
|
||||||
|
backoff: f64,
|
||||||
|
max_backoff_steps: i32
|
||||||
|
) -> Option<(SearchState, i32)> {
|
||||||
|
let mut rate = 1.0;
|
||||||
|
for backoff_steps in 0..max_backoff_steps {
|
||||||
|
let trial_config = &state.config + rate * base_step;
|
||||||
|
let trial_state = SearchState::from_config(gram, trial_config);
|
||||||
|
let improvement = state.loss - trial_state.loss;
|
||||||
|
if improvement >= min_efficiency * rate * base_target_improvement {
|
||||||
|
return Some((trial_state, backoff_steps));
|
||||||
|
}
|
||||||
|
rate *= backoff;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// seek a matrix `config` for which `config' * Q * config` matches the partial
|
||||||
|
// matrix `gram`. use gradient descent starting from `guess`
|
||||||
|
pub fn realize_gram(
|
||||||
|
gram: &PartialMatrix,
|
||||||
|
guess: DMatrix<f64>,
|
||||||
|
frozen: &[(usize, usize)],
|
||||||
|
scaled_tol: f64,
|
||||||
|
min_efficiency: f64,
|
||||||
|
backoff: f64,
|
||||||
|
reg_scale: f64,
|
||||||
|
max_descent_steps: i32,
|
||||||
|
max_backoff_steps: i32
|
||||||
|
) -> (DMatrix<f64>, bool, DescentHistory) {
|
||||||
|
// start the descent history
|
||||||
|
let mut history = DescentHistory::new();
|
||||||
|
|
||||||
|
// find the dimension of the search space
|
||||||
|
let element_dim = guess.nrows();
|
||||||
|
let assembly_dim = guess.ncols();
|
||||||
|
let total_dim = element_dim * assembly_dim;
|
||||||
|
|
||||||
|
// scale the tolerance
|
||||||
|
let scale_adjustment = (gram.0.len() as f64).sqrt();
|
||||||
|
let tol = scale_adjustment * scaled_tol;
|
||||||
|
|
||||||
|
// convert the frozen indices to stacked format
|
||||||
|
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|
||||||
|
|index| index.1*element_dim + index.0
|
||||||
|
).collect();
|
||||||
|
|
||||||
|
// use Newton's method with backtracking and gradient descent backup
|
||||||
|
let mut state = SearchState::from_config(gram, guess);
|
||||||
|
for _ in 0..max_descent_steps {
|
||||||
|
// stop if the loss is tolerably low
|
||||||
|
history.config.push(state.config.clone());
|
||||||
|
history.scaled_loss.push(state.loss / scale_adjustment);
|
||||||
|
if state.loss < tol { break; }
|
||||||
|
|
||||||
|
// find the negative gradient of the loss function
|
||||||
|
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
||||||
|
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
||||||
|
history.neg_grad.push(neg_grad.clone());
|
||||||
|
|
||||||
|
// find the negative Hessian of the loss function
|
||||||
|
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
|
||||||
|
for col in 0..assembly_dim {
|
||||||
|
for row in 0..element_dim {
|
||||||
|
let index = (row, col);
|
||||||
|
let basis_mat = basis_matrix(index, element_dim, assembly_dim);
|
||||||
|
let neg_d_err =
|
||||||
|
basis_mat.tr_mul(&*Q) * &state.config
|
||||||
|
+ state.config.tr_mul(&*Q) * &basis_mat;
|
||||||
|
let neg_d_err_proj = gram.proj(&neg_d_err);
|
||||||
|
let deriv_grad = 4.0 * &*Q * (
|
||||||
|
-&basis_mat * &state.err_proj
|
||||||
|
+ &state.config * &neg_d_err_proj
|
||||||
|
);
|
||||||
|
hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut hess = DMatrix::from_columns(hess_cols.as_slice());
|
||||||
|
|
||||||
|
// regularize the Hessian
|
||||||
|
let min_eigval = hess.symmetric_eigenvalues().min();
|
||||||
|
if min_eigval <= 0.0 {
|
||||||
|
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
|
||||||
|
}
|
||||||
|
history.min_eigval.push(min_eigval);
|
||||||
|
|
||||||
|
// project the negative gradient and negative Hessian onto the
|
||||||
|
// orthogonal complement of the frozen subspace
|
||||||
|
let zero_col = DVector::zeros(total_dim);
|
||||||
|
let zero_row = zero_col.transpose();
|
||||||
|
for &k in &frozen_stacked {
|
||||||
|
neg_grad_stacked[k] = 0.0;
|
||||||
|
hess.set_row(k, &zero_row);
|
||||||
|
hess.set_column(k, &zero_col);
|
||||||
|
hess[(k, k)] = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute the Newton step
|
||||||
|
/*
|
||||||
|
we need to either handle or eliminate the case where the minimum
|
||||||
|
eigenvalue of the Hessian is zero, so the regularized Hessian is
|
||||||
|
singular. right now, this causes the Cholesky decomposition to return
|
||||||
|
`None`, leading to a panic when we unrap
|
||||||
|
*/
|
||||||
|
let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked);
|
||||||
|
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
||||||
|
history.base_step.push(base_step.clone());
|
||||||
|
|
||||||
|
// use backtracking line search to find a better configuration
|
||||||
|
match seek_better_config(
|
||||||
|
gram, &state, &base_step, neg_grad.dot(&base_step),
|
||||||
|
min_efficiency, backoff, max_backoff_steps
|
||||||
|
) {
|
||||||
|
Some((better_state, backoff_steps)) => {
|
||||||
|
state = better_state;
|
||||||
|
history.backoff_steps.push(backoff_steps);
|
||||||
|
},
|
||||||
|
None => return (state.config, false, history)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(state.config, state.loss < tol, history)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tests ---
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{array, f64::consts::PI};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sub_proj_test() {
|
||||||
|
let target = PartialMatrix(vec![
|
||||||
|
MatrixEntry { index: (0, 0), value: 19.0 },
|
||||||
|
MatrixEntry { index: (0, 2), value: 39.0 },
|
||||||
|
MatrixEntry { index: (1, 1), value: 59.0 },
|
||||||
|
MatrixEntry { index: (1, 2), value: 69.0 }
|
||||||
|
]);
|
||||||
|
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
|
||||||
|
1.0, 2.0, 3.0,
|
||||||
|
4.0, 5.0, 6.0
|
||||||
|
]);
|
||||||
|
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
|
||||||
|
18.0, 0.0, 36.0,
|
||||||
|
0.0, 54.0, 63.0
|
||||||
|
]);
|
||||||
|
assert_eq!(target.sub_proj(&attempt), expected_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_loss_test() {
|
||||||
|
let gram = PartialMatrix({
|
||||||
|
let mut entries = Vec::<MatrixEntry>::new();
|
||||||
|
for j in 0..3 {
|
||||||
|
for k in 0..3 {
|
||||||
|
entries.push(MatrixEntry {
|
||||||
|
index: (j, k),
|
||||||
|
value: if j == k { 1.0 } else { -1.0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
});
|
||||||
|
let config = {
|
||||||
|
let a: f64 = 0.75_f64.sqrt();
|
||||||
|
DMatrix::from_columns(&[
|
||||||
|
sphere(1.0, 0.0, 0.0, a),
|
||||||
|
sphere(-0.5, a, 0.0, a),
|
||||||
|
sphere(-0.5, -a, 0.0, a)
|
||||||
|
])
|
||||||
|
};
|
||||||
|
let state = SearchState::from_config(&gram, config);
|
||||||
|
assert!(state.loss.abs() < f64::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
|
||||||
|
// below includes a nice translation of the problem statement, which was
|
||||||
|
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
|
||||||
|
// Present_)
|
||||||
|
//
|
||||||
|
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
|
||||||
|
// https://www.nippon.com/en/japan-topics/c12801/
|
||||||
|
//
|
||||||
|
#[test]
|
||||||
|
fn irisawa_hexlet_test() {
|
||||||
|
let gram = PartialMatrix({
|
||||||
|
let mut entries = Vec::<MatrixEntry>::new();
|
||||||
|
for s in 0..9 {
|
||||||
|
// each sphere is represented by a spacelike vector
|
||||||
|
entries.push(MatrixEntry { index: (s, s), value: 1.0 });
|
||||||
|
|
||||||
|
// the circumscribing sphere is tangent to all of the other
|
||||||
|
// spheres, with matching orientation
|
||||||
|
if s > 0 {
|
||||||
|
entries.push(MatrixEntry { index: (0, s), value: 1.0 });
|
||||||
|
entries.push(MatrixEntry { index: (s, 0), value: 1.0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if s > 2 {
|
||||||
|
// each chain sphere is tangent to the "sun" and "moon"
|
||||||
|
// spheres, with opposing orientation
|
||||||
|
for n in 1..3 {
|
||||||
|
entries.push(MatrixEntry { index: (s, n), value: -1.0 });
|
||||||
|
entries.push(MatrixEntry { index: (n, s), value: -1.0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// each chain sphere is tangent to the next chain sphere,
|
||||||
|
// with opposing orientation
|
||||||
|
let s_next = 3 + (s-2) % 6;
|
||||||
|
entries.push(MatrixEntry { index: (s, s_next), value: -1.0 });
|
||||||
|
entries.push(MatrixEntry { index: (s_next, s), value: -1.0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
});
|
||||||
|
let guess = DMatrix::from_columns(
|
||||||
|
[
|
||||||
|
sphere(0.0, 0.0, 0.0, 15.0),
|
||||||
|
sphere(0.0, 0.0, -9.0, 5.0),
|
||||||
|
sphere(0.0, 0.0, 11.0, 3.0)
|
||||||
|
].into_iter().chain(
|
||||||
|
(1..=6).map(
|
||||||
|
|k| {
|
||||||
|
let ang = (k as f64) * PI/3.0;
|
||||||
|
sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).collect::<Vec<_>>().as_slice()
|
||||||
|
);
|
||||||
|
let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k));
|
||||||
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
|
let (config, success, history) = realize_gram(
|
||||||
|
&gram, guess, &frozen,
|
||||||
|
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
|
);
|
||||||
|
let entry_tol = SCALED_TOL.sqrt();
|
||||||
|
let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0];
|
||||||
|
for (k, diam) in solution_diams.into_iter().enumerate() {
|
||||||
|
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
|
||||||
|
}
|
||||||
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
|
if success {
|
||||||
|
println!("Target accuracy achieved!");
|
||||||
|
} else {
|
||||||
|
println!("Failed to reach target accuracy");
|
||||||
|
}
|
||||||
|
println!("Steps: {}", history.scaled_loss.len() - 1);
|
||||||
|
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
||||||
|
if success {
|
||||||
|
println!("\nChain diameters:");
|
||||||
|
println!(" {} sun (given)", 1.0 / config[(3, 3)]);
|
||||||
|
for k in 4..9 {
|
||||||
|
println!(" {} sun", 1.0 / config[(3, k)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
||||||
|
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
||||||
|
println!("{:<4} │ {}", step, scaled_loss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- process inspection examples ---
|
||||||
|
|
||||||
|
// these tests are meant for human inspection, not automated use. run them
|
||||||
|
// one at a time in `--nocapture` mode and read through the results and
|
||||||
|
// optimization histories that they print out. the `run-examples` script
|
||||||
|
// will run all of them
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn three_spheres_example() {
|
||||||
|
let gram = PartialMatrix({
|
||||||
|
let mut entries = Vec::<MatrixEntry>::new();
|
||||||
|
for j in 0..3 {
|
||||||
|
for k in 0..3 {
|
||||||
|
entries.push(MatrixEntry {
|
||||||
|
index: (j, k),
|
||||||
|
value: if j == k { 1.0 } else { -1.0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
});
|
||||||
|
let guess = {
|
||||||
|
let a: f64 = 0.75_f64.sqrt();
|
||||||
|
DMatrix::from_columns(&[
|
||||||
|
sphere(1.0, 0.0, 0.0, 1.0),
|
||||||
|
sphere(-0.5, a, 0.0, 1.0),
|
||||||
|
sphere(-0.5, -a, 0.0, 1.0)
|
||||||
|
])
|
||||||
|
};
|
||||||
|
println!();
|
||||||
|
let (config, success, history) = realize_gram(
|
||||||
|
&gram, guess, &[],
|
||||||
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
|
);
|
||||||
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
|
if success {
|
||||||
|
println!("Target accuracy achieved!");
|
||||||
|
} else {
|
||||||
|
println!("Failed to reach target accuracy");
|
||||||
|
}
|
||||||
|
println!("Steps: {}", history.scaled_loss.len() - 1);
|
||||||
|
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
||||||
|
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
||||||
|
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
||||||
|
println!("{:<4} │ {}", step, scaled_loss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn point_on_sphere_example() {
|
||||||
|
let gram = PartialMatrix({
|
||||||
|
let mut entries = Vec::<MatrixEntry>::new();
|
||||||
|
for j in 0..2 {
|
||||||
|
for k in 0..2 {
|
||||||
|
entries.push(MatrixEntry {
|
||||||
|
index: (j, k),
|
||||||
|
value: if (j, k) == (1, 1) { 1.0 } else { 0.0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
});
|
||||||
|
let guess = DMatrix::from_columns(&[
|
||||||
|
point(0.0, 0.0, 2.0),
|
||||||
|
sphere(0.0, 0.0, 0.0, 1.0)
|
||||||
|
]);
|
||||||
|
let frozen = [(3, 0)];
|
||||||
|
println!();
|
||||||
|
let (config, success, history) = realize_gram(
|
||||||
|
&gram, guess, &frozen,
|
||||||
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
|
);
|
||||||
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
|
print!("Configuration:{}", config);
|
||||||
|
if success {
|
||||||
|
println!("Target accuracy achieved!");
|
||||||
|
} else {
|
||||||
|
println!("Failed to reach target accuracy");
|
||||||
|
}
|
||||||
|
println!("Steps: {}", history.scaled_loss.len() - 1);
|
||||||
|
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
||||||
|
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
||||||
|
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
||||||
|
println!("{:<4} │ {}", step, scaled_loss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TO DO */
|
||||||
|
// --- new test placed here to avoid merge conflict ---
|
||||||
|
|
||||||
|
// at the frozen indices, the optimization steps should have exact zeros,
|
||||||
|
// and the realized configuration should match the initial guess
|
||||||
|
#[test]
|
||||||
|
fn frozen_entry_test() {
|
||||||
|
let gram = {
|
||||||
|
let mut gram_to_be = PartialMatrix::new();
|
||||||
|
for j in 0..2 {
|
||||||
|
for k in j..2 {
|
||||||
|
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gram_to_be
|
||||||
|
};
|
||||||
|
let guess = DMatrix::from_columns(&[
|
||||||
|
point(0.0, 0.0, 2.0),
|
||||||
|
sphere(0.0, 0.0, 0.0, 1.0)
|
||||||
|
]);
|
||||||
|
let frozen = [(3, 0), (3, 1)];
|
||||||
|
println!();
|
||||||
|
let (config, success, history) = realize_gram(
|
||||||
|
&gram, guess.clone(), &frozen,
|
||||||
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
|
);
|
||||||
|
assert_eq!(success, true);
|
||||||
|
for base_step in history.base_step.into_iter() {
|
||||||
|
for index in frozen {
|
||||||
|
assert_eq!(base_step[index], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index in frozen {
|
||||||
|
assert_eq!(config[index], guess[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
app-proto/src/identity.vert
Normal file
7
app-proto/src/identity.vert
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#version 300 es
|
||||||
|
|
||||||
|
in vec4 position;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = position;
|
||||||
|
}
|
234
app-proto/src/inversive.frag
Normal file
234
app-proto/src/inversive.frag
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
#version 300 es
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
// --- inversive geometry ---
|
||||||
|
|
||||||
|
struct vecInv {
|
||||||
|
vec3 sp;
|
||||||
|
vec2 lt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- uniforms ---
|
||||||
|
|
||||||
|
// assembly
|
||||||
|
const int SPHERE_MAX = 200;
|
||||||
|
uniform int sphere_cnt;
|
||||||
|
uniform vecInv sphere_list[SPHERE_MAX];
|
||||||
|
uniform vec3 color_list[SPHERE_MAX];
|
||||||
|
uniform float highlight_list[SPHERE_MAX];
|
||||||
|
|
||||||
|
// view
|
||||||
|
uniform vec2 resolution;
|
||||||
|
uniform float shortdim;
|
||||||
|
|
||||||
|
// controls
|
||||||
|
uniform float opacity;
|
||||||
|
uniform int layer_threshold;
|
||||||
|
uniform bool debug_mode;
|
||||||
|
|
||||||
|
// light and camera
|
||||||
|
const float focal_slope = 0.3;
|
||||||
|
const vec3 light_dir = normalize(vec3(2., 2., 1.));
|
||||||
|
const float ixn_threshold = 0.005;
|
||||||
|
const float INTERIOR_DIMMING = 0.7;
|
||||||
|
|
||||||
|
// --- sRGB ---
|
||||||
|
|
||||||
|
// map colors from RGB space to sRGB space, as specified in the sRGB standard
|
||||||
|
// (IEC 61966-2-1:1999)
|
||||||
|
//
|
||||||
|
// https://www.color.org/sRGB.pdf
|
||||||
|
// https://www.color.org/chardata/rgb/srgb.xalter
|
||||||
|
//
|
||||||
|
// in RGB space, color value is proportional to light intensity, so linear
|
||||||
|
// color-vector interpolation corresponds to physical light mixing. in sRGB
|
||||||
|
// space, the color encoding used by many monitors, we use more of the value
|
||||||
|
// interval to represent low intensities, and less of the interval to represent
|
||||||
|
// high intensities. this improves color quantization
|
||||||
|
|
||||||
|
float sRGB(float t) {
|
||||||
|
if (t <= 0.0031308) {
|
||||||
|
return 12.92*t;
|
||||||
|
} else {
|
||||||
|
return 1.055*pow(t, 5./12.) - 0.055;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 sRGB(vec3 color) {
|
||||||
|
return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- shading ---
|
||||||
|
|
||||||
|
struct Fragment {
|
||||||
|
vec3 pt;
|
||||||
|
vec3 normal;
|
||||||
|
vec4 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) {
|
||||||
|
// the expression for normal needs to be checked. it's supposed to give the
|
||||||
|
// negative gradient of the lorentz product between the impact point vector
|
||||||
|
// and the sphere vector with respect to the coordinates of the impact
|
||||||
|
// point. i calculated it in my head and decided that the result looked good
|
||||||
|
// enough for now
|
||||||
|
vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt);
|
||||||
|
|
||||||
|
float incidence = dot(normal, light_dir);
|
||||||
|
float illum = mix(0.4, 1.0, max(incidence, 0.0));
|
||||||
|
return Fragment(pt, normal, vec4(illum * base_color, opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
float intersection_dist(Fragment a, Fragment b) {
|
||||||
|
float intersection_sin = length(cross(a.normal, b.normal));
|
||||||
|
vec3 disp = a.pt - b.pt;
|
||||||
|
return max(
|
||||||
|
abs(dot(a.normal, disp)),
|
||||||
|
abs(dot(b.normal, disp))
|
||||||
|
) / intersection_sin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ray-casting ---
|
||||||
|
|
||||||
|
struct TaggedDepth {
|
||||||
|
float depth;
|
||||||
|
float dimming;
|
||||||
|
int id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by
|
||||||
|
// the linear function `b*u + c`
|
||||||
|
const float DEG_THRESHOLD = 1e-9;
|
||||||
|
|
||||||
|
// the depths, represented as multiples of `dir`, where the line generated by
|
||||||
|
// `dir` hits the sphere represented by `v`. if both depths are positive, the
|
||||||
|
// smaller one is returned in the first component. if only one depth is
|
||||||
|
// positive, it could be returned in either component
|
||||||
|
vec2 sphere_cast(vecInv v, vec3 dir) {
|
||||||
|
float a = -v.lt.s * dot(dir, dir);
|
||||||
|
float b = dot(v.sp, dir);
|
||||||
|
float c = -v.lt.t;
|
||||||
|
|
||||||
|
float adjust = 4.*a*c/(b*b);
|
||||||
|
if (adjust < 1.) {
|
||||||
|
// as long as `b` is non-zero, the linear approximation of
|
||||||
|
//
|
||||||
|
// a*u^2 + b*u + c
|
||||||
|
//
|
||||||
|
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of the
|
||||||
|
// quadratic adjacent to `u_lin` is stored in `lin_root`. if both roots
|
||||||
|
// have the same sign, `lin_root` will be the one closer to `u = 0`
|
||||||
|
float square_rect_ratio = 1. + sqrt(1. - adjust);
|
||||||
|
float lin_root = -(2.*c)/b / square_rect_ratio;
|
||||||
|
if (abs(a) > DEG_THRESHOLD * abs(b)) {
|
||||||
|
return vec2(lin_root, -b/(2.*a) * square_rect_ratio);
|
||||||
|
} else {
|
||||||
|
return vec2(lin_root, -1.);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the line through `dir` misses the sphere completely
|
||||||
|
return vec2(-1., -1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim;
|
||||||
|
vec3 dir = vec3(focal_slope * scr, -1.);
|
||||||
|
|
||||||
|
// cast rays through the spheres
|
||||||
|
const int LAYER_MAX = 12;
|
||||||
|
TaggedDepth top_hits [LAYER_MAX];
|
||||||
|
int layer_cnt = 0;
|
||||||
|
for (int id = 0; id < sphere_cnt; ++id) {
|
||||||
|
// find out where the ray hits the sphere
|
||||||
|
vec2 hit_depths = sphere_cast(sphere_list[id], dir);
|
||||||
|
|
||||||
|
// insertion-sort the points we hit into the hit list
|
||||||
|
float dimming = 1.;
|
||||||
|
for (int side = 0; side < 2; ++side) {
|
||||||
|
float depth = hit_depths[side];
|
||||||
|
if (depth > 0.) {
|
||||||
|
for (int layer = layer_cnt; layer >= 0; --layer) {
|
||||||
|
if (layer < 1 || top_hits[layer-1].depth <= depth) {
|
||||||
|
// we're not as close to the screen as the hit before
|
||||||
|
// the empty slot, so insert here
|
||||||
|
if (layer < LAYER_MAX) {
|
||||||
|
top_hits[layer] = TaggedDepth(depth, dimming, id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// we're closer to the screen than the hit before the
|
||||||
|
// empty slot, so move that hit into the empty slot
|
||||||
|
top_hits[layer] = top_hits[layer-1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layer_cnt = min(layer_cnt + 1, LAYER_MAX);
|
||||||
|
dimming = INTERIOR_DIMMING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// in debug mode, show the layer count instead of the shaded image
|
||||||
|
if (debug_mode) {
|
||||||
|
// at the bottom of the screen, show the color scale instead of the
|
||||||
|
// layer count
|
||||||
|
if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
|
||||||
|
|
||||||
|
// convert number to color
|
||||||
|
ivec3 bits = layer_cnt / ivec3(1, 2, 4);
|
||||||
|
vec3 color = mod(vec3(bits), 2.);
|
||||||
|
if (layer_cnt % 16 >= 8) {
|
||||||
|
color = mix(color, vec3(0.5), 0.5);
|
||||||
|
}
|
||||||
|
outColor = vec4(color, 1.);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// composite the sphere fragments
|
||||||
|
vec3 color = vec3(0.);
|
||||||
|
int layer = layer_cnt - 1;
|
||||||
|
TaggedDepth hit = top_hits[layer];
|
||||||
|
Fragment frag_next = sphere_shading(
|
||||||
|
sphere_list[hit.id],
|
||||||
|
hit.depth * dir,
|
||||||
|
hit.dimming * color_list[hit.id]
|
||||||
|
);
|
||||||
|
float highlight_next = highlight_list[hit.id];
|
||||||
|
--layer;
|
||||||
|
for (; layer >= layer_threshold; --layer) {
|
||||||
|
// load the current fragment
|
||||||
|
Fragment frag = frag_next;
|
||||||
|
float highlight = highlight_next;
|
||||||
|
|
||||||
|
// shade the next fragment
|
||||||
|
hit = top_hits[layer];
|
||||||
|
frag_next = sphere_shading(
|
||||||
|
sphere_list[hit.id],
|
||||||
|
hit.depth * dir,
|
||||||
|
hit.dimming * color_list[hit.id]
|
||||||
|
);
|
||||||
|
highlight_next = highlight_list[hit.id];
|
||||||
|
|
||||||
|
// highlight intersections
|
||||||
|
float ixn_dist = intersection_dist(frag, frag_next);
|
||||||
|
float max_highlight = max(highlight, highlight_next);
|
||||||
|
float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist));
|
||||||
|
frag.color = mix(frag.color, vec4(1.), ixn_highlight);
|
||||||
|
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
|
||||||
|
|
||||||
|
// highlight cusps
|
||||||
|
float cusp_cos = abs(dot(dir, frag.normal));
|
||||||
|
float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s);
|
||||||
|
float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos));
|
||||||
|
frag.color = mix(frag.color, vec4(1.), cusp_highlight);
|
||||||
|
|
||||||
|
// composite the current fragment
|
||||||
|
color = mix(color, frag.color.rgb, frag.color.a);
|
||||||
|
}
|
||||||
|
color = mix(color, frag_next.color.rgb, frag_next.color.a);
|
||||||
|
outColor = vec4(sRGB(color), 1.);
|
||||||
|
}
|
42
app-proto/src/main.rs
Normal file
42
app-proto/src/main.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
mod add_remove;
|
||||||
|
mod assembly;
|
||||||
|
mod display;
|
||||||
|
mod engine;
|
||||||
|
mod outline;
|
||||||
|
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
|
use add_remove::AddRemove;
|
||||||
|
use assembly::{Assembly, ElementKey};
|
||||||
|
use display::Display;
|
||||||
|
use outline::Outline;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
assembly: Assembly,
|
||||||
|
selection: Signal<FxHashSet<ElementKey>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
fn new() -> AppState {
|
||||||
|
AppState {
|
||||||
|
assembly: Assembly::new(),
|
||||||
|
selection: create_signal(FxHashSet::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
sycamore::render(|| {
|
||||||
|
provide_context(AppState::new());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
div(id="sidebar") {
|
||||||
|
AddRemove {}
|
||||||
|
Outline {}
|
||||||
|
}
|
||||||
|
Display {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
207
app-proto/src/outline.rs
Normal file
207
app-proto/src/outline.rs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
use web_sys::{
|
||||||
|
Event,
|
||||||
|
HtmlInputElement,
|
||||||
|
KeyboardEvent,
|
||||||
|
MouseEvent,
|
||||||
|
wasm_bindgen::JsCast
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}};
|
||||||
|
|
||||||
|
// an editable view of the Lorentz product representing a constraint
|
||||||
|
#[component(inline_props)]
|
||||||
|
fn LorentzProductInput(constraint: Constraint) -> View {
|
||||||
|
view! {
|
||||||
|
input(
|
||||||
|
r#type="text",
|
||||||
|
bind:value=constraint.lorentz_prod_text,
|
||||||
|
on:change=move |event: Event| {
|
||||||
|
let target: HtmlInputElement = event.target().unwrap().unchecked_into();
|
||||||
|
match target.value().parse::<f64>() {
|
||||||
|
Ok(lorentz_prod) => batch(|| {
|
||||||
|
constraint.lorentz_prod.set(lorentz_prod);
|
||||||
|
constraint.lorentz_prod_valid.set(true);
|
||||||
|
}),
|
||||||
|
Err(_) => constraint.lorentz_prod_valid.set(false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a list item that shows a constraint in an outline view of an element
|
||||||
|
#[component(inline_props)]
|
||||||
|
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let assembly = &state.assembly;
|
||||||
|
let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone());
|
||||||
|
let other_subject = if constraint.subjects.0 == element_key {
|
||||||
|
constraint.subjects.1
|
||||||
|
} else {
|
||||||
|
constraint.subjects.0
|
||||||
|
};
|
||||||
|
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
|
||||||
|
let class = constraint.lorentz_prod_valid.map(
|
||||||
|
|&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" }
|
||||||
|
);
|
||||||
|
view! {
|
||||||
|
li(class=class.get()) {
|
||||||
|
input(r#type="checkbox", bind:checked=constraint.active)
|
||||||
|
div(class="constraint-label") { (other_subject_label) }
|
||||||
|
LorentzProductInput(constraint=constraint)
|
||||||
|
div(class="status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a list item that shows an element in an outline view of an assembly
|
||||||
|
#[component(inline_props)]
|
||||||
|
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let class = state.selection.map(
|
||||||
|
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
||||||
|
);
|
||||||
|
let label = element.label.clone();
|
||||||
|
let rep_components = element.representation.map(
|
||||||
|
|rep| rep.iter().map(
|
||||||
|
|u| format!("{:.3}", u).replace("-", "\u{2212}")
|
||||||
|
).collect()
|
||||||
|
);
|
||||||
|
let constrained = element.constraints.map(|csts| csts.len() > 0);
|
||||||
|
let constraint_list = element.constraints.map(
|
||||||
|
|csts| csts.clone().into_iter().collect()
|
||||||
|
);
|
||||||
|
let details_node = create_node_ref();
|
||||||
|
view! {
|
||||||
|
li {
|
||||||
|
details(ref=details_node) {
|
||||||
|
summary(
|
||||||
|
class=class.get(),
|
||||||
|
on:keydown={
|
||||||
|
move |event: KeyboardEvent| {
|
||||||
|
match event.key().as_str() {
|
||||||
|
"Enter" => {
|
||||||
|
if event.shift_key() {
|
||||||
|
state.selection.update(|sel| {
|
||||||
|
if !sel.remove(&key) {
|
||||||
|
sel.insert(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
state.selection.update(|sel| {
|
||||||
|
sel.clear();
|
||||||
|
sel.insert(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
event.prevent_default();
|
||||||
|
},
|
||||||
|
"ArrowRight" if constrained.get() => {
|
||||||
|
let _ = details_node
|
||||||
|
.get()
|
||||||
|
.unchecked_into::<web_sys::Element>()
|
||||||
|
.set_attribute("open", "");
|
||||||
|
},
|
||||||
|
"ArrowLeft" => {
|
||||||
|
let _ = details_node
|
||||||
|
.get()
|
||||||
|
.unchecked_into::<web_sys::Element>()
|
||||||
|
.remove_attribute("open");
|
||||||
|
},
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
div(
|
||||||
|
class="element-switch",
|
||||||
|
on:click=|event: MouseEvent| event.stop_propagation()
|
||||||
|
)
|
||||||
|
div(
|
||||||
|
class="element",
|
||||||
|
on:click={
|
||||||
|
move |event: MouseEvent| {
|
||||||
|
if event.shift_key() {
|
||||||
|
state.selection.update(|sel| {
|
||||||
|
if !sel.remove(&key) {
|
||||||
|
sel.insert(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
state.selection.update(|sel| {
|
||||||
|
sel.clear();
|
||||||
|
sel.insert(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
event.stop_propagation();
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
div(class="element-label") { (label) }
|
||||||
|
div(class="element-representation") {
|
||||||
|
Indexed(
|
||||||
|
list=rep_components,
|
||||||
|
view=|coord_str| view! {
|
||||||
|
div { (coord_str) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
div(class="status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul(class="constraints") {
|
||||||
|
Keyed(
|
||||||
|
list=constraint_list,
|
||||||
|
view=move |cst_key| view! {
|
||||||
|
ConstraintOutlineItem(
|
||||||
|
constraint_key=cst_key,
|
||||||
|
element_key=key
|
||||||
|
)
|
||||||
|
},
|
||||||
|
key=|cst_key| cst_key.clone()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a component that lists the elements of the current assembly, showing the
|
||||||
|
// constraints on each element as a collapsible sub-list. its implementation
|
||||||
|
// is based on Kate Morley's HTML + CSS tree views:
|
||||||
|
//
|
||||||
|
// https://iamkate.com/code/tree-views/
|
||||||
|
//
|
||||||
|
#[component]
|
||||||
|
pub fn Outline() -> View {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
|
||||||
|
// list the elements alphabetically by ID
|
||||||
|
let element_list = state.assembly.elements.map(
|
||||||
|
|elts| elts
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.sorted_by_key(|(_, elt)| elt.id.clone())
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
ul(
|
||||||
|
id="outline",
|
||||||
|
on:click={
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
move |_| state.selection.update(|sel| sel.clear())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Keyed(
|
||||||
|
list=element_list,
|
||||||
|
view=|(key, elt)| view! {
|
||||||
|
ElementOutlineItem(key=key, element=elt)
|
||||||
|
},
|
||||||
|
key=|(_, elt)| elt.serial
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,8 @@ using Optim
|
|||||||
|
|
||||||
export
|
export
|
||||||
rand_on_shell, Q, DescentHistory,
|
rand_on_shell, Q, DescentHistory,
|
||||||
realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram
|
realize_gram_gradient, realize_gram_newton, realize_gram_optim,
|
||||||
|
realize_gram_alt_proj, realize_gram
|
||||||
|
|
||||||
# === guessing ===
|
# === guessing ===
|
||||||
|
|
||||||
@ -143,7 +144,7 @@ function realize_gram_gradient(
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# find negative gradient of loss function
|
# find the negative gradient of the loss function
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
slope = norm(neg_grad)
|
slope = norm(neg_grad)
|
||||||
dir = neg_grad / slope
|
dir = neg_grad / slope
|
||||||
@ -232,7 +233,7 @@ function realize_gram_newton(
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# find the negative gradient of loss function
|
# find the negative gradient of the loss function
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
|
|
||||||
# find the negative Hessian of the loss function
|
# find the negative Hessian of the loss function
|
||||||
@ -313,6 +314,129 @@ function realize_gram_optim(
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every
|
||||||
|
# explicit entry of `gram`. use gradient descent starting from `guess`, with an
|
||||||
|
# alternate technique for finding the projected base step from the unprojected
|
||||||
|
# Hessian
|
||||||
|
function realize_gram_alt_proj(
|
||||||
|
gram::SparseMatrixCSC{T, <:Any},
|
||||||
|
guess::Matrix{T},
|
||||||
|
frozen = CartesianIndex[];
|
||||||
|
scaled_tol = 1e-30,
|
||||||
|
min_efficiency = 0.5,
|
||||||
|
backoff = 0.9,
|
||||||
|
reg_scale = 1.1,
|
||||||
|
max_descent_steps = 200,
|
||||||
|
max_backoff_steps = 110
|
||||||
|
) where T <: Number
|
||||||
|
# start history
|
||||||
|
history = DescentHistory{T}()
|
||||||
|
|
||||||
|
# find the dimension of the search space
|
||||||
|
dims = size(guess)
|
||||||
|
element_dim, construction_dim = dims
|
||||||
|
total_dim = element_dim * construction_dim
|
||||||
|
|
||||||
|
# list the constrained entries of the gram matrix
|
||||||
|
J, K, _ = findnz(gram)
|
||||||
|
constrained = zip(J, K)
|
||||||
|
|
||||||
|
# scale the tolerance
|
||||||
|
scale_adjustment = sqrt(T(length(constrained)))
|
||||||
|
tol = scale_adjustment * scaled_tol
|
||||||
|
|
||||||
|
# convert the frozen indices to stacked format
|
||||||
|
frozen_stacked = [(index[2]-1)*element_dim + index[1] for index in frozen]
|
||||||
|
|
||||||
|
# initialize search state
|
||||||
|
L = copy(guess)
|
||||||
|
Δ_proj = proj_diff(gram, L'*Q*L)
|
||||||
|
loss = dot(Δ_proj, Δ_proj)
|
||||||
|
|
||||||
|
# use Newton's method with backtracking and gradient descent backup
|
||||||
|
for step in 1:max_descent_steps
|
||||||
|
# stop if the loss is tolerably low
|
||||||
|
if loss < tol
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
# find the negative gradient of the loss function
|
||||||
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
|
|
||||||
|
# find the negative Hessian of the loss function
|
||||||
|
hess = Matrix{T}(undef, total_dim, total_dim)
|
||||||
|
indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim]
|
||||||
|
for (j, k) in indices
|
||||||
|
basis_mat = basis_matrix(T, j, k, dims)
|
||||||
|
neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat
|
||||||
|
neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained)
|
||||||
|
deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj)
|
||||||
|
hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim)
|
||||||
|
end
|
||||||
|
hess_sym = Hermitian(hess)
|
||||||
|
push!(history.hess, hess_sym)
|
||||||
|
|
||||||
|
# regularize the Hessian
|
||||||
|
min_eigval = minimum(eigvals(hess_sym))
|
||||||
|
push!(history.positive, min_eigval > 0)
|
||||||
|
if min_eigval <= 0
|
||||||
|
hess -= reg_scale * min_eigval * I
|
||||||
|
end
|
||||||
|
|
||||||
|
# compute the Newton step
|
||||||
|
neg_grad_stacked = reshape(neg_grad, total_dim)
|
||||||
|
for k in frozen_stacked
|
||||||
|
neg_grad_stacked[k] = 0
|
||||||
|
hess[k, :] .= 0
|
||||||
|
hess[:, k] .= 0
|
||||||
|
hess[k, k] = 1
|
||||||
|
end
|
||||||
|
base_step_stacked = Hermitian(hess) \ neg_grad_stacked
|
||||||
|
base_step = reshape(base_step_stacked, dims)
|
||||||
|
push!(history.base_step, base_step)
|
||||||
|
|
||||||
|
# store the current position, loss, and slope
|
||||||
|
L_last = L
|
||||||
|
loss_last = loss
|
||||||
|
push!(history.scaled_loss, loss / scale_adjustment)
|
||||||
|
push!(history.neg_grad, neg_grad)
|
||||||
|
push!(history.slope, norm(neg_grad))
|
||||||
|
|
||||||
|
# find a good step size using backtracking line search
|
||||||
|
push!(history.stepsize, 0)
|
||||||
|
push!(history.backoff_steps, max_backoff_steps)
|
||||||
|
empty!(history.last_line_L)
|
||||||
|
empty!(history.last_line_loss)
|
||||||
|
rate = one(T)
|
||||||
|
step_success = false
|
||||||
|
base_target_improvement = dot(neg_grad, base_step)
|
||||||
|
for backoff_steps in 0:max_backoff_steps
|
||||||
|
history.stepsize[end] = rate
|
||||||
|
L = L_last + rate * base_step
|
||||||
|
Δ_proj = proj_diff(gram, L'*Q*L)
|
||||||
|
loss = dot(Δ_proj, Δ_proj)
|
||||||
|
improvement = loss_last - loss
|
||||||
|
push!(history.last_line_L, L)
|
||||||
|
push!(history.last_line_loss, loss / scale_adjustment)
|
||||||
|
if improvement >= min_efficiency * rate * base_target_improvement
|
||||||
|
history.backoff_steps[end] = backoff_steps
|
||||||
|
step_success = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
rate *= backoff
|
||||||
|
end
|
||||||
|
|
||||||
|
# if we've hit a wall, quit
|
||||||
|
if !step_success
|
||||||
|
return L_last, false, history
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# return the factorization and its history
|
||||||
|
push!(history.scaled_loss, loss / scale_adjustment)
|
||||||
|
L, loss < tol, history
|
||||||
|
end
|
||||||
|
|
||||||
# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every
|
# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every
|
||||||
# explicit entry of `gram`. use gradient descent starting from `guess`
|
# explicit entry of `gram`. use gradient descent starting from `guess`
|
||||||
function realize_gram(
|
function realize_gram(
|
||||||
@ -321,7 +445,6 @@ function realize_gram(
|
|||||||
frozen = nothing;
|
frozen = nothing;
|
||||||
scaled_tol = 1e-30,
|
scaled_tol = 1e-30,
|
||||||
min_efficiency = 0.5,
|
min_efficiency = 0.5,
|
||||||
init_rate = 1.0,
|
|
||||||
backoff = 0.9,
|
backoff = 0.9,
|
||||||
reg_scale = 1.1,
|
reg_scale = 1.1,
|
||||||
max_descent_steps = 200,
|
max_descent_steps = 200,
|
||||||
@ -352,20 +475,19 @@ function realize_gram(
|
|||||||
unfrozen_stacked = reshape(is_unfrozen, total_dim)
|
unfrozen_stacked = reshape(is_unfrozen, total_dim)
|
||||||
end
|
end
|
||||||
|
|
||||||
# initialize variables
|
# initialize search state
|
||||||
grad_rate = init_rate
|
|
||||||
L = copy(guess)
|
L = copy(guess)
|
||||||
|
|
||||||
# use Newton's method with backtracking and gradient descent backup
|
|
||||||
Δ_proj = proj_diff(gram, L'*Q*L)
|
Δ_proj = proj_diff(gram, L'*Q*L)
|
||||||
loss = dot(Δ_proj, Δ_proj)
|
loss = dot(Δ_proj, Δ_proj)
|
||||||
|
|
||||||
|
# use Newton's method with backtracking and gradient descent backup
|
||||||
for step in 1:max_descent_steps
|
for step in 1:max_descent_steps
|
||||||
# stop if the loss is tolerably low
|
# stop if the loss is tolerably low
|
||||||
if loss < tol
|
if loss < tol
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# find the negative gradient of loss function
|
# find the negative gradient of the loss function
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
|
|
||||||
# find the negative Hessian of the loss function
|
# find the negative Hessian of the loss function
|
||||||
@ -420,6 +542,7 @@ function realize_gram(
|
|||||||
empty!(history.last_line_loss)
|
empty!(history.last_line_loss)
|
||||||
rate = one(T)
|
rate = one(T)
|
||||||
step_success = false
|
step_success = false
|
||||||
|
base_target_improvement = dot(neg_grad, base_step)
|
||||||
for backoff_steps in 0:max_backoff_steps
|
for backoff_steps in 0:max_backoff_steps
|
||||||
history.stepsize[end] = rate
|
history.stepsize[end] = rate
|
||||||
L = L_last + rate * base_step
|
L = L_last + rate * base_step
|
||||||
@ -428,7 +551,7 @@ function realize_gram(
|
|||||||
improvement = loss_last - loss
|
improvement = loss_last - loss
|
||||||
push!(history.last_line_L, L)
|
push!(history.last_line_L, L)
|
||||||
push!(history.last_line_loss, loss / scale_adjustment)
|
push!(history.last_line_loss, loss / scale_adjustment)
|
||||||
if improvement >= min_efficiency * rate * dot(neg_grad, base_step)
|
if improvement >= min_efficiency * rate * base_target_improvement
|
||||||
history.backoff_steps[end] = backoff_steps
|
history.backoff_steps[end] = backoff_steps
|
||||||
step_success = true
|
step_success = true
|
||||||
break
|
break
|
||||||
|
@ -75,3 +75,12 @@ if success
|
|||||||
println(" ", 1 / L[4,k], " sun")
|
println(" ", 1 / L[4,k], " sun")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# test an alternate technique for finding the projected base step from the
|
||||||
|
# unprojected Hessian
|
||||||
|
L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen)
|
||||||
|
completed_gram_alt = L_alt'*Engine.Q*L_alt
|
||||||
|
println("\nDifference in result using alternate projection:\n")
|
||||||
|
display(completed_gram_alt - completed_gram)
|
||||||
|
println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1))
|
||||||
|
println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n")
|
@ -65,3 +65,12 @@ else
|
|||||||
end
|
end
|
||||||
println("Steps: ", size(history.scaled_loss, 1))
|
println("Steps: ", size(history.scaled_loss, 1))
|
||||||
println("Loss: ", history.scaled_loss[end], "\n")
|
println("Loss: ", history.scaled_loss[end], "\n")
|
||||||
|
|
||||||
|
# test an alternate technique for finding the projected base step from the
|
||||||
|
# unprojected Hessian
|
||||||
|
L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen)
|
||||||
|
completed_gram_alt = L_alt'*Engine.Q*L_alt
|
||||||
|
println("\nDifference in result using alternate projection:\n")
|
||||||
|
display(completed_gram_alt - completed_gram)
|
||||||
|
println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1))
|
||||||
|
println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n")
|
@ -94,3 +94,12 @@ if success
|
|||||||
radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6])
|
radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6])
|
||||||
println("\nCircumradius / inradius: ", radius_ratio)
|
println("\nCircumradius / inradius: ", radius_ratio)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# test an alternate technique for finding the projected base step from the
|
||||||
|
# unprojected Hessian
|
||||||
|
L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen)
|
||||||
|
completed_gram_alt = L_alt'*Engine.Q*L_alt
|
||||||
|
println("\nDifference in result using alternate projection:\n")
|
||||||
|
display(completed_gram_alt - completed_gram)
|
||||||
|
println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1))
|
||||||
|
println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n")
|
@ -41,3 +41,25 @@ I will have to work out formulas for the Euclidean distance between two entities
|
|||||||
In this vein, it seems as though if J1 and J2 are the reps of two points, then Q(J1,J2) = d^2/2. So then the sphere centered at J1 through J2 is (J1-(2Q(J1,J2),0,0,0,0))/sqrt(2Q(J1,J2)). Ugh has a sqrt in it. Similarly for sphere centered at J3 through J2, (J3-(2Q(J3,J2),0000))/sqrt(2Q(J3,J2)). J1,J2,J3 are collinear if these spheres are tangent, i.e. if those vectors have Q-inner-product 1, which is to say Q(J1,J3) - Q(J1,J2) - Q(J3,J2) = 2sqrt(Q(J1,J2)Q(J2,J3)). But maybe that's not the simplest way of putting it. After all, we can just say that the cross-product of the two differences is 0; that has no square roots in it.
|
In this vein, it seems as though if J1 and J2 are the reps of two points, then Q(J1,J2) = d^2/2. So then the sphere centered at J1 through J2 is (J1-(2Q(J1,J2),0,0,0,0))/sqrt(2Q(J1,J2)). Ugh has a sqrt in it. Similarly for sphere centered at J3 through J2, (J3-(2Q(J3,J2),0000))/sqrt(2Q(J3,J2)). J1,J2,J3 are collinear if these spheres are tangent, i.e. if those vectors have Q-inner-product 1, which is to say Q(J1,J3) - Q(J1,J2) - Q(J3,J2) = 2sqrt(Q(J1,J2)Q(J2,J3)). But maybe that's not the simplest way of putting it. After all, we can just say that the cross-product of the two differences is 0; that has no square roots in it.
|
||||||
|
|
||||||
One conceivable way to canonicalize lines is to use the *perpendicular* plane that goes through the origin, that's uniquely defined, and anyway just amounts to I = (0,0,d) where d is the ordinary direction vector of the line; and a point J in that plane that the line goes through, which just amounts to J=(r^2,1,E) with Q(I,J) = 0, i.e. E\dot d = 0. It's also the point on the line closest to the origin. The reason that we don't usually use that point as the companion to the direction vector is that the resulting set of six coordinates is not homogeneous. But here that's not an issue, since we have our standard point coordinates and plane coordinates; and for a plane through the origin, only two of the direction coordinates are really free, and then we have the one dot-product relation, so only two of the point coordinates are really free, giving us the correct dimensionality of 4 for the set of lines. So in some sense this says that we could take naively as coordinates for a line the projection of the unit direction vector to the xy plane and the projection of the line's closest point to the origin to the xy plane. That doesn't seem to have any weird gimbal locks or discontinuities or anything. And with these coordinates, you can test if the point E=x,y,z is on the line (dx,dy,cx,cy) by extending (dx,dy) to d via dz = sqrt(1-dx^2 - dy^2), extending (cx,cy) to c by determining cz via d\dot c = 0, and then checking if d\cross(E-c) = 0. And you can see if two lines are parallel just by checking if they have the same direction vector, and if not, you can see if they are coplanar by projecting both of their closest points perpendicularly onto the line in the direction of the cross product of their directions, and if the projections match they are coplanar.
|
One conceivable way to canonicalize lines is to use the *perpendicular* plane that goes through the origin, that's uniquely defined, and anyway just amounts to I = (0,0,d) where d is the ordinary direction vector of the line; and a point J in that plane that the line goes through, which just amounts to J=(r^2,1,E) with Q(I,J) = 0, i.e. E\dot d = 0. It's also the point on the line closest to the origin. The reason that we don't usually use that point as the companion to the direction vector is that the resulting set of six coordinates is not homogeneous. But here that's not an issue, since we have our standard point coordinates and plane coordinates; and for a plane through the origin, only two of the direction coordinates are really free, and then we have the one dot-product relation, so only two of the point coordinates are really free, giving us the correct dimensionality of 4 for the set of lines. So in some sense this says that we could take naively as coordinates for a line the projection of the unit direction vector to the xy plane and the projection of the line's closest point to the origin to the xy plane. That doesn't seem to have any weird gimbal locks or discontinuities or anything. And with these coordinates, you can test if the point E=x,y,z is on the line (dx,dy,cx,cy) by extending (dx,dy) to d via dz = sqrt(1-dx^2 - dy^2), extending (cx,cy) to c by determining cz via d\dot c = 0, and then checking if d\cross(E-c) = 0. And you can see if two lines are parallel just by checking if they have the same direction vector, and if not, you can see if they are coplanar by projecting both of their closest points perpendicularly onto the line in the direction of the cross product of their directions, and if the projections match they are coplanar.
|
||||||
|
|
||||||
|
#### Engine Conventions
|
||||||
|
|
||||||
|
The coordinate conventions used in the engine are different from the ones used in these notes. Marking the engine vectors and coordinates with $'$, we have
|
||||||
|
$$I' = (x', y', z', b', c'),$$
|
||||||
|
where
|
||||||
|
$$
|
||||||
|
\begin{align*}
|
||||||
|
x' & = x & b' & = b/2 \\
|
||||||
|
y' & = y & c' & = c/2. \\
|
||||||
|
z' & = z
|
||||||
|
\end{align*}
|
||||||
|
$$
|
||||||
|
The engine uses the quadratic form $Q' = -Q$, which is expressed in engine coordinates as
|
||||||
|
$$Q'(I'_1, I'_2) = x'_1 x'_2 + y'_1 y'_2 + z'_1 z'_2 - 2(b'_1c'_2 + c'_1 b'_2).$$
|
||||||
|
In the `engine` module, the matrix of $Q'$ is encoded in the lazy static variable `Q`.
|
||||||
|
|
||||||
|
In the engine's coordinate conventions, a sphere with radius $r > 0$ centered on $P = (P_x, P_y, P_z)$ is represented by the vector
|
||||||
|
$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$
|
||||||
|
which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector
|
||||||
|
$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$
|
||||||
|
In the `engine` module, these formulas are encoded in the `sphere` and `point` functions.
|
Loading…
Reference in New Issue
Block a user