Compare commits

..

9 commits

Author SHA1 Message Date
Aaron Fenyes
5233d8eb93 Move the components into their own module
All checks were successful
/ test (pull_request) Successful in 3m36s
This makes the module tree more reflective of module use patterns. In
particular, it restores the condition that every top-level module is
used in top-level code, which was broken by the addition of the
`test_assembly_chooser` module in commit 91e4e1f.
2025-07-22 13:28:48 -07:00
Aaron Fenyes
91e4e1f414 Make the test assembly chooser its own component
All checks were successful
/ test (pull_request) Successful in 3m32s
2025-07-21 12:16:16 -07:00
Aaron Fenyes
4e5cd6e4a4 Add a dodecahedral circle packing test assembly
All checks were successful
/ test (pull_request) Successful in 3m33s
2025-07-20 23:27:10 -07:00
Aaron Fenyes
97d61e154a Add a tridiminished icosahedron test assembly 2025-07-20 23:27:10 -07:00
Aaron Fenyes
9c22eebb46 Add an off-center test assembly
This test assembly should provide a simple example where the loss
function is saddle-shaped near its manifold of minima.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
27a8cbfd69 Add a balanced test assembly
This test assembly reveals one way that the engine can stall, indicated
by a long plateau in the loss history. You can make the plateau even
longer by shrinking the inner spheres.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
fae3f4531e Add an Irisawa hexlet test assembly
This partial setup of the Irisawa hexlet problem is from the summer 2025
tech demo.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
543bc4020f Add a tetrahedron radius ratio test assembly
This partial setup of the tetrahedron radius ratio problem is from the
summer 2025 tech demo.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
40d665d8ac Pause realization while loading assemblies
This avoids redundant realizations as we set an assembly's regulators
during loading. Adding some regulators to the low-curvature assembly
confirms that the feature is working as intended.
2025-07-20 23:27:10 -07:00
30 changed files with 839 additions and 1098 deletions

View file

@ -10,7 +10,7 @@ runs:
using: "composite" using: "composite"
steps: steps:
- run: rustup target add wasm32-unknown-unknown - run: rustup target add wasm32-unknown-unknown
# install the Trunk binary to `ci-bin` within the workspace directory, which # install the Trunk binary to `ci-bin` within the workspace directory, which
# is determined by the `github.workspace` label and reflected in the # is determined by the `github.workspace` label and reflected in the
# `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command # `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command

View file

@ -24,6 +24,6 @@ jobs:
# workspace directory (action variable `github.workspace`, environment # workspace directory (action variable `github.workspace`, environment
# variable `$GITHUB_WORKSPACE`): # variable `$GITHUB_WORKSPACE`):
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/setup-trunk - uses: ./.forgejo/setup-trunk
- run: RUSTFLAGS='-D warnings' cargo test - run: RUSTFLAGS='-D warnings' cargo test

View file

@ -12,11 +12,11 @@ Note that currently this is just the barest beginnings of the project, more of a
### Implementation goals ### Implementation goals
* Provide a comfortable, intuitive UI * Comfortable, intuitive UI
* Allow execution in browser (so implemented in WASM-compatible language) * Able to run in browser (so implemented in WASM-compatible language)
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well * Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well.
## Prototype ## Prototype
@ -24,66 +24,44 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
### Install the prerequisites ### Install the prerequisites
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager. 1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager
- It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup). * It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup)
2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain". 2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain"
- If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you. * If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you
3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html). 3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html)
4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/). 4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/)
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool. 5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool
- In the future, `trunk` can be updated with the same command. (You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`.)
6. Add the `.cargo/bin` folder in your home directory to your executable search path 6. Add the `.cargo/bin` folder in your home directory to your executable search path
- This lets you call Trunk, and other tools installed by Cargo, without specifying their paths. * This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
- On POSIX systems, the search path is stored in the `PATH` environment variable. * On POSIX systems, the search path is stored in the `PATH` environment variable
- Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk`.
### Play with the prototype ### Play with the prototype
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype. 1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype
- The crates the prototype depends on will be downloaded and served automatically. * *The crates the prototype depends on will be downloaded and served automatically*
- For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag. * *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag*
- If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead. * *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead.
3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`. 3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`
- Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype. * *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype*
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype. 4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
### Run the engine on some example problems ### Run the engine on some example problems
1. Use `sh` to run the script `tools/run-examples.sh`. 1. Go into the `app-proto` folder
- The script is location-independent, so you can do this from anywhere in the dyna3 repository. 2. Call `./run-examples`
- The call from the top level of the repository is: * *For each example problem, the engine will print the value of the loss function at each optimization step*
* *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then*
```bash
sh tools/run-examples.sh
```
- For each example problem, the engine will print the value of the loss function at each optimization step.
- The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then execute
```julia ```julia
include("irisawa-hexlet.jl") include("irisawa-hexlet.jl")
for (step, scaled_loss) in enumerate(history_alt.scaled_loss) for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
println(rpad(step-1, 4), " | ", scaled_loss) println(rpad(step-1, 4), " | ", scaled_loss)
end end
``` ```
you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show. *you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show*
### Run the automated tests ### Run the automated tests
1. Go into the `app-proto` folder. 1. Go into the `app-proto` folder
2. Call `cargo test`. 2. Call `cargo test`
### Deploy the prototype
1. From the `app-proto` folder, call `trunk build --release`.
- Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build.
- If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead.
2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`.
- The script is location-independent, so you can do this from anywhere in the dyna3 repository.
- The call from the top level of the repository is:
```bash
sh tools/package-for-deployment.sh
```
- This will overwrite or replace the files in `deploy/dyna3`.
3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from.
- To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path.

21
app-proto/Cargo.lock generated
View file

@ -255,7 +255,6 @@ dependencies = [
"charming", "charming",
"console_error_panic_hook", "console_error_panic_hook",
"dyna3", "dyna3",
"enum-iterator",
"itertools", "itertools",
"js-sys", "js-sys",
"lazy_static", "lazy_static",
@ -272,26 +271,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "enum-iterator"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"

View file

@ -10,7 +10,6 @@ default = ["console_error_panic_hook"]
dev = [] dev = []
[dependencies] [dependencies]
enum-iterator = "2.3.0"
itertools = "0.13.0" itertools = "0.13.0"
js-sys = "0.3.70" js-sys = "0.3.70"
lazy_static = "1.5.0" lazy_static = "1.5.0"

View file

@ -1,2 +0,0 @@
[build]
public_url = "./"

View file

@ -15,9 +15,9 @@ fn main() {
for k in 4..9 { for k in 4..9 {
println!(" {} sun", 1.0 / config[(3, k)]); println!(" {} sun", 1.0 / config[(3, k)]);
} }
// print the completed Gram matrix // print the completed Gram matrix
print::gram_matrix(&config); print::gram_matrix(&config);
} }
print::loss_history(&realization.history); print::loss_history(&realization.history);
} }

View file

@ -14,7 +14,7 @@ fn main() {
// print the completed Gram matrix and the realized configuration // print the completed Gram matrix and the realized configuration
print::gram_matrix(&config); print::gram_matrix(&config);
print::config(&config); print::config(&config);
// find the kaleidocycle's twist motion by projecting onto the tangent // find the kaleidocycle's twist motion by projecting onto the tangent
// space // space
const N_POINTS: usize = 12; const N_POINTS: usize = 12;
@ -23,10 +23,10 @@ fn main() {
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|n| [ |n| [
tangent.proj(&up.as_view(), n), tangent.proj(&up.as_view(), n),
tangent.proj(&down.as_view(), n+1), tangent.proj(&down.as_view(), n+1)
] ]
).sum(); ).sum();
let normalization = 5.0 / twist_motion[(2, 0)]; let normalization = 5.0 / twist_motion[(2, 0)];
println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end());
} }
} }

View file

@ -6,7 +6,7 @@ use dyna3::engine::{
realize_gram, realize_gram,
sphere, sphere,
ConfigNeighborhood, ConfigNeighborhood,
ConstraintProblem, ConstraintProblem
}; };
fn main() { fn main() {
@ -25,7 +25,7 @@ fn main() {
); );
print::title("Point on a sphere"); print::title("Point on a sphere");
print::realization_diagnostics(&realization); print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood { config, .. }) = realization.result { if let Ok(ConfigNeighborhood{ config, .. }) = realization.result {
print::gram_matrix(&config); print::gram_matrix(&config);
print::config(&config); print::config(&config);
} }

View file

@ -5,7 +5,7 @@ use dyna3::engine::{
realize_gram, realize_gram,
sphere, sphere,
ConfigNeighborhood, ConfigNeighborhood,
ConstraintProblem, ConstraintProblem
}; };
fn main() { fn main() {
@ -14,7 +14,7 @@ fn main() {
&[ &[
sphere(1.0, 0.0, 0.0, 1.0), 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),
sphere(-0.5, -a, 0.0, 1.0), sphere(-0.5, -a, 0.0, 1.0)
] ]
}); });
for j in 0..3 { for j in 0..3 {
@ -27,7 +27,7 @@ fn main() {
); );
print::title("Three spheres"); print::title("Three spheres");
print::realization_diagnostics(&realization); print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood { config, .. }) = realization.result { if let Ok(ConfigNeighborhood{ config, .. }) = realization.result {
print::gram_matrix(&config); print::gram_matrix(&config);
} }
print::loss_history(&realization.history); print::loss_history(&realization.history);

View file

@ -6,7 +6,7 @@
<link data-trunk rel="css" href="main.css"/> <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=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"> <link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
<!-- <!--
the Charming visualization crate, which we use to show engine diagnostics, the Charming visualization crate, which we use to show engine diagnostics,
depends the ECharts JavaScript package depends the ECharts JavaScript package

View file

@ -184,7 +184,6 @@ details[open]:has(li) .element-switch::after {
#diagnostics-bar { #diagnostics-bar {
display: flex; display: flex;
gap: 8px;
} }
#realization-status { #realization-status {
@ -208,14 +207,6 @@ details[open]:has(li) .element-switch::after {
content: '⚠'; content: '⚠';
} }
#step-input > label {
padding-right: 4px;
}
#step-input > input {
width: 45px;
}
.diagnostics-panel { .diagnostics-panel {
margin-top: 10px; margin-top: 10px;
min-height: 180px; min-height: 180px;

View file

@ -8,7 +8,7 @@
# the application prototype # the application prototype
# find the manifest file for the application prototype # find the manifest file for the application prototype
MANIFEST="$(dirname -- $0)/../app-proto/Cargo.toml" MANIFEST="$(dirname -- $0)/Cargo.toml"
# set up the command that runs each example # set up the command that runs each example
RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example"

View file

@ -1,14 +1,13 @@
use enum_iterator::{all, Sequence};
use nalgebra::{DMatrix, DVector, DVectorView}; use nalgebra::{DMatrix, DVector, DVectorView};
use std::{ use std::{
cell::Cell, cell::Cell,
cmp::Ordering,
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
cmp::Ordering,
fmt, fmt,
fmt::{Debug, Display, Formatter}, fmt::{Debug, Formatter},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
rc::Rc, rc::Rc,
sync::{atomic, atomic::AtomicU64}, sync::{atomic, atomic::AtomicU64}
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
@ -17,6 +16,7 @@ use crate::{
components::{display::DisplayItem, outline::OutlineItem}, components::{display::DisplayItem, outline::OutlineItem},
engine::{ engine::{
Q, Q,
change_half_curvature,
local_unif_to_std, local_unif_to_std,
point, point,
project_point_to_normalized, project_point_to_normalized,
@ -27,10 +27,9 @@ use crate::{
ConfigSubspace, ConfigSubspace,
ConstraintProblem, ConstraintProblem,
DescentHistory, DescentHistory,
MatrixEntry, Realization
Realization,
}, },
specified::SpecifiedValue, specified::SpecifiedValue
}; };
pub type ElementColor = [f32; 3]; pub type ElementColor = [f32; 3];
@ -45,7 +44,7 @@ static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);
pub trait Serial { pub trait Serial {
// a serial number that uniquely identifies this element // a serial number that uniquely identifies this element
fn serial(&self) -> u64; fn serial(&self) -> u64;
// take the next serial number, panicking if that was the last one left // take the next serial number, panicking if that was the last one left
fn next_serial() -> u64 where Self: Sized { fn next_serial() -> u64 where Self: Sized {
// the technique we use to panic on overflow is taken from _Rust Atomics // the technique we use to panic on overflow is taken from _Rust Atomics
@ -86,14 +85,6 @@ impl Ord for dyn Serial {
} }
} }
// Small helper function to generate consistent errors when there
// are indexing issues in a ProblemPoser
fn indexing_error(item: &str, name: &str, actor: &str) -> String {
format!(
"{item} \"{name}\" must be indexed before {actor} writes problem data"
)
}
pub trait ProblemPoser { pub trait ProblemPoser {
fn pose(&self, problem: &mut ConstraintProblem); fn pose(&self, problem: &mut ConstraintProblem);
} }
@ -101,33 +92,33 @@ pub trait ProblemPoser {
pub trait Element: Serial + ProblemPoser + DisplayItem { pub trait Element: Serial + ProblemPoser + DisplayItem {
// the default identifier for an element of this type // the default identifier for an element of this type
fn default_id() -> String where Self: Sized; fn default_id() -> String where Self: Sized;
// the default example of an element of this type // the default example of an element of this type
fn default(id: String, id_num: u64) -> Self where Self: Sized; fn default(id: String, id_num: u64) -> Self where Self: Sized;
// the default regulators that come with this element // the default regulators that come with this element
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> { fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
Vec::new() Vec::new()
} }
fn id(&self) -> &String; fn id(&self) -> &String;
fn label(&self) -> &String; fn label(&self) -> &String;
fn representation(&self) -> Signal<DVector<f64>>; fn representation(&self) -> Signal<DVector<f64>>;
fn ghost(&self) -> Signal<bool>; fn ghost(&self) -> Signal<bool>;
// the regulators the element is subject to. the assembly that owns the // the regulators the element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date // element is responsible for keeping this set up to date
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>; fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
// project a representation vector for this kind of element onto its // project a representation vector for this kind of element onto its
// normalization variety // normalization variety
fn project_to_normalized(&self, rep: &mut DVector<f64>); fn project_to_normalized(&self, rep: &mut DVector<f64>);
// the configuration matrix column index that was assigned to the element // the configuration matrix column index that was assigned to the element
// last time the assembly was realized, or `None` if the element has never // last time the assembly was realized, or `None` if the element has never
// been through a realization // been through a realization
fn column_index(&self) -> Option<usize>; fn column_index(&self) -> Option<usize>;
// assign the element a configuration matrix column index. this method must // assign the element a configuration matrix column index. this method must
// be used carefully to preserve invariant (1), described in the comment on // be used carefully to preserve invariant (1), described in the comment on
// the `tangent` field of the `Assembly` structure // the `tangent` field of the `Assembly` structure
@ -135,8 +126,8 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
} }
impl Debug for dyn Element { impl Debug for dyn Element {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
Debug::fmt(&self.id(), f) self.id().fmt(f)
} }
} }
@ -174,27 +165,27 @@ pub struct Sphere {
pub ghost: Signal<bool>, pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>>, column_index: Cell<Option<usize>>
} }
impl Sphere { impl Sphere {
const CURVATURE_COMPONENT: usize = 3; const CURVATURE_COMPONENT: usize = 3;
pub fn new( pub fn new(
id: String, id: String,
label: String, label: String,
color: ElementColor, color: ElementColor,
representation: DVector<f64>, representation: DVector<f64>
) -> Self { ) -> Sphere {
Self { Sphere {
id, id: id,
label, label: label,
color, color: color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false), ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into(), column_index: None.into()
} }
} }
} }
@ -203,48 +194,48 @@ impl Element for Sphere {
fn default_id() -> String { fn default_id() -> String {
"sphere".to_string() "sphere".to_string()
} }
fn default(id: String, id_num: u64) -> Self { fn default(id: String, id_num: u64) -> Sphere {
Self::new( Sphere::new(
id, id,
format!("Sphere {id_num}"), format!("Sphere {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
sphere(0.0, 0.0, 0.0, 1.0), sphere(0.0, 0.0, 0.0, 1.0)
) )
} }
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> { fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
vec![Rc::new(HalfCurvatureRegulator::new(self))] vec![Rc::new(HalfCurvatureRegulator::new(self))]
} }
fn id(&self) -> &String { fn id(&self) -> &String {
&self.id &self.id
} }
fn label(&self) -> &String { fn label(&self) -> &String {
&self.label &self.label
} }
fn representation(&self) -> Signal<DVector<f64>> { fn representation(&self) -> Signal<DVector<f64>> {
self.representation self.representation
} }
fn ghost(&self) -> Signal<bool> { fn ghost(&self) -> Signal<bool> {
self.ghost self.ghost
} }
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> { fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators self.regulators
} }
fn project_to_normalized(&self, rep: &mut DVector<f64>) { fn project_to_normalized(&self, rep: &mut DVector<f64>) {
project_sphere_to_normalized(rep); project_sphere_to_normalized(rep);
} }
fn column_index(&self) -> Option<usize> { fn column_index(&self) -> Option<usize> {
self.column_index.get() self.column_index.get()
} }
fn set_column_index(&self, index: usize) { fn set_column_index(&self, index: usize) {
self.column_index.set(Some(index)); self.column_index.set(Some(index));
} }
@ -259,7 +250,8 @@ impl Serial for Sphere {
impl ProblemPoser for Sphere { impl ProblemPoser for Sphere {
fn pose(&self, problem: &mut ConstraintProblem) { fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect( let index = self.column_index().expect(
indexing_error("Sphere", &self.id, "it").as_str()); format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
);
problem.gram.push_sym(index, index, 1.0); problem.gram.push_sym(index, index, 1.0);
problem.guess.set_column(index, &self.representation.get_clone_untracked()); problem.guess.set_column(index, &self.representation.get_clone_untracked());
} }
@ -273,20 +265,19 @@ pub struct Point {
pub ghost: Signal<bool>, pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>>, column_index: Cell<Option<usize>>
} }
impl Point { impl Point {
const WEIGHT_COMPONENT: usize = 3; const WEIGHT_COMPONENT: usize = 3;
const NORM_COMPONENT: usize = 4;
pub fn new( pub fn new(
id: String, id: String,
label: String, label: String,
color: ElementColor, color: ElementColor,
representation: DVector<f64>, representation: DVector<f64>
) -> Self { ) -> Point {
Self { Point {
id, id,
label, label,
color, color,
@ -294,7 +285,7 @@ impl Point {
ghost: create_signal(false), ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into(), column_index: None.into()
} }
} }
} }
@ -303,53 +294,44 @@ impl Element for Point {
fn default_id() -> String { fn default_id() -> String {
"point".to_string() "point".to_string()
} }
fn default(id: String, id_num: u64) -> Self { fn default(id: String, id_num: u64) -> Point {
Self::new( Point::new(
id, id,
format!("Point {id_num}"), format!("Point {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
point(0.0, 0.0, 0.0), point(0.0, 0.0, 0.0)
) )
} }
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
all::<Axis>()
.map(|axis| {
Rc::new(PointCoordinateRegulator::new(self.clone(), axis))
as Rc::<dyn Regulator>
})
.collect()
}
fn id(&self) -> &String { fn id(&self) -> &String {
&self.id &self.id
} }
fn label(&self) -> &String { fn label(&self) -> &String {
&self.label &self.label
} }
fn representation(&self) -> Signal<DVector<f64>> { fn representation(&self) -> Signal<DVector<f64>> {
self.representation self.representation
} }
fn ghost(&self) -> Signal<bool> { fn ghost(&self) -> Signal<bool> {
self.ghost self.ghost
} }
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> { fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators self.regulators
} }
fn project_to_normalized(&self, rep: &mut DVector<f64>) { fn project_to_normalized(&self, rep: &mut DVector<f64>) {
project_point_to_normalized(rep); project_point_to_normalized(rep);
} }
fn column_index(&self) -> Option<usize> { fn column_index(&self) -> Option<usize> {
self.column_index.get() self.column_index.get()
} }
fn set_column_index(&self, index: usize) { fn set_column_index(&self, index: usize) {
self.column_index.set(Some(index)); self.column_index.set(Some(index));
} }
@ -364,9 +346,10 @@ impl Serial for Point {
impl ProblemPoser for Point { impl ProblemPoser for Point {
fn pose(&self, problem: &mut ConstraintProblem) { fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect( let index = self.column_index().expect(
indexing_error("Point", &self.id, "it").as_str()); format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
);
problem.gram.push_sym(index, index, 0.0); problem.gram.push_sym(index, index, 0.0);
problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5);
problem.guess.set_column(index, &self.representation.get_clone_untracked()); problem.guess.set_column(index, &self.representation.get_clone_untracked());
} }
} }
@ -375,6 +358,16 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>; fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>; fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>; fn set_point(&self) -> Signal<SpecifiedValue>;
// this method is used to responsively precondition the assembly for
// realization when the regulator becomes a constraint, or is edited while
// acting as a constraint. it should track the set point, do any desired
// preconditioning when the set point is present, and use its return value
// to report whether the set is present. the default implementation does no
// preconditioning
fn try_activate(&self) -> bool {
self.set_point().with(|set_pt| set_pt.is_present())
}
} }
impl Hash for dyn Regulator { impl Hash for dyn Regulator {
@ -407,11 +400,11 @@ pub struct InversiveDistanceRegulator {
pub subjects: [Rc<dyn Element>; 2], pub subjects: [Rc<dyn Element>; 2],
pub measurement: ReadSignal<f64>, pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>, pub set_point: Signal<SpecifiedValue>,
serial: u64, serial: u64
} }
impl InversiveDistanceRegulator { impl InversiveDistanceRegulator {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self { pub fn new(subjects: [Rc<dyn Element>; 2]) -> InversiveDistanceRegulator {
let representations = subjects.each_ref().map(|subj| subj.representation()); let representations = subjects.each_ref().map(|subj| subj.representation());
let measurement = create_memo(move || { let measurement = create_memo(move || {
representations[0].with(|rep_0| representations[0].with(|rep_0|
@ -420,11 +413,11 @@ impl InversiveDistanceRegulator {
) )
) )
}); });
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial(); let serial = Self::next_serial();
Self { subjects, measurement, set_point, serial } InversiveDistanceRegulator { subjects, measurement, set_point, serial }
} }
} }
@ -432,11 +425,11 @@ impl Regulator for InversiveDistanceRegulator {
fn subjects(&self) -> Vec<Rc<dyn Element>> { fn subjects(&self) -> Vec<Rc<dyn Element>> {
self.subjects.clone().into() self.subjects.clone().into()
} }
fn measurement(&self) -> ReadSignal<f64> { fn measurement(&self) -> ReadSignal<f64> {
self.measurement self.measurement
} }
fn set_point(&self) -> Signal<SpecifiedValue> { fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point self.set_point
} }
@ -454,8 +447,8 @@ impl ProblemPoser for InversiveDistanceRegulator {
if let Some(val) = set_pt.value { if let Some(val) = set_pt.value {
let [row, col] = self.subjects.each_ref().map( let [row, col] = self.subjects.each_ref().map(
|subj| subj.column_index().expect( |subj| subj.column_index().expect(
indexing_error("Subject", subj.id(), "Subjects should be indexed before inversive distance regulator writes problem data"
"inversive distance regulator").as_str()) )
); );
problem.gram.push_sym(row, col, val); problem.gram.push_sym(row, col, val);
} }
@ -464,22 +457,22 @@ impl ProblemPoser for InversiveDistanceRegulator {
} }
pub struct HalfCurvatureRegulator { pub struct HalfCurvatureRegulator {
pub subject: Rc<Sphere>, pub subject: Rc<dyn Element>,
pub measurement: ReadSignal<f64>, pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>, pub set_point: Signal<SpecifiedValue>,
serial: u64, serial: u64
} }
impl HalfCurvatureRegulator { impl HalfCurvatureRegulator {
pub fn new(subject: Rc<Sphere>) -> Self { pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator {
let measurement = subject.representation().map( let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT] |rep| rep[Sphere::CURVATURE_COMPONENT]
); );
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial(); let serial = Self::next_serial();
Self { subject, measurement, set_point, serial } HalfCurvatureRegulator { subject, measurement, set_point, serial }
} }
} }
@ -487,14 +480,26 @@ impl Regulator for HalfCurvatureRegulator {
fn subjects(&self) -> Vec<Rc<dyn Element>> { fn subjects(&self) -> Vec<Rc<dyn Element>> {
vec![self.subject.clone()] vec![self.subject.clone()]
} }
fn measurement(&self) -> ReadSignal<f64> { fn measurement(&self) -> ReadSignal<f64> {
self.measurement self.measurement
} }
fn set_point(&self) -> Signal<SpecifiedValue> { fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point self.set_point
} }
fn try_activate(&self) -> bool {
match self.set_point.with(|set_pt| set_pt.value) {
Some(half_curv) => {
self.subject.representation().update(
|rep| change_half_curvature(rep, half_curv)
);
true
}
None => false
}
}
} }
impl Serial for HalfCurvatureRegulator { impl Serial for HalfCurvatureRegulator {
@ -508,88 +513,18 @@ impl ProblemPoser for HalfCurvatureRegulator {
self.set_point.with_untracked(|set_pt| { self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value { if let Some(val) = set_pt.value {
let col = self.subject.column_index().expect( let col = self.subject.column_index().expect(
indexing_error("Subject", &self.subject.id, "Subject should be indexed before half-curvature regulator writes problem data"
"half-curvature regulator").as_str()); );
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
} }
}); });
} }
} }
#[derive(Clone, Copy, Sequence)]
pub enum Axis { X = 0, Y = 1, Z = 2 }
impl Axis {
fn name(&self) -> &'static str {
match self { Axis::X => "X", Axis::Y => "Y", Axis::Z => "Z" }
}
}
impl Display for Axis {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
pub struct PointCoordinateRegulator {
pub subject: Rc<Point>,
pub axis: Axis,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
serial: u64
}
impl PointCoordinateRegulator {
pub fn new(subject: Rc<Point>, axis: Axis) -> Self {
let measurement = subject.representation().map(
move |rep| rep[axis as usize]
);
let set_point = create_signal(SpecifiedValue::from_empty_spec());
Self { subject, axis, measurement, set_point, serial: Self::next_serial() }
}
}
impl Serial for PointCoordinateRegulator {
fn serial(&self) -> u64 { self.serial }
}
impl Regulator for PointCoordinateRegulator {
fn subjects(&self) -> Vec<Rc<dyn Element>> { vec![self.subject.clone()] }
fn measurement(&self) -> ReadSignal<f64> { self.measurement }
fn set_point(&self) -> Signal<SpecifiedValue> { self.set_point }
}
impl ProblemPoser for PointCoordinateRegulator {
fn pose(&self, problem: &mut ConstraintProblem) {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let col = self.subject.column_index().expect(
indexing_error("Subject", &self.subject.id,
"point-coordinate regulator").as_str());
problem.frozen.push(self.axis as usize, col, val);
// If all three of the subject's spatial coordinates have been
// frozen, then freeze its norm component:
let mut coords = [0.0; Axis::CARDINALITY];
let mut nset: usize = 0;
for &MatrixEntry {index, value} in &(problem.frozen) {
if index.1 == col && index.0 < Axis::CARDINALITY {
nset += 1;
coords[index.0] = value
}
}
if nset == Axis::CARDINALITY {
let [x, y, z] = coords;
problem.frozen.push(
Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]);
}
}
});
}
}
// the velocity is expressed in uniform coordinates // the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> { pub struct ElementMotion<'a> {
pub element: Rc<dyn Element>, pub element: Rc<dyn Element>,
pub velocity: DVectorView<'a, f64>, pub velocity: DVectorView<'a, f64>
} }
type AssemblyMotion<'a> = Vec<ElementMotion<'a>>; type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
@ -600,7 +535,7 @@ pub struct Assembly {
// elements and regulators // elements and regulators
pub elements: Signal<BTreeSet<Rc<dyn Element>>>, pub elements: Signal<BTreeSet<Rc<dyn Element>>>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
// solution variety tangent space. the basis vectors are stored in // solution variety tangent space. the basis vectors are stored in
// configuration matrix format, ordered according to the elements' column // configuration matrix format, ordered according to the elements' column
// indices. when you realize the assembly, every element that's present // indices. when you realize the assembly, every element that's present
@ -612,17 +547,17 @@ pub struct Assembly {
// in that column of the tangent space basis matrices // in that column of the tangent space basis matrices
// //
pub tangent: Signal<ConfigSubspace>, pub tangent: Signal<ConfigSubspace>,
// indexing // indexing
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>, pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization control // realization control
pub realization_trigger: Signal<()>, pub keep_realized: Signal<bool>,
pub needs_realization: Signal<bool>,
// realization diagnostics // realization diagnostics
pub realization_status: Signal<Result<(), String>>, pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>, pub descent_history: Signal<DescentHistory>
pub step: Signal<SpecifiedValue>,
} }
impl Assembly { impl Assembly {
@ -633,43 +568,28 @@ impl Assembly {
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)), tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(BTreeMap::default()), elements_by_id: create_signal(BTreeMap::default()),
realization_trigger: create_signal(()), keep_realized: create_signal(true),
needs_realization: create_signal(false),
realization_status: create_signal(Ok(())), realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new()), descent_history: create_signal(DescentHistory::new())
step: create_signal(SpecifiedValue::from_empty_spec()),
}; };
// realize the assembly whenever the element list, the regulator list, // realize the assembly whenever it becomes simultaneously true that
// a regulator's set point, or the realization trigger is updated // we're trying to keep it realized and it needs realization
let assembly_for_realization = assembly.clone(); let assembly_for_effect = assembly.clone();
create_effect(move || { create_effect(move || {
assembly_for_realization.elements.track(); let should_realize = assembly_for_effect.keep_realized.get()
assembly_for_realization.regulators.with( && assembly_for_effect.needs_realization.get();
|regs| for reg in regs { if should_realize {
reg.set_point().track(); assembly_for_effect.realize();
}
);
assembly_for_realization.realization_trigger.track();
assembly_for_realization.realize();
});
// load a configuration from the descent history whenever the active
// step is updated
let assembly_for_step_selection = assembly.clone();
create_effect(move || {
if let Some(step) = assembly.step.with(|n| n.value) {
let config = assembly.descent_history.with_untracked(
|history| history.config[step as usize].clone()
);
assembly_for_step_selection.load_config(&config)
} }
}); });
assembly assembly
} }
// --- inserting elements and regulators --- // --- inserting elements and regulators ---
// insert an element into the assembly without checking whether we already // insert an element into the assembly without checking whether we already
// have an element with the same identifier. any element that does have the // 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 // same identifier will get kicked out of the `elements_by_id` index
@ -679,13 +599,13 @@ impl Assembly {
let elt_rc = Rc::new(elt); let elt_rc = Rc::new(elt);
self.elements.update(|elts| elts.insert(elt_rc.clone())); self.elements.update(|elts| elts.insert(elt_rc.clone()));
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
// create and insert the element's default regulators // create and insert the element's default regulators
for reg in elt_rc.default_regulators() { for reg in elt_rc.default_regulators() {
self.insert_regulator(reg); self.insert_regulator(reg);
} }
} }
pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
let can_insert = self.elements_by_id.with_untracked( let can_insert = self.elements_by_id.with_untracked(
|elts_by_id| !elts_by_id.contains_key(elt.id()) |elts_by_id| !elts_by_id.contains_key(elt.id())
@ -695,7 +615,7 @@ impl Assembly {
} }
can_insert can_insert
} }
pub fn insert_element_default<T: Element + 'static>(&self) { pub fn insert_element_default<T: Element + 'static>(&self) {
// find the next unused identifier in the default sequence // find the next unused identifier in the default sequence
let default_id = T::default_id(); let default_id = T::default_id();
@ -707,17 +627,17 @@ impl Assembly {
id_num += 1; id_num += 1;
id = format!("{default_id}{id_num}"); id = format!("{default_id}{id_num}");
} }
// create and insert the default example of `T` // create and insert the default example of `T`
let _ = self.insert_element_unchecked(T::default(id, id_num)); let _ = self.insert_element_unchecked(T::default(id, id_num));
} }
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) { pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
// add the regulator to the assembly's regulator list // add the regulator to the assembly's regulator list
self.regulators.update( self.regulators.update(
|regs| regs.insert(regulator.clone()) |regs| regs.insert(regulator.clone())
); );
// add the regulator to each subject's regulator list // add the regulator to each subject's regulator list
let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( let subject_regulators: Vec<_> = regulator.subjects().into_iter().map(
|subj| subj.regulators() |subj| subj.regulators()
@ -725,7 +645,20 @@ impl Assembly {
for regulators in subject_regulators { for regulators in subject_regulators {
regulators.update(|regs| regs.insert(regulator.clone())); regulators.update(|regs| regs.insert(regulator.clone()));
} }
// request a realization when the regulator becomes a constraint, or is
// edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || {
/* DEBUG */
// log the regulator update
console_log!("Updated regulator with subjects {:?}", regulator.subjects());
if regulator.try_activate() {
self_for_effect.needs_realization.set(true);
}
});
/* DEBUG */ /* DEBUG */
// print an updated list of regulators // print an updated list of regulators
console_log!("Regulators:"); console_log!("Regulators:");
@ -748,19 +681,9 @@ impl Assembly {
} }
}); });
} }
// --- updating the configuration ---
pub fn load_config(&self, config: &DMatrix<f64>) {
for elt in self.elements.get_clone_untracked() {
elt.representation().update(
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
);
}
}
// --- realization --- // --- realization ---
pub fn realize(&self) { pub fn realize(&self) {
// index the elements // index the elements
self.elements.update_silent(|elts| { self.elements.update_silent(|elts| {
@ -768,7 +691,7 @@ impl Assembly {
elt.set_column_index(index); elt.set_column_index(index);
} }
}); });
// set up the constraint problem // set up the constraint problem
let problem = self.elements.with_untracked(|elts| { let problem = self.elements.with_untracked(|elts| {
let mut problem = ConstraintProblem::new(elts.len()); let mut problem = ConstraintProblem::new(elts.len());
@ -782,21 +705,20 @@ impl Assembly {
}); });
problem problem
}); });
/* DEBUG */ /* DEBUG */
// log the Gram matrix // log the Gram matrix
console_log!("Gram matrix:\n{}", problem.gram); console_log!("Gram matrix:\n{}", problem.gram);
console_log!("Frozen entries:\n{}", problem.frozen);
/* DEBUG */ /* DEBUG */
// log the initial configuration matrix // log the initial configuration matrix
console_log!("Old configuration:{:>8.3}", problem.guess); console_log!("Old configuration:{:>8.3}", problem.guess);
// look for a configuration with the given Gram matrix // look for a configuration with the given Gram matrix
let Realization { result, history } = realize_gram( let Realization { result, history } = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
/* DEBUG */ /* DEBUG */
// report the outcome of the search in the browser console // report the outcome of the search in the browser console
if let Err(ref message) = result { if let Err(ref message) = result {
@ -804,52 +726,46 @@ impl Assembly {
} else { } else {
console_log!("✅️ Target accuracy achieved!"); console_log!("✅️ Target accuracy achieved!");
} }
if history.scaled_loss.len() > 0 { console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Loss: {}", history.scaled_loss.last().unwrap());
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
} // report the loss history
// report the descent history
let step_cnt = history.config.len();
self.descent_history.set(history); self.descent_history.set(history);
match result { match result {
Ok(ConfigNeighborhood { nbhd: tangent, .. }) => { Ok(ConfigNeighborhood { config, nbhd: tangent }) => {
/* DEBUG */ /* DEBUG */
// report the tangent dimension // report the tangent dimension
console_log!("Tangent dimension: {}", tangent.dim()); console_log!("Tangent dimension: {}", tangent.dim());
// report the realization status // report the realization status
self.realization_status.set(Ok(())); self.realization_status.set(Ok(()));
// display the last realization step // read out the solution
self.step.set( for elt in self.elements.get_clone_untracked() {
if step_cnt > 0 { elt.representation().update(
let last_step = step_cnt - 1; |rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
SpecifiedValue::try_from(last_step.to_string()).unwrap() );
} else { }
SpecifiedValue::from_empty_spec()
}
);
// save the tangent space // save the tangent space
self.tangent.set_silent(tangent); self.tangent.set_silent(tangent);
// clear the realization request flag
self.needs_realization.set(false);
}, },
Err(message) => { Err(message) => {
// report the realization status. the `Err(message)` we're // report the realization status. the `Err(message)` we're
// setting the status to has a different type than the // setting the status to has a different type than the
// `Err(message)` we received from the match: we're changing the // `Err(message)` we received from the match: we're changing the
// `Ok` type from `Realization` to `()` // `Ok` type from `Realization` to `()`
self.realization_status.set(Err(message)); self.realization_status.set(Err(message))
}
// display the initial guess
self.step.set(SpecifiedValue::from(Some(0.0)));
},
} }
} }
// --- deformation --- // --- deformation ---
// project the given motion to the tangent space of the solution variety and // project the given motion to the tangent space of the solution variety and
// move the assembly along it. the implementation is based on invariant (1) // move the assembly along it. the implementation is based on invariant (1)
// from above and the following additional invariant: // from above and the following additional invariant:
@ -866,7 +782,7 @@ impl Assembly {
if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) { if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) {
console::log_1(&JsValue::from("The assembly is rigid")); console::log_1(&JsValue::from("The assembly is rigid"));
} }
// give a column index to each moving element that doesn't have one yet. // give a column index to each moving element that doesn't have one yet.
// this temporarily breaks invariant (1), but the invariant will be // this temporarily breaks invariant (1), but the invariant will be
// restored when we realize the assembly at the end of the deformation. // restored when we realize the assembly at the end of the deformation.
@ -884,7 +800,7 @@ impl Assembly {
} }
next_column_index next_column_index
}; };
// project the element motions onto the tangent space of the solution // project the element motions onto the tangent space of the solution
// variety and sum them to get a deformation of the whole assembly. the // variety and sum them to get a deformation of the whole assembly. the
// matrix `motion_proj` that holds the deformation has extra columns for // matrix `motion_proj` that holds the deformation has extra columns for
@ -895,7 +811,7 @@ impl Assembly {
// we can unwrap the column index because we know that every moving // we can unwrap the column index because we know that every moving
// element has one at this point // element has one at this point
let column_index = elt_motion.element.column_index().unwrap(); let column_index = elt_motion.element.column_index().unwrap();
if column_index < realized_dim { if column_index < realized_dim {
// this element had a column index when we started, so by // this element had a column index when we started, so by
// invariant (1), it's reflected in the tangent space // invariant (1), it's reflected in the tangent space
@ -913,7 +829,7 @@ impl Assembly {
target_column += unif_to_std * elt_motion.velocity; target_column += unif_to_std * elt_motion.velocity;
} }
} }
// step the assembly along the deformation. this changes the elements' // step the assembly along the deformation. this changes the elements'
// normalizations, so we restore those afterward // normalizations, so we restore those afterward
for elt in self.elements.get_clone_untracked() { for elt in self.elements.get_clone_untracked() {
@ -927,37 +843,35 @@ impl Assembly {
}, },
None => { None => {
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
}, }
}; };
}); });
} }
// trigger a realization to bring the configuration back onto the // request a realization to bring the configuration back onto the
// solution variety. this also gets the elements' column indices and the // solution variety. this also gets the elements' column indices and the
// saved tangent space back in sync // saved tangent space back in sync
self.realization_trigger.set(()); self.needs_realization.set(true);
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::engine; use crate::engine;
#[test] #[test]
#[should_panic(expected = #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
"Sphere \"sphere\" must be indexed before it writes problem data")]
fn unindexed_element_test() { fn unindexed_element_test() {
let _ = create_root(|| { let _ = create_root(|| {
let elt = Sphere::default("sphere".to_string(), 0); let elt = Sphere::default("sphere".to_string(), 0);
elt.pose(&mut ConstraintProblem::new(1)); elt.pose(&mut ConstraintProblem::new(1));
}); });
} }
#[test] #[test]
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \ #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() { fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| { let _ = create_root(|| {
let subjects = [0, 1].map( let subjects = [0, 1].map(
@ -972,7 +886,7 @@ mod tests {
}.pose(&mut ConstraintProblem::new(2)); }.pose(&mut ConstraintProblem::new(2));
}); });
} }
#[test] #[test]
fn curvature_drift_test() { fn curvature_drift_test() {
const INITIAL_RADIUS: f64 = 0.25; const INITIAL_RADIUS: f64 = 0.25;
@ -989,10 +903,10 @@ mod tests {
String::from(sphere_id), String::from(sphere_id),
String::from("Sphere 0"), String::from("Sphere 0"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS), engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS)
) )
); );
// nudge the sphere repeatedly along the `z` axis // nudge the sphere repeatedly along the `z` axis
const STEP_SIZE: f64 = 0.0025; const STEP_SIZE: f64 = 0.0025;
const STEP_CNT: usize = 400; const STEP_CNT: usize = 400;
@ -1003,12 +917,12 @@ mod tests {
vec![ vec![
ElementMotion { ElementMotion {
element: sphere.clone(), element: sphere.clone(),
velocity: velocity.as_view(), velocity: velocity.as_view()
} }
] ]
); );
} }
// check how much the sphere's curvature has drifted // check how much the sphere's curvature has drifted
const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS;
const DRIFT_TOL: f64 = 0.015; const DRIFT_TOL: f64 = 0.015;
@ -1018,4 +932,4 @@ mod tests {
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
}); });
} }
} }

View file

@ -4,47 +4,32 @@ use sycamore::prelude::*;
use super::test_assembly_chooser::TestAssemblyChooser; use super::test_assembly_chooser::TestAssemblyChooser;
use crate::{ use crate::{
AppState, AppState,
assembly::{InversiveDistanceRegulator, Point, Sphere}, assembly::{InversiveDistanceRegulator, Point, Sphere}
}; };
#[component] #[component]
pub fn AddRemove() -> View { pub fn AddRemove() -> View {
view! { view! {
div(id = "add-remove") { div(id="add-remove") {
button( button(
on:click = |_| { on:click=|_| {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
batch(|| { state.assembly.insert_element_default::<Sphere>();
// this call is batched to avoid redundant realizations.
// it updates the element list and the regulator list,
// which are both tracked by the realization effect
/* TO DO */
// it would make more to do the batching inside
// `insert_element_default`, but that will have to wait
// until Sycamore handles nested batches correctly.
//
// https://github.com/sycamore-rs/sycamore/issues/802
//
// the nested batch issue is relevant here because the
// assembly loaders in the test assembly chooser use
// `insert_element_default` within larger batches
state.assembly.insert_element_default::<Sphere>();
});
} }
) { "Add sphere" } ) { "Add sphere" }
button( button(
on:click = |_| { on:click=|_| {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
state.assembly.insert_element_default::<Point>(); state.assembly.insert_element_default::<Point>();
} }
) { "Add point" } ) { "Add point" }
button( button(
class = "emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
disabled = { disabled={
let state = use_context::<AppState>(); let state = use_context::<AppState>();
state.selection.with(|sel| sel.len() != 2) state.selection.with(|sel| sel.len() != 2)
}, },
on:click = |_| { on:click=|_| {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let subjects: [_; 2] = state.selection.with( let subjects: [_; 2] = state.selection.with(
// the button is only enabled when two elements are // the button is only enabled when two elements are

View file

@ -7,16 +7,18 @@ use charming::{
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::{AppState, specified::SpecifiedValue}; use crate::AppState;
#[derive(Clone)] #[derive(Clone)]
struct DiagnosticsState { struct DiagnosticsState {
active_tab: Signal<String>, active_tab: Signal<String>
} }
impl DiagnosticsState { impl DiagnosticsState {
fn new(initial_tab: String) -> Self { fn new(initial_tab: String) -> DiagnosticsState {
Self { active_tab: create_signal(initial_tab) } DiagnosticsState {
active_tab: create_signal(initial_tab)
}
} }
} }
@ -27,20 +29,20 @@ fn RealizationStatus() -> View {
let realization_status = state.assembly.realization_status; let realization_status = state.assembly.realization_status;
view! { view! {
div( div(
id = "realization-status", id="realization-status",
class = realization_status.with( class=realization_status.with(
|status| match status { |status| match status {
Ok(_) => "", Ok(_) => "",
Err(_) => "invalid", Err(_) => "invalid"
} }
) )
) { ) {
div(class = "status") div(class="status")
div { div {
(realization_status.with( (realization_status.with(
|status| match status { |status| match status {
Ok(_) => "Target accuracy achieved".to_string(), Ok(_) => "Target accuracy achieved".to_string(),
Err(message) => message.clone(), Err(message) => message.clone()
} }
)) ))
} }
@ -48,73 +50,10 @@ fn RealizationStatus() -> View {
} }
} }
// history step input
#[component]
fn StepInput() -> View {
// get the assembly
let state = use_context::<AppState>();
let assembly = state.assembly;
// the `last_step` signal holds the index of the last step
let last_step = assembly.descent_history.map(
|history| match history.config.len() {
0 => None,
n => Some(n - 1),
}
);
let input_max = last_step.map(|last| last.unwrap_or(0));
// these signals hold the entered step number
let value = create_signal(String::new());
let value_as_number = create_signal(0.0);
create_effect(move || {
value.set(assembly.step.with(|n| n.spec.clone()));
});
view! {
div(id = "step-input") {
label { "Step" }
input(
r#type = "number",
min = "0",
max = input_max.with(|max| max.to_string()),
bind:value = value,
bind:valueAsNumber = value_as_number,
on:change = move |_| {
if last_step.with(|last| last.is_some()) {
// clamp the step within its allowed range. the lower
// bound is redundant on browsers that make it
// impossible to type negative values into a number
// input with a non-negative `min`, but there's no harm
// in being careful
let step_raw = value.with(
|val| SpecifiedValue::try_from(val.clone())
.unwrap_or(SpecifiedValue::from_empty_spec()
)
);
let step = SpecifiedValue::from(
step_raw.value.map(
|val| val.clamp(0.0, input_max.get() as f64)
)
);
// set the input string and the assembly's active step
value.set(step.spec.clone());
assembly.step.set(step);
} else {
value.set(String::new());
}
},
)
}
}
}
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> { fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![ vec![
Some(step as f64), Some(step as f64),
if value == 0.0 { None } else { Some(value.abs().log10()) }, if value == 0.0 { None } else { Some(value.abs().log10()) }
] ]
} }
@ -124,7 +63,7 @@ fn LossHistory() -> View {
const CONTAINER_ID: &str = "loss-history"; const CONTAINER_ID: &str = "loss-history";
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let renderer = WasmRenderer::new_opt(None, Some(178)); let renderer = WasmRenderer::new_opt(None, Some(178));
on_mount(move || { on_mount(move || {
create_effect(move || { create_effect(move || {
// get the loss history // get the loss history
@ -136,13 +75,13 @@ fn LossHistory() -> View {
.map(into_log10_time_point) .map(into_log10_time_point)
.collect() .collect()
); );
// initialize the chart axes // initialize the chart axes
let step_axis = Axis::new() let step_axis = Axis::new()
.type_(AxisType::Category) .type_(AxisType::Category)
.boundary_gap(false); .boundary_gap(false);
let scaled_loss_axis = Axis::new(); let scaled_loss_axis = Axis::new();
// load the chart data. when there's no history, we load the data // load the chart data. when there's no history, we load the data
// point (0, None) to clear the chart. it would feel more natural to // point (0, None) to clear the chart. it would feel more natural to
// load empty data vectors, but that turns out not to clear the // load empty data vectors, but that turns out not to clear the
@ -164,9 +103,9 @@ fn LossHistory() -> View {
renderer.render(CONTAINER_ID, &chart).unwrap(); renderer.render(CONTAINER_ID, &chart).unwrap();
}); });
}); });
view! { view! {
div(id = CONTAINER_ID, class = "diagnostics-chart") div(id=CONTAINER_ID, class="diagnostics-chart")
} }
} }
@ -176,14 +115,14 @@ fn SpectrumHistory() -> View {
const CONTAINER_ID: &str = "spectrum-history"; const CONTAINER_ID: &str = "spectrum-history";
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let renderer = WasmRenderer::new(478, 178); let renderer = WasmRenderer::new(478, 178);
on_mount(move || { on_mount(move || {
create_effect(move || { create_effect(move || {
// get the spectrum of the Hessian at each step, split into its // get the spectrum of the Hessian at each step, split into its
// positive, negative, and strictly-zero parts // positive, negative, and strictly-zero parts
let ( let (
hess_eigvals_zero, hess_eigvals_zero,
hess_eigvals_nonzero, hess_eigvals_nonzero
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|history| history.hess_eigvals |history| history.hess_eigvals
.iter() .iter()
@ -204,17 +143,17 @@ fn SpectrumHistory() -> View {
.unwrap_or(1.0); .unwrap_or(1.0);
let ( let (
hess_eigvals_pos, hess_eigvals_pos,
hess_eigvals_neg, hess_eigvals_neg
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
.into_iter() .into_iter()
.partition(|&(_, val)| val > 0.0); .partition(|&(_, val)| val > 0.0);
// initialize the chart axes // initialize the chart axes
let step_axis = Axis::new() let step_axis = Axis::new()
.type_(AxisType::Category) .type_(AxisType::Category)
.boundary_gap(false); .boundary_gap(false);
let eigval_axis = Axis::new(); let eigval_axis = Axis::new();
// load the chart data. when there's no history, we load the data // load the chart data. when there's no history, we load the data
// point (0, None) to clear the chart. it would feel more natural to // point (0, None) to clear the chart. it would feel more natural to
// load empty data vectors, but that turns out not to clear the // load empty data vectors, but that turns out not to clear the
@ -270,9 +209,9 @@ fn SpectrumHistory() -> View {
renderer.render(CONTAINER_ID, &chart).unwrap(); renderer.render(CONTAINER_ID, &chart).unwrap();
}); });
}); });
view! { view! {
div(id = CONTAINER_ID, class = "diagnostics-chart") div(id=CONTAINER_ID, class="diagnostics-chart")
} }
} }
@ -281,8 +220,8 @@ fn DiagnosticsPanel(name: &'static str, children: Children) -> View {
let diagnostics_state = use_context::<DiagnosticsState>(); let diagnostics_state = use_context::<DiagnosticsState>();
view! { view! {
div( div(
class = "diagnostics-panel", class="diagnostics-panel",
"hidden" = diagnostics_state.active_tab.with( "hidden"=diagnostics_state.active_tab.with(
|active_tab| { |active_tab| {
if active_tab == name { if active_tab == name {
None None
@ -302,19 +241,18 @@ pub fn Diagnostics() -> View {
let diagnostics_state = DiagnosticsState::new("loss".to_string()); let diagnostics_state = DiagnosticsState::new("loss".to_string());
let active_tab = diagnostics_state.active_tab.clone(); let active_tab = diagnostics_state.active_tab.clone();
provide_context(diagnostics_state); provide_context(diagnostics_state);
view! { view! {
div(id = "diagnostics") { div(id="diagnostics") {
div(id = "diagnostics-bar") { div(id="diagnostics-bar") {
RealizationStatus {} RealizationStatus {}
select(bind:value = active_tab) { select(bind:value=active_tab) {
option(value = "loss") { "Loss" } option(value="loss") { "Loss" }
option(value = "spectrum") { "Spectrum" } option(value="spectrum") { "Spectrum" }
} }
StepInput {}
} }
DiagnosticsPanel(name = "loss") { LossHistory {} } DiagnosticsPanel(name="loss") { LossHistory {} }
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } DiagnosticsPanel(name="spectrum") { SpectrumHistory {} }
} }
} }
} }

View file

@ -12,12 +12,12 @@ use web_sys::{
WebGlProgram, WebGlProgram,
WebGlShader, WebGlShader,
WebGlUniformLocation, WebGlUniformLocation,
wasm_bindgen::{JsCast, JsValue}, wasm_bindgen::{JsCast, JsValue}
}; };
use crate::{ use crate::{
AppState, AppState,
assembly::{Element, ElementColor, ElementMotion, Point, Sphere}, assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
}; };
// --- color --- // --- color ---
@ -37,26 +37,23 @@ fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
struct SceneSpheres { struct SceneSpheres {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>
} }
impl SceneSpheres { impl SceneSpheres {
fn new() -> Self { fn new() -> SceneSpheres{
Self { SceneSpheres {
representations: Vec::new(), representations: Vec::new(),
colors_with_opacity: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new()
} }
} }
fn len_i32(&self) -> i32 { fn len_i32(&self) -> i32 {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
} }
fn push( fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32,
) {
self.representations.push(representation); self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity)); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
@ -67,23 +64,20 @@ struct ScenePoints {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>,
selections: Vec<f32>, selections: Vec<f32>
} }
impl ScenePoints { impl ScenePoints {
fn new() -> Self { fn new() -> ScenePoints {
Self { ScenePoints {
representations: Vec::new(), representations: Vec::new(),
colors_with_opacity: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new(),
selections: Vec::new(), selections: Vec::new()
} }
} }
fn push( fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
) {
self.representations.push(representation); self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity)); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
@ -93,30 +87,25 @@ impl ScenePoints {
pub struct Scene { pub struct Scene {
spheres: SceneSpheres, spheres: SceneSpheres,
points: ScenePoints, points: ScenePoints
} }
impl Scene { impl Scene {
fn new() -> Self { fn new() -> Scene {
Self { Scene {
spheres: SceneSpheres::new(), spheres: SceneSpheres::new(),
points: ScenePoints::new(), points: ScenePoints::new()
} }
} }
} }
pub trait DisplayItem { pub trait DisplayItem {
fn show(&self, scene: &mut Scene, selected: bool); fn show(&self, scene: &mut Scene, selected: bool);
// the smallest positive depth, represented as a multiple of `dir`, where // the smallest positive depth, represented as a multiple of `dir`, where
// the line generated by `dir` hits the element. returns `None` if the line // the line generated by `dir` hits the element. returns `None` if the line
// misses the element // misses the element
fn cast( fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64>;
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64>;
} }
impl DisplayItem for Sphere { impl DisplayItem for Sphere {
@ -125,31 +114,26 @@ impl DisplayItem for Sphere {
const DEFAULT_OPACITY: f32 = 0.5; const DEFAULT_OPACITY: f32 = 0.5;
const GHOST_OPACITY: f32 = 0.2; const GHOST_OPACITY: f32 = 0.2;
const HIGHLIGHT: f32 = 0.2; const HIGHLIGHT: f32 = 0.2;
let representation = self.representation.get_clone_untracked(); let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY }; let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
let highlight = if selected { 1.0 } else { HIGHLIGHT }; let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.spheres.push(representation, color, opacity, highlight); scene.spheres.push(representation, color, opacity, highlight);
} }
// this method should be kept synchronized with `sphere_cast` in // this method should be kept synchronized with `sphere_cast` in
// `spheres.frag`, which does essentially the same thing on the GPU side // `spheres.frag`, which does essentially the same thing on the GPU side
fn cast( fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, _pixel_size: f64) -> Option<f64> {
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
_pixel_size: f64,
) -> Option<f64> {
// if `a/b` is less than this threshold, we approximate // if `a/b` is less than this threshold, we approximate
// `a*u^2 + b*u + c` by the linear function `b*u + c` // `a*u^2 + b*u + c` by the linear function `b*u + c`
const DEG_THRESHOLD: f64 = 1e-9; const DEG_THRESHOLD: f64 = 1e-9;
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
let a = -rep[3] * dir.norm_squared(); let a = -rep[3] * dir.norm_squared();
let b = rep.rows_range(..3).dot(&dir); let b = rep.rows_range(..3).dot(&dir);
let c = -rep[4]; let c = -rep[4];
let adjust = 4.0*a*c/(b*b); let adjust = 4.0*a*c/(b*b);
if adjust < 1.0 { if adjust < 1.0 {
// as long as `b` is non-zero, the linear approximation of // as long as `b` is non-zero, the linear approximation of
@ -184,35 +168,30 @@ impl DisplayItem for Point {
/* SCAFFOLDING */ /* SCAFFOLDING */
const GHOST_OPACITY: f32 = 0.4; const GHOST_OPACITY: f32 = 0.4;
const HIGHLIGHT: f32 = 0.5; const HIGHLIGHT: f32 = 0.5;
let representation = self.representation.get_clone_untracked(); let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 }; let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
let highlight = if selected { 1.0 } else { HIGHLIGHT }; let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.points.push(representation, color, opacity, highlight, selected); scene.points.push(representation, color, opacity, highlight, selected);
} }
/* SCAFFOLDING */ /* SCAFFOLDING */
fn cast( fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64> {
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64> {
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
if rep[2] < 0.0 { if rep[2] < 0.0 {
// this constant should be kept synchronized with `point.frag` // this constant should be kept synchronized with `point.frag`
const POINT_RADIUS_PX: f64 = 4.0; const POINT_RADIUS_PX: f64 = 4.0;
// find the radius of the point in screen projection units // find the radius of the point in screen projection units
let point_radius_proj = POINT_RADIUS_PX * pixel_size; let point_radius_proj = POINT_RADIUS_PX * pixel_size;
// find the squared distance between the screen projections of the // find the squared distance between the screen projections of the
// ray and the point // ray and the point
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2]; let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2]; let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
let dist_sq = (dir_proj - rep_proj).norm_squared(); let dist_sq = (dir_proj - rep_proj).norm_squared();
// if the ray hits the point, return its depth // if the ray hits the point, return its depth
if dist_sq < point_radius_proj * point_radius_proj { if dist_sq < point_radius_proj * point_radius_proj {
Some(rep[2] / dir[2]) Some(rep[2] / dir[2])
@ -241,7 +220,7 @@ fn compile_shader(
fn set_up_program( fn set_up_program(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
vertex_shader_source: &str, vertex_shader_source: &str,
fragment_shader_source: &str, fragment_shader_source: &str
) -> WebGlProgram { ) -> WebGlProgram {
// compile the shaders // compile the shaders
let vertex_shader = compile_shader( let vertex_shader = compile_shader(
@ -254,13 +233,13 @@ fn set_up_program(
WebGl2RenderingContext::FRAGMENT_SHADER, WebGl2RenderingContext::FRAGMENT_SHADER,
fragment_shader_source, fragment_shader_source,
); );
// create the program and attach the shaders // create the program and attach the shaders
let program = context.create_program().unwrap(); let program = context.create_program().unwrap();
context.attach_shader(&program, &vertex_shader); context.attach_shader(&program, &vertex_shader);
context.attach_shader(&program, &fragment_shader); context.attach_shader(&program, &fragment_shader);
context.link_program(&program); context.link_program(&program);
/* DEBUG */ /* DEBUG */
// report whether linking succeeded // report whether linking succeeded
let link_status = context let link_status = context
@ -273,7 +252,7 @@ fn set_up_program(
"Linking failed" "Linking failed"
}; };
console::log_1(&JsValue::from(link_msg)); console::log_1(&JsValue::from(link_msg));
program program
} }
@ -281,12 +260,12 @@ fn get_uniform_array_locations<const N: usize>(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
program: &WebGlProgram, program: &WebGlProgram,
var_name: &str, var_name: &str,
member_name_opt: Option<&str>, member_name_opt: Option<&str>
) -> [Option<WebGlUniformLocation>; N] { ) -> [Option<WebGlUniformLocation>; N] {
array::from_fn(|n| { array::from_fn(|n| {
let name = match member_name_opt { let name = match member_name_opt {
Some(member_name) => format!("{var_name}[{n}].{member_name}"), Some(member_name) => format!("{var_name}[{n}].{member_name}"),
None => format!("{var_name}[{n}]"), None => format!("{var_name}[{n}]")
}; };
context.get_uniform_location(&program, name.as_str()) context.get_uniform_location(&program, name.as_str())
}) })
@ -297,7 +276,7 @@ fn bind_to_attribute(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
attr_index: u32, attr_index: u32,
attr_size: i32, attr_size: i32,
buffer: &Option<WebGlBuffer>, buffer: &Option<WebGlBuffer>
) { ) {
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
context.vertex_attrib_pointer_with_i32( context.vertex_attrib_pointer_with_i32(
@ -313,12 +292,12 @@ fn bind_to_attribute(
// load the given data into a new vertex buffer object // load the given data into a new vertex buffer object
fn load_new_buffer( fn load_new_buffer(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
data: &[f32], data: &[f32]
) -> Option<WebGlBuffer> { ) -> Option<WebGlBuffer> {
// create a buffer and bind it to ARRAY_BUFFER // create a buffer and bind it to ARRAY_BUFFER
let buffer = context.create_buffer(); let buffer = context.create_buffer();
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
// load the given data into the buffer. this block is unsafe because // load the given data into the buffer. this block is unsafe because
// `Float32Array::view` creates a raw view into our module's // `Float32Array::view` creates a raw view into our module's
// `WebAssembly.Memory` buffer. allocating more memory will change the // `WebAssembly.Memory` buffer. allocating more memory will change the
@ -332,7 +311,7 @@ fn load_new_buffer(
WebGl2RenderingContext::STATIC_DRAW, WebGl2RenderingContext::STATIC_DRAW,
); );
} }
buffer buffer
} }
@ -340,7 +319,7 @@ fn bind_new_buffer_to_attribute(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
attr_index: u32, attr_index: u32,
attr_size: i32, attr_size: i32,
data: &[f32], data: &[f32]
) { ) {
let buffer = load_new_buffer(context, data); let buffer = load_new_buffer(context, data);
bind_to_attribute(context, attr_index, attr_size, &buffer); bind_to_attribute(context, attr_index, attr_size, &buffer);
@ -353,18 +332,18 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
let width = rect.width(); let width = rect.width();
let height = rect.height(); let height = rect.height();
let shortdim = width.min(height); let shortdim = width.min(height);
// this constant should be kept synchronized with `spheres.frag` and // this constant should be kept synchronized with `spheres.frag` and
// `point.vert` // `point.vert`
const FOCAL_SLOPE: f64 = 0.3; const FOCAL_SLOPE: f64 = 0.3;
( (
Vector3::new( Vector3::new(
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
-1.0, -1.0
), ),
FOCAL_SLOPE * 2.0 / shortdim, FOCAL_SLOPE * 2.0 / shortdim
) )
} }
@ -373,13 +352,13 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
#[component] #[component]
pub fn Display() -> View { pub fn Display() -> View {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
// canvas // canvas
let display = create_node_ref(); let display = create_node_ref();
// viewpoint // viewpoint
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5)); let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
// navigation // navigation
let pitch_up = create_signal(0.0); let pitch_up = create_signal(0.0);
let pitch_down = create_signal(0.0); let pitch_down = create_signal(0.0);
@ -390,7 +369,7 @@ pub fn Display() -> View {
let zoom_in = create_signal(0.0); let zoom_in = create_signal(0.0);
let zoom_out = create_signal(0.0); let zoom_out = create_signal(0.0);
let turntable = create_signal(false); /* BENCHMARKING */ let turntable = create_signal(false); /* BENCHMARKING */
// manipulation // manipulation
let translate_neg_x = create_signal(0.0); let translate_neg_x = create_signal(0.0);
let translate_pos_x = create_signal(0.0); let translate_pos_x = create_signal(0.0);
@ -400,7 +379,7 @@ pub fn Display() -> View {
let translate_pos_z = create_signal(0.0); let translate_pos_z = create_signal(0.0);
let shrink_neg = create_signal(0.0); let shrink_neg = create_signal(0.0);
let shrink_pos = create_signal(0.0); let shrink_pos = create_signal(0.0);
// change listener // change listener
let scene_changed = create_signal(true); let scene_changed = create_signal(true);
create_effect(move || { create_effect(move || {
@ -413,18 +392,18 @@ pub fn Display() -> View {
state.selection.track(); state.selection.track();
scene_changed.set(true); scene_changed.set(true);
}); });
/* INSTRUMENTS */ /* INSTRUMENTS */
const SAMPLE_PERIOD: i32 = 60; const SAMPLE_PERIOD: i32 = 60;
let mut last_sample_time = 0.0; let mut last_sample_time = 0.0;
let mut frames_since_last_sample = 0; let mut frames_since_last_sample = 0;
let mean_frame_interval = create_signal(0.0); let mean_frame_interval = create_signal(0.0);
let assembly_for_raf = state.assembly.clone(); let assembly_for_raf = state.assembly.clone();
on_mount(move || { on_mount(move || {
// timing // timing
let mut last_time = 0.0; let mut last_time = 0.0;
// viewpoint // viewpoint
const ROT_SPEED: f64 = 0.4; // in radians per second const ROT_SPEED: f64 = 0.4; // in radians per second
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
@ -432,18 +411,18 @@ pub fn Display() -> View {
let mut orientation = DMatrix::<f64>::identity(5, 5); let mut orientation = DMatrix::<f64>::identity(5, 5);
let mut rotation = DMatrix::<f64>::identity(5, 5); let mut rotation = DMatrix::<f64>::identity(5, 5);
let mut location_z: f64 = 5.0; let mut location_z: f64 = 5.0;
// manipulation // manipulation
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
const SHRINKING_SPEED: f64 = 0.15; // in length units per second const SHRINKING_SPEED: f64 = 0.15; // in length units per second
// display parameters // display parameters
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
const DEBUG_MODE: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */
/* INSTRUMENTS */ /* INSTRUMENTS */
let performance = window().unwrap().performance().unwrap(); let performance = window().unwrap().performance().unwrap();
// get the display canvas // get the display canvas
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>(); let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
let ctx = canvas let ctx = canvas
@ -452,28 +431,28 @@ pub fn Display() -> View {
.unwrap() .unwrap()
.dyn_into::<WebGl2RenderingContext>() .dyn_into::<WebGl2RenderingContext>()
.unwrap(); .unwrap();
// disable depth testing // disable depth testing
ctx.disable(WebGl2RenderingContext::DEPTH_TEST); ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
// set blend mode // set blend mode
ctx.enable(WebGl2RenderingContext::BLEND); ctx.enable(WebGl2RenderingContext::BLEND);
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA); ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
// set up the sphere rendering program // set up the sphere rendering program
let sphere_program = set_up_program( let sphere_program = set_up_program(
&ctx, &ctx,
include_str!("identity.vert"), include_str!("identity.vert"),
include_str!("spheres.frag"), include_str!("spheres.frag")
); );
// set up the point rendering program // set up the point rendering program
let point_program = set_up_program( let point_program = set_up_program(
&ctx, &ctx,
include_str!("point.vert"), include_str!("point.vert"),
include_str!("point.frag"), include_str!("point.frag")
); );
/* DEBUG */ /* DEBUG */
// print the maximum number of vectors that can be passed as // print the maximum number of vectors that can be passed as
// uniforms to a fragment shader. the OpenGL ES 3.0 standard // uniforms to a fragment shader. the OpenGL ES 3.0 standard
@ -488,12 +467,12 @@ pub fn Display() -> View {
// capped at 1024 elements // capped at 1024 elements
console::log_2( console::log_2(
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
&JsValue::from("uniform vectors available"), &JsValue::from("uniform vectors available")
); );
// find the sphere program's vertex attribute // find the sphere program's vertex attribute
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32; let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
// find the sphere program's uniforms // find the sphere program's uniforms
const SPHERE_MAX: usize = 200; const SPHERE_MAX: usize = 200;
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
@ -513,7 +492,7 @@ pub fn Display() -> View {
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
// load the viewport vertex positions into a new vertex buffer object // load the viewport vertex positions into a new vertex buffer object
const VERTEX_CNT: usize = 6; const VERTEX_CNT: usize = 6;
let viewport_positions: [f32; 3*VERTEX_CNT] = [ let viewport_positions: [f32; 3*VERTEX_CNT] = [
@ -524,23 +503,23 @@ pub fn Display() -> View {
// southeast triangle // southeast triangle
-1.0, -1.0, 0.0, -1.0, -1.0, 0.0,
1.0, 1.0, 0.0, 1.0, 1.0, 0.0,
1.0, -1.0, 0.0, 1.0, -1.0, 0.0
]; ];
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
// find the point program's vertex attributes // find the point program's vertex attributes
let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
// set up a repainting routine // set up a repainting routine
let (_, start_animation_loop, _) = create_raf(move || { let (_, start_animation_loop, _) = create_raf(move || {
// get the time step // get the time step
let time = performance.now(); let time = performance.now();
let time_step = 0.001*(time - last_time); let time_step = 0.001*(time - last_time);
last_time = time; last_time = time;
// get the navigation state // get the navigation state
let pitch_up_val = pitch_up.get(); let pitch_up_val = pitch_up.get();
let pitch_down_val = pitch_down.get(); let pitch_down_val = pitch_down.get();
@ -551,7 +530,7 @@ pub fn Display() -> View {
let zoom_in_val = zoom_in.get(); let zoom_in_val = zoom_in.get();
let zoom_out_val = zoom_out.get(); let zoom_out_val = zoom_out.get();
let turntable_val = turntable.get(); /* BENCHMARKING */ let turntable_val = turntable.get(); /* BENCHMARKING */
// get the manipulation state // get the manipulation state
let translate_neg_x_val = translate_neg_x.get(); let translate_neg_x_val = translate_neg_x.get();
let translate_pos_x_val = translate_pos_x.get(); let translate_pos_x_val = translate_pos_x.get();
@ -561,7 +540,7 @@ pub fn Display() -> View {
let translate_pos_z_val = translate_pos_z.get(); let translate_pos_z_val = translate_pos_z.get();
let shrink_neg_val = shrink_neg.get(); let shrink_neg_val = shrink_neg.get();
let shrink_pos_val = shrink_pos.get(); let shrink_pos_val = shrink_pos.get();
// update the assembly's orientation // update the assembly's orientation
let ang_vel = { let ang_vel = {
let pitch = pitch_up_val - pitch_down_val; let pitch = pitch_up_val - pitch_down_val;
@ -582,31 +561,13 @@ pub fn Display() -> View {
Rotation3::from_scaled_axis(time_step * ang_vel).matrix() Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
); );
orientation = &rotation * &orientation; orientation = &rotation * &orientation;
// update the assembly's location // update the assembly's location
let zoom = zoom_out_val - zoom_in_val; let zoom = zoom_out_val - zoom_in_val;
location_z *= (time_step * ZOOM_SPEED * zoom).exp(); location_z *= (time_step * ZOOM_SPEED * zoom).exp();
// manipulate the assembly // manipulate the assembly
/* KLUDGE */ if state.selection.with(|sel| sel.len() == 1) {
// to avoid the complexity of making tangent space projection
// conditional and dealing with unnormalized representation vectors,
// we only allow manipulation when we're looking at the last step of
// a successful realization
let realization_successful = state.assembly.realization_status.with(
|status| status.is_ok()
);
let step_val = state.assembly.step.with_untracked(|step| step.value);
let on_init_step = step_val.is_some_and(|n| n == 0.0);
let on_last_step = step_val.is_some_and(
|n| state.assembly.descent_history.with_untracked(
|history| n as usize + 1 == history.config.len().max(1)
)
);
let on_manipulable_step =
!realization_successful && on_init_step
|| realization_successful && on_last_step;
if on_manipulable_step && state.selection.with(|sel| sel.len() == 1) {
let sel = state.selection.with( let sel = state.selection.with(
|sel| sel.into_iter().next().unwrap().clone() |sel| sel.into_iter().next().unwrap().clone()
); );
@ -635,18 +596,18 @@ pub fn Display() -> View {
vec![ vec![
ElementMotion { ElementMotion {
element: sel, element: sel,
velocity: elt_motion.as_view(), velocity: elt_motion.as_view()
} }
] ]
); );
scene_changed.set(true); scene_changed.set(true);
} }
} }
if scene_changed.get() { if scene_changed.get() {
const SPACE_DIM: usize = 3; const SPACE_DIM: usize = 3;
const COLOR_SIZE: usize = 3; const COLOR_SIZE: usize = 3;
/* INSTRUMENTS */ /* INSTRUMENTS */
// measure mean frame interval // measure mean frame interval
frames_since_last_sample += 1; frames_since_last_sample += 1;
@ -655,11 +616,11 @@ pub fn Display() -> View {
last_sample_time = time; last_sample_time = time;
frames_since_last_sample = 0; frames_since_last_sample = 0;
} }
// --- get the assembly --- // --- get the assembly ---
let mut scene = Scene::new(); let mut scene = Scene::new();
// find the map from assembly space to world space // find the map from assembly space to world space
let location = { let location = {
let u = -location_z; let u = -location_z;
@ -668,11 +629,11 @@ pub fn Display() -> View {
0.0, 1.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, 1.0, 0.0, u,
0.0, 0.0, 2.0*u, 1.0, u*u, 0.0, 0.0, 2.0*u, 1.0, u*u,
0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0
]) ])
}; };
let asm_to_world = &location * &orientation; let asm_to_world = &location * &orientation;
// set up the scene // set up the scene
state.assembly.elements.with_untracked( state.assembly.elements.with_untracked(
|elts| for elt in elts { |elts| for elt in elts {
@ -681,74 +642,74 @@ pub fn Display() -> View {
} }
); );
let sphere_cnt = scene.spheres.len_i32(); let sphere_cnt = scene.spheres.len_i32();
// --- draw the spheres --- // --- draw the spheres ---
// use the sphere rendering program // use the sphere rendering program
ctx.use_program(Some(&sphere_program)); ctx.use_program(Some(&sphere_program));
// enable the sphere program's vertex attribute // enable the sphere program's vertex attribute
ctx.enable_vertex_attrib_array(viewport_position_attr); ctx.enable_vertex_attrib_array(viewport_position_attr);
// write the spheres in world coordinates // write the spheres in world coordinates
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|rep| (&asm_to_world * rep).cast::<f32>() |rep| (&asm_to_world * rep).cast::<f32>()
).collect(); ).collect();
// set the resolution // set the resolution
let width = canvas.width() as f32; let width = canvas.width() as f32;
let height = canvas.height() as f32; let height = canvas.height() as f32;
ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform2f(resolution_loc.as_ref(), width, height);
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
// pass the scene data // pass the scene data
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt); ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
for n in 0..sphere_reps_world.len() { for n in 0..sphere_reps_world.len() {
let v = &sphere_reps_world[n]; let v = &sphere_reps_world[n];
ctx.uniform3fv_with_f32_array( ctx.uniform3fv_with_f32_array(
sphere_sp_locs[n].as_ref(), sphere_sp_locs[n].as_ref(),
v.rows(0, 3).as_slice(), v.rows(0, 3).as_slice()
); );
ctx.uniform2fv_with_f32_array( ctx.uniform2fv_with_f32_array(
sphere_lt_locs[n].as_ref(), sphere_lt_locs[n].as_ref(),
v.rows(3, 2).as_slice(), v.rows(3, 2).as_slice()
); );
ctx.uniform4fv_with_f32_array( ctx.uniform4fv_with_f32_array(
sphere_color_locs[n].as_ref(), sphere_color_locs[n].as_ref(),
&scene.spheres.colors_with_opacity[n], &scene.spheres.colors_with_opacity[n]
); );
ctx.uniform1f( ctx.uniform1f(
sphere_highlight_locs[n].as_ref(), sphere_highlight_locs[n].as_ref(),
scene.spheres.highlights[n], scene.spheres.highlights[n]
); );
} }
// pass the display parameters // pass the display parameters
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
// bind the viewport vertex position buffer to the position // bind the viewport vertex position buffer to the position
// attribute in the vertex shader // attribute in the vertex shader
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer); bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
// draw the scene // draw the scene
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
// disable the sphere program's vertex attribute // disable the sphere program's vertex attribute
ctx.disable_vertex_attrib_array(viewport_position_attr); ctx.disable_vertex_attrib_array(viewport_position_attr);
// --- draw the points --- // --- draw the points ---
if !scene.points.representations.is_empty() { if !scene.points.representations.is_empty() {
// use the point rendering program // use the point rendering program
ctx.use_program(Some(&point_program)); ctx.use_program(Some(&point_program));
// enable the point program's vertex attributes // enable the point program's vertex attributes
ctx.enable_vertex_attrib_array(point_position_attr); ctx.enable_vertex_attrib_array(point_position_attr);
ctx.enable_vertex_attrib_array(point_color_attr); ctx.enable_vertex_attrib_array(point_color_attr);
ctx.enable_vertex_attrib_array(point_highlight_attr); ctx.enable_vertex_attrib_array(point_highlight_attr);
ctx.enable_vertex_attrib_array(point_selection_attr); ctx.enable_vertex_attrib_array(point_selection_attr);
// write the points in world coordinates // write the points in world coordinates
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
let point_positions = DMatrix::from_columns( let point_positions = DMatrix::from_columns(
@ -756,7 +717,7 @@ pub fn Display() -> View {
|rep| &asm_to_world_sp * rep |rep| &asm_to_world_sp * rep
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
).cast::<f32>(); ).cast::<f32>();
// load the point positions and colors into new buffers and // load the point positions and colors into new buffers and
// bind them to the corresponding attributes in the vertex // bind them to the corresponding attributes in the vertex
// shader // shader
@ -764,22 +725,22 @@ pub fn Display() -> View {
bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
// draw the scene // draw the scene
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
// disable the point program's vertex attributes // disable the point program's vertex attributes
ctx.disable_vertex_attrib_array(point_position_attr); ctx.disable_vertex_attrib_array(point_position_attr);
ctx.disable_vertex_attrib_array(point_color_attr); ctx.disable_vertex_attrib_array(point_color_attr);
ctx.disable_vertex_attrib_array(point_highlight_attr); ctx.disable_vertex_attrib_array(point_highlight_attr);
ctx.disable_vertex_attrib_array(point_selection_attr); ctx.disable_vertex_attrib_array(point_selection_attr);
} }
// --- update the display state --- // --- update the display state ---
// update the viewpoint // update the viewpoint
assembly_to_world.set(asm_to_world); assembly_to_world.set(asm_to_world);
// clear the scene change flag // clear the scene change flag
scene_changed.set( scene_changed.set(
pitch_up_val != 0.0 pitch_up_val != 0.0
@ -799,7 +760,7 @@ pub fn Display() -> View {
}); });
start_animation_loop(); start_animation_loop();
}); });
let set_nav_signal = move |event: &KeyboardEvent, value: f64| { let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
let mut navigating = true; let mut navigating = true;
let shift = event.shift_key(); let shift = event.shift_key();
@ -812,14 +773,14 @@ pub fn Display() -> View {
"ArrowLeft" if shift => roll_ccw.set(value), "ArrowLeft" if shift => roll_ccw.set(value),
"ArrowRight" => yaw_right.set(value), "ArrowRight" => yaw_right.set(value),
"ArrowLeft" => yaw_left.set(value), "ArrowLeft" => yaw_left.set(value),
_ => navigating = false, _ => navigating = false
}; };
if navigating { if navigating {
scene_changed.set(true); scene_changed.set(true);
event.prevent_default(); event.prevent_default();
} }
}; };
let set_manip_signal = move |event: &KeyboardEvent, value: f64| { let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
let mut manipulating = true; let mut manipulating = true;
let shift = event.shift_key(); let shift = event.shift_key();
@ -832,24 +793,24 @@ pub fn Display() -> View {
"s" | "S" => translate_neg_y.set(value), "s" | "S" => translate_neg_y.set(value),
"]" | "}" => shrink_neg.set(value), "]" | "}" => shrink_neg.set(value),
"[" | "{" => shrink_pos.set(value), "[" | "{" => shrink_pos.set(value),
_ => manipulating = false, _ => manipulating = false
}; };
if manipulating { if manipulating {
event.prevent_default(); event.prevent_default();
} }
}; };
view! { view! {
/* TO DO */ /* TO DO */
// switch back to integer-valued parameters when that becomes possible // switch back to integer-valued parameters when that becomes possible
// again // again
canvas( canvas(
ref = display, ref=display,
id = "display", id="display",
width = "600", width="600",
height = "600", height="600",
tabindex = "0", tabindex="0",
on:keydown = move |event: KeyboardEvent| { on:keydown=move |event: KeyboardEvent| {
if event.key() == "Shift" { if event.key() == "Shift" {
// swap navigation inputs // swap navigation inputs
roll_cw.set(yaw_right.get()); roll_cw.set(yaw_right.get());
@ -860,7 +821,7 @@ pub fn Display() -> View {
yaw_left.set(0.0); yaw_left.set(0.0);
pitch_up.set(0.0); pitch_up.set(0.0);
pitch_down.set(0.0); pitch_down.set(0.0);
// swap manipulation inputs // swap manipulation inputs
translate_pos_z.set(translate_neg_y.get()); translate_pos_z.set(translate_neg_y.get());
translate_neg_z.set(translate_pos_y.get()); translate_neg_z.set(translate_pos_y.get());
@ -875,7 +836,7 @@ pub fn Display() -> View {
set_manip_signal(&event, 1.0); set_manip_signal(&event, 1.0);
} }
}, },
on:keyup = move |event: KeyboardEvent| { on:keyup=move |event: KeyboardEvent| {
if event.key() == "Shift" { if event.key() == "Shift" {
// swap navigation inputs // swap navigation inputs
yaw_right.set(roll_cw.get()); yaw_right.set(roll_cw.get());
@ -886,7 +847,7 @@ pub fn Display() -> View {
roll_ccw.set(0.0); roll_ccw.set(0.0);
zoom_in.set(0.0); zoom_in.set(0.0);
zoom_out.set(0.0); zoom_out.set(0.0);
// swap manipulation inputs // swap manipulation inputs
translate_pos_y.set(translate_neg_z.get()); translate_pos_y.set(translate_neg_z.get());
translate_neg_y.set(translate_pos_z.get()); translate_neg_y.set(translate_pos_z.get());
@ -897,7 +858,7 @@ pub fn Display() -> View {
set_manip_signal(&event, 0.0); set_manip_signal(&event, 0.0);
} }
}, },
on:blur = move |_| { on:blur=move |_| {
pitch_up.set(0.0); pitch_up.set(0.0);
pitch_down.set(0.0); pitch_down.set(0.0);
yaw_right.set(0.0); yaw_right.set(0.0);
@ -905,7 +866,7 @@ pub fn Display() -> View {
roll_ccw.set(0.0); roll_ccw.set(0.0);
roll_cw.set(0.0); roll_cw.set(0.0);
}, },
on:click = move |event: MouseEvent| { on:click=move |event: MouseEvent| {
// find the nearest element along the pointer direction // find the nearest element along the pointer direction
let (dir, pixel_size) = event_dir(&event); let (dir, pixel_size) = event_dir(&event);
console::log_1(&JsValue::from(dir.to_string())); console::log_1(&JsValue::from(dir.to_string()));
@ -922,18 +883,18 @@ pub fn Display() -> View {
clicked = Some((elt, depth)) clicked = Some((elt, depth))
} }
}, },
None => clicked = Some((elt, depth)), None => clicked = Some((elt, depth))
}, }
None => (), None => ()
}; };
} }
// if we clicked something, select it // if we clicked something, select it
match clicked { match clicked {
Some((elt, _)) => state.select(&elt, event.shift_key()), Some((elt, _)) => state.select(&elt, event.shift_key()),
None => state.selection.update(|sel| sel.clear()), None => state.selection.update(|sel| sel.clear())
}; };
}, }
) )
} }
} }

View file

@ -1,7 +1,11 @@
use itertools::Itertools; use itertools::Itertools;
use std::rc::Rc; use std::rc::Rc;
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use web_sys::{
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
};
use crate::{ use crate::{
AppState, AppState,
@ -9,8 +13,7 @@ use crate::{
Element, Element,
HalfCurvatureRegulator, HalfCurvatureRegulator,
InversiveDistanceRegulator, InversiveDistanceRegulator,
PointCoordinateRegulator, Regulator
Regulator,
}, },
specified::SpecifiedValue specified::SpecifiedValue
}; };
@ -21,16 +24,16 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
// get the regulator's measurement and set point signals // get the regulator's measurement and set point signals
let measurement = regulator.measurement(); let measurement = regulator.measurement();
let set_point = regulator.set_point(); let set_point = regulator.set_point();
// the `valid` signal tracks whether the last entered value is a valid set // the `valid` signal tracks whether the last entered value is a valid set
// point specification // point specification
let valid = create_signal(true); let valid = create_signal(true);
// the `value` signal holds the current set point specification // the `value` signal holds the current set point specification
let value = create_signal( let value = create_signal(
set_point.with_untracked(|set_pt| set_pt.spec.clone()) set_point.with_untracked(|set_pt| set_pt.spec.clone())
); );
// this `reset_value` closure resets the input value to the regulator's set // this `reset_value` closure resets the input value to the regulator's set
// point specification // point specification
let reset_value = move || { let reset_value = move || {
@ -39,15 +42,15 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
value.set(set_point.with(|set_pt| set_pt.spec.clone())); value.set(set_point.with(|set_pt| set_pt.spec.clone()));
}) })
}; };
// reset the input value whenever the regulator's set point specification // reset the input value whenever the regulator's set point specification
// is updated // is updated
create_effect(reset_value); create_effect(reset_value);
view! { view! {
input( input(
r#type = "text", r#type="text",
class = move || { class=move || {
if valid.get() { if valid.get() {
set_point.with(|set_pt| { set_point.with(|set_pt| {
if set_pt.is_present() { if set_pt.is_present() {
@ -60,27 +63,27 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
"regulator-input invalid" "regulator-input invalid"
} }
}, },
placeholder = measurement.with(|result| result.to_string()), placeholder=measurement.with(|result| result.to_string()),
bind:value = value, bind:value=value,
on:change = move |_| { on:change=move |_| {
valid.set( valid.set(
match SpecifiedValue::try_from(value.get_clone_untracked()) { match SpecifiedValue::try_from(value.get_clone_untracked()) {
Ok(set_pt) => { Ok(set_pt) => {
set_point.set(set_pt); set_point.set(set_pt);
true true
}, }
Err(_) => false, Err(_) => false
} }
) )
}, },
on:keydown = { on:keydown={
move |event: KeyboardEvent| { move |event: KeyboardEvent| {
match event.key().as_str() { match event.key().as_str() {
"Escape" => reset_value(), "Escape" => reset_value(),
_ => (), _ => ()
} }
} }
}, }
) )
} }
} }
@ -97,11 +100,11 @@ impl OutlineItem for InversiveDistanceRegulator {
self.subjects[0].label() self.subjects[0].label()
}.clone(); }.clone();
view! { view! {
li(class = "regulator") { li(class="regulator") {
div(class = "regulator-label") { (other_subject_label) } div(class="regulator-label") { (other_subject_label) }
div(class = "regulator-type") { "Inversive distance" } div(class="regulator-type") { "Inversive distance" }
RegulatorInput(regulator = self) RegulatorInput(regulator=self)
div(class = "status") div(class="status")
} }
} }
} }
@ -110,25 +113,11 @@ impl OutlineItem for InversiveDistanceRegulator {
impl OutlineItem for HalfCurvatureRegulator { impl OutlineItem for HalfCurvatureRegulator {
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View { fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
view! { view! {
li(class = "regulator") { li(class="regulator") {
div(class = "regulator-label") // for spacing div(class="regulator-label") // for spacing
div(class = "regulator-type") { "Half-curvature" } div(class="regulator-type") { "Half-curvature" }
RegulatorInput(regulator = self) RegulatorInput(regulator=self)
div(class = "status") div(class="status")
}
}
}
}
impl OutlineItem for PointCoordinateRegulator {
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
let name = format!("{} coordinate", self.axis);
view! {
li(class = "regulator") {
div(class = "regulator-label") // for spacing
div(class = "regulator-type") { (name) }
RegulatorInput(regulator = self)
div(class = "status")
} }
} }
} }
@ -167,10 +156,10 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
let details_node = create_node_ref(); let details_node = create_node_ref();
view! { view! {
li { li {
details(ref = details_node) { details(ref=details_node) {
summary( summary(
class = class.get(), class=class.get(),
on:keydown = { on:keydown={
let element_for_handler = element.clone(); let element_for_handler = element.clone();
move |event: KeyboardEvent| { move |event: KeyboardEvent| {
match event.key().as_str() { match event.key().as_str() {
@ -190,18 +179,18 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
.unchecked_into::<web_sys::Element>() .unchecked_into::<web_sys::Element>()
.remove_attribute("open"); .remove_attribute("open");
}, },
_ => (), _ => ()
} }
} }
} }
) { ) {
div( div(
class = "element-switch", class="element-switch",
on:click = |event: MouseEvent| event.stop_propagation() on:click=|event: MouseEvent| event.stop_propagation()
) )
div( div(
class = "element", class="element",
on:click = { on:click={
let state_for_handler = state.clone(); let state_for_handler = state.clone();
let element_for_handler = element.clone(); let element_for_handler = element.clone();
move |event: MouseEvent| { move |event: MouseEvent| {
@ -211,20 +200,20 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
} }
} }
) { ) {
div(class = "element-label") { (label) } div(class="element-label") { (label) }
div(class = "element-representation") { (rep_components) } div(class="element-representation") { (rep_components) }
input( input(
r#type = "checkbox", r#type="checkbox",
bind:checked = element.ghost(), bind:checked=element.ghost(),
on:click = |event: MouseEvent| event.stop_propagation() on:click=|event: MouseEvent| event.stop_propagation()
) )
} }
} }
ul(class = "regulators") { ul(class="regulators") {
Keyed( Keyed(
list = regulator_list, list=regulator_list,
view = move |reg| reg.outline_item(&element), view=move |reg| reg.outline_item(&element),
key = |reg| reg.serial() key=|reg| reg.serial()
) )
} }
} }
@ -241,7 +230,7 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
#[component] #[component]
pub fn Outline() -> View { pub fn Outline() -> View {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
// list the elements alphabetically by ID // list the elements alphabetically by ID
/* TO DO */ /* TO DO */
// this code is designed to generalize easily to other sort keys. if we only // this code is designed to generalize easily to other sort keys. if we only
@ -254,22 +243,22 @@ pub fn Outline() -> View {
.sorted_by_key(|elt| elt.id().clone()) .sorted_by_key(|elt| elt.id().clone())
.collect::<Vec<_>>() .collect::<Vec<_>>()
); );
view! { view! {
ul( ul(
id = "outline", id="outline",
on:click = { on:click={
let state = use_context::<AppState>(); let state = use_context::<AppState>();
move |_| state.selection.update(|sel| sel.clear()) move |_| state.selection.update(|sel| sel.clear())
} }
) { ) {
Keyed( Keyed(
list = element_list, list=element_list,
view = |elt| view! { view=|elt| view! {
ElementOutlineItem(element = elt) ElementOutlineItem(element=elt)
}, },
key = |elt| elt.serial() key=|elt| elt.serial()
) )
} }
} }
} }

View file

@ -10,10 +10,10 @@ out vec4 outColor;
void main() { void main() {
float r = total_radius * length(2.*gl_PointCoord - vec2(1.)); float r = total_radius * length(2.*gl_PointCoord - vec2(1.));
const float POINT_RADIUS = 4.; const float POINT_RADIUS = 4.;
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
float disk = 1. - smoothstep(total_radius - 1., total_radius, r); float disk = 1. - smoothstep(total_radius - 1., total_radius, r);
vec4 color = mix(point_color, vec4(1.), border * point_highlight); vec4 color = mix(point_color, vec4(1.), border * point_highlight);
outColor = vec4(vec3(1.), disk) * color; outColor = vec4(vec3(1.), disk) * color;
} }

View file

@ -14,11 +14,11 @@ const float focal_slope = 0.3;
void main() { void main() {
total_radius = 5. + 0.5*selected; total_radius = 5. + 0.5*selected;
float depth = -focal_slope * position.z; float depth = -focal_slope * position.z;
gl_Position = vec4(position.xy / depth, 0., 1.); gl_Position = vec4(position.xy / depth, 0., 1.);
gl_PointSize = 2.*total_radius; gl_PointSize = 2.*total_radius;
point_color = color; point_color = color;
point_highlight = highlight; point_highlight = highlight;
} }

View file

@ -75,7 +75,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
// point. i calculated it in my head and decided that the result looked good // point. i calculated it in my head and decided that the result looked good
// enough for now // enough for now
vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt);
float incidence = dot(normal, light_dir); float incidence = dot(normal, light_dir);
float illum = mix(0.4, 1.0, max(incidence, 0.0)); float illum = mix(0.4, 1.0, max(incidence, 0.0));
return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a)); return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a));
@ -110,7 +110,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) {
float a = -v.lt.s * dot(dir, dir); float a = -v.lt.s * dot(dir, dir);
float b = dot(v.sp, dir); float b = dot(v.sp, dir);
float c = -v.lt.t; float c = -v.lt.t;
float adjust = 4.*a*c/(b*b); float adjust = 4.*a*c/(b*b);
if (adjust < 1.) { if (adjust < 1.) {
// as long as `b` is non-zero, the linear approximation of // as long as `b` is non-zero, the linear approximation of
@ -136,7 +136,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) {
void main() { void main() {
vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim;
vec3 dir = vec3(focal_slope * scr, -1.); vec3 dir = vec3(focal_slope * scr, -1.);
// cast rays through the spheres // cast rays through the spheres
const int LAYER_MAX = 12; const int LAYER_MAX = 12;
TaggedDepth top_hits [LAYER_MAX]; TaggedDepth top_hits [LAYER_MAX];
@ -144,7 +144,7 @@ void main() {
for (int id = 0; id < sphere_cnt; ++id) { for (int id = 0; id < sphere_cnt; ++id) {
// find out where the ray hits the sphere // find out where the ray hits the sphere
vec2 hit_depths = sphere_cast(sphere_list[id], dir); vec2 hit_depths = sphere_cast(sphere_list[id], dir);
// insertion-sort the points we hit into the hit list // insertion-sort the points we hit into the hit list
float dimming = 1.; float dimming = 1.;
for (int side = 0; side < 2; ++side) { for (int side = 0; side < 2; ++side) {
@ -169,14 +169,14 @@ void main() {
} }
} }
} }
/* DEBUG */ /* DEBUG */
// in debug mode, show the layer count instead of the shaded image // in debug mode, show the layer count instead of the shaded image
if (debug_mode) { if (debug_mode) {
// at the bottom of the screen, show the color scale instead of the // at the bottom of the screen, show the color scale instead of the
// layer count // layer count
if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
// convert number to color // convert number to color
ivec3 bits = layer_cnt / ivec3(1, 2, 4); ivec3 bits = layer_cnt / ivec3(1, 2, 4);
vec3 color = mod(vec3(bits), 2.); vec3 color = mod(vec3(bits), 2.);
@ -186,7 +186,7 @@ void main() {
outColor = vec4(color, 1.); outColor = vec4(color, 1.);
return; return;
} }
// composite the sphere fragments // composite the sphere fragments
vec3 color = vec3(0.); vec3 color = vec3(0.);
int layer = layer_cnt - 1; int layer = layer_cnt - 1;
@ -203,7 +203,7 @@ void main() {
// load the current fragment // load the current fragment
Fragment frag = frag_next; Fragment frag = frag_next;
float highlight = highlight_next; float highlight = highlight_next;
// shade the next fragment // shade the next fragment
hit = top_hits[layer]; hit = top_hits[layer];
sphere_color = color_list[hit.id]; sphere_color = color_list[hit.id];
@ -213,23 +213,23 @@ void main() {
vec4(hit.dimming * sphere_color.rgb, sphere_color.a) vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
); );
highlight_next = highlight_list[hit.id]; highlight_next = highlight_list[hit.id];
// highlight intersections // highlight intersections
float ixn_dist = intersection_dist(frag, frag_next); float ixn_dist = intersection_dist(frag, frag_next);
float max_highlight = max(highlight, highlight_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)); 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.color = mix(frag.color, vec4(1.), ixn_highlight);
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight); frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
// highlight cusps // highlight cusps
float cusp_cos = abs(dot(dir, frag.normal)); float cusp_cos = abs(dot(dir, frag.normal));
float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s); 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)); 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); frag.color = mix(frag.color, vec4(1.), cusp_highlight);
// composite the current fragment // composite the current fragment
color = mix(color, frag.color.rgb, frag.color.a); color = mix(color, frag.color.rgb, frag.color.a);
} }
color = mix(color, frag_next.color.rgb, frag_next.color.a); color = mix(color, frag_next.color.rgb, frag_next.color.a);
outColor = vec4(sRGB(color), 1.); outColor = vec4(sRGB(color), 1.);
} }

View file

@ -6,17 +6,17 @@ use web_sys::{console, wasm_bindgen::JsValue};
use crate::{ use crate::{
AppState, AppState,
engine,
engine::DescentHistory,
assembly::{ assembly::{
Assembly, Assembly,
Element, Element,
ElementColor, ElementColor,
InversiveDistanceRegulator, InversiveDistanceRegulator,
Point, Point,
Sphere, Sphere
}, },
engine, specified::SpecifiedValue
engine::DescentHistory,
specified::SpecifiedValue,
}; };
// --- loaders --- // --- loaders ---
@ -26,13 +26,13 @@ use crate::{
// done more work on saving and loading assemblies, we should come back to this // done more work on saving and loading assemblies, we should come back to this
// code to see if it can be simplified // code to see if it can be simplified
fn load_general(assembly: &Assembly) { fn load_gen_assemb(assembly: &Assembly) {
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Sphere::new( Sphere::new(
String::from("gemini_a"), String::from("gemini_a"),
String::from("Castor"), String::from("Castor"),
[1.00_f32, 0.25_f32, 0.00_f32], [1.00_f32, 0.25_f32, 0.00_f32],
engine::sphere(0.5, 0.5, 0.0, 1.0), engine::sphere(0.5, 0.5, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -40,7 +40,7 @@ fn load_general(assembly: &Assembly) {
String::from("gemini_b"), String::from("gemini_b"),
String::from("Pollux"), String::from("Pollux"),
[0.00_f32, 0.25_f32, 1.00_f32], [0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere(-0.5, -0.5, 0.0, 1.0), engine::sphere(-0.5, -0.5, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -48,7 +48,7 @@ fn load_general(assembly: &Assembly) {
String::from("ursa_major"), String::from("ursa_major"),
String::from("Ursa major"), String::from("Ursa major"),
[0.25_f32, 0.00_f32, 1.00_f32], [0.25_f32, 0.00_f32, 1.00_f32],
engine::sphere(-0.5, 0.5, 0.0, 0.75), engine::sphere(-0.5, 0.5, 0.0, 0.75)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -56,7 +56,7 @@ fn load_general(assembly: &Assembly) {
String::from("ursa_minor"), String::from("ursa_minor"),
String::from("Ursa minor"), String::from("Ursa minor"),
[0.25_f32, 1.00_f32, 0.00_f32], [0.25_f32, 1.00_f32, 0.00_f32],
engine::sphere(0.5, -0.5, 0.0, 0.5), engine::sphere(0.5, -0.5, 0.0, 0.5)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -64,7 +64,7 @@ fn load_general(assembly: &Assembly) {
String::from("moon_deimos"), String::from("moon_deimos"),
String::from("Deimos"), String::from("Deimos"),
[0.75_f32, 0.75_f32, 0.00_f32], [0.75_f32, 0.75_f32, 0.00_f32],
engine::sphere(0.0, 0.15, 1.0, 0.25), engine::sphere(0.0, 0.15, 1.0, 0.25)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -72,12 +72,12 @@ fn load_general(assembly: &Assembly) {
String::from("moon_phobos"), String::from("moon_phobos"),
String::from("Phobos"), String::from("Phobos"),
[0.00_f32, 0.75_f32, 0.50_f32], [0.00_f32, 0.75_f32, 0.50_f32],
engine::sphere(0.0, -0.15, -1.0, 0.25), engine::sphere(0.0, -0.15, -1.0, 0.25)
) )
); );
} }
fn load_low_curvature(assembly: &Assembly) { fn load_low_curv_assemb(assembly: &Assembly) {
// create the spheres // create the spheres
let a = 0.75_f64.sqrt(); let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -85,7 +85,7 @@ fn load_low_curvature(assembly: &Assembly) {
"central".to_string(), "central".to_string(),
"Central".to_string(), "Central".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0), engine::sphere(0.0, 0.0, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -93,7 +93,7 @@ fn load_low_curvature(assembly: &Assembly) {
"assemb_plane".to_string(), "assemb_plane".to_string(),
"Assembly plane".to_string(), "Assembly plane".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -101,7 +101,7 @@ fn load_low_curvature(assembly: &Assembly) {
"side1".to_string(), "side1".to_string(),
"Side 1".to_string(), "Side 1".to_string(),
[1.00_f32, 0.00_f32, 0.25_f32], [1.00_f32, 0.00_f32, 0.25_f32],
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -109,7 +109,7 @@ fn load_low_curvature(assembly: &Assembly) {
"side2".to_string(), "side2".to_string(),
"Side 2".to_string(), "Side 2".to_string(),
[0.25_f32, 1.00_f32, 0.00_f32], [0.25_f32, 1.00_f32, 0.00_f32],
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -117,7 +117,7 @@ fn load_low_curvature(assembly: &Assembly) {
"side3".to_string(), "side3".to_string(),
"Side 3".to_string(), "Side 3".to_string(),
[0.00_f32, 0.25_f32, 1.00_f32], [0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -125,7 +125,7 @@ fn load_low_curvature(assembly: &Assembly) {
"corner1".to_string(), "corner1".to_string(),
"Corner 1".to_string(), "Corner 1".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -133,7 +133,7 @@ fn load_low_curvature(assembly: &Assembly) {
"corner2".to_string(), "corner2".to_string(),
"Corner 2".to_string(), "Corner 2".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [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), engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -141,10 +141,10 @@ fn load_low_curvature(assembly: &Assembly) {
String::from("corner3"), String::from("corner3"),
String::from("Corner 3"), String::from("Corner 3"),
[0.75_f32, 0.75_f32, 0.75_f32], [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), engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
) )
); );
// impose the desired tangencies and make the sides planar // impose the desired tangencies and make the sides planar
let index_range = 1..=3; let index_range = 1..=3;
let [central, assemb_plane] = ["central", "assemb_plane"].map( let [central, assemb_plane] = ["central", "assemb_plane"].map(
@ -196,13 +196,13 @@ fn load_low_curvature(assembly: &Assembly) {
} }
} }
fn load_pointed(assembly: &Assembly) { fn load_pointed_assemb(assembly: &Assembly) {
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Point::new( Point::new(
format!("point_front"), format!("point_front"),
format!("Front point"), format!("Front point"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::point(0.0, 0.0, FRAC_1_SQRT_2), engine::point(0.0, 0.0, FRAC_1_SQRT_2)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -210,29 +210,29 @@ fn load_pointed(assembly: &Assembly) {
format!("point_back"), format!("point_back"),
format!("Back point"), format!("Back point"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::point(0.0, 0.0, -FRAC_1_SQRT_2), engine::point(0.0, 0.0, -FRAC_1_SQRT_2)
) )
); );
for index_x in 0..=1 { for index_x in 0..=1 {
for index_y in 0..=1 { for index_y in 0..=1 {
let x = index_x as f64 - 0.5; let x = index_x as f64 - 0.5;
let y = index_y as f64 - 0.5; let y = index_y as f64 - 0.5;
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Sphere::new( Sphere::new(
format!("sphere{index_x}{index_y}"), format!("sphere{index_x}{index_y}"),
format!("Sphere {index_x}{index_y}"), format!("Sphere {index_x}{index_y}"),
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
engine::sphere(x, y, 0.0, 1.0), engine::sphere(x, y, 0.0, 1.0)
) )
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Point::new( Point::new(
format!("point{index_x}{index_y}"), format!("point{index_x}{index_y}"),
format!("Point {index_x}{index_y}"), format!("Point {index_x}{index_y}"),
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
engine::point(x, y, 0.0), engine::point(x, y, 0.0)
) )
); );
} }
@ -246,7 +246,7 @@ fn load_pointed(assembly: &Assembly) {
// B-C " // B-C "
// C-C " // C-C "
// A-C -0.25 * φ^2 = -0.6545084971874737 // A-C -0.25 * φ^2 = -0.6545084971874737
fn load_tridiminished_icosahedron(assembly: &Assembly) { fn load_tridim_icosahedron_assemb(assembly: &Assembly) {
// create the vertices // create the vertices
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32];
const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
@ -256,61 +256,61 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
"a1".to_string(), "a1".to_string(),
"A₁".to_string(), "A₁".to_string(),
COLOR_A, COLOR_A,
engine::point(0.25, 0.75, 0.75), engine::point(0.25, 0.75, 0.75)
), ),
Point::new( Point::new(
"a2".to_string(), "a2".to_string(),
"A₂".to_string(), "A₂".to_string(),
COLOR_A, COLOR_A,
engine::point(0.75, 0.25, 0.75), engine::point(0.75, 0.25, 0.75)
), ),
Point::new( Point::new(
"a3".to_string(), "a3".to_string(),
"A₃".to_string(), "A₃".to_string(),
COLOR_A, COLOR_A,
engine::point(0.75, 0.75, 0.25), engine::point(0.75, 0.75, 0.25)
), ),
Point::new( Point::new(
"b1".to_string(), "b1".to_string(),
"B₁".to_string(), "B₁".to_string(),
COLOR_B, COLOR_B,
engine::point(0.75, -0.25, -0.25), engine::point(0.75, -0.25, -0.25)
), ),
Point::new( Point::new(
"b2".to_string(), "b2".to_string(),
"B₂".to_string(), "B₂".to_string(),
COLOR_B, COLOR_B,
engine::point(-0.25, 0.75, -0.25), engine::point(-0.25, 0.75, -0.25)
), ),
Point::new( Point::new(
"b3".to_string(), "b3".to_string(),
"B₃".to_string(), "B₃".to_string(),
COLOR_B, COLOR_B,
engine::point(-0.25, -0.25, 0.75), engine::point(-0.25, -0.25, 0.75)
), ),
Point::new( Point::new(
"c1".to_string(), "c1".to_string(),
"C₁".to_string(), "C₁".to_string(),
COLOR_C, COLOR_C,
engine::point(0.0, -1.0, -1.0), engine::point(0.0, -1.0, -1.0)
), ),
Point::new( Point::new(
"c2".to_string(), "c2".to_string(),
"C₂".to_string(), "C₂".to_string(),
COLOR_C, COLOR_C,
engine::point(-1.0, 0.0, -1.0), engine::point(-1.0, 0.0, -1.0)
), ),
Point::new( Point::new(
"c3".to_string(), "c3".to_string(),
"C₃".to_string(), "C₃".to_string(),
COLOR_C, COLOR_C,
engine::point(-1.0, -1.0, 0.0), engine::point(-1.0, -1.0, 0.0)
), )
]; ];
for vertex in vertices { for vertex in vertices {
let _ = assembly.try_insert_element(vertex); let _ = assembly.try_insert_element(vertex);
} }
// create the faces // create the faces
const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt(); let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt();
@ -320,26 +320,26 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
"face1".to_string(), "face1".to_string(),
"Face 1".to_string(), "Face 1".to_string(),
COLOR_FACE, COLOR_FACE,
engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0)
), ),
Sphere::new( Sphere::new(
"face2".to_string(), "face2".to_string(),
"Face 2".to_string(), "Face 2".to_string(),
COLOR_FACE, COLOR_FACE,
engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0)
), ),
Sphere::new( Sphere::new(
"face3".to_string(), "face3".to_string(),
"Face 3".to_string(), "Face 3".to_string(),
COLOR_FACE, COLOR_FACE,
engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0), engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0)
), )
]; ];
for face in faces { for face in faces {
face.ghost().set(true); face.ghost().set(true);
let _ = assembly.try_insert_element(face); let _ = assembly.try_insert_element(face);
} }
let index_range = 1..=3; let index_range = 1..=3;
for j in index_range.clone() { for j in index_range.clone() {
// make each face planar // make each face planar
@ -352,7 +352,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
curvature_regulator.set_point().set( curvature_regulator.set_point().set(
SpecifiedValue::try_from("0".to_string()).unwrap() SpecifiedValue::try_from("0".to_string()).unwrap()
); );
// put each A vertex on the face it belongs to // put each A vertex on the face it belongs to
let vertex_a = assembly.elements_by_id.with_untracked( let vertex_a = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("a{j}")].clone() |elts_by_id| elts_by_id[&format!("a{j}")].clone()
@ -360,7 +360,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]); let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]);
incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(incidence_a)); assembly.insert_regulator(Rc::new(incidence_a));
// regulate the B-C vertex distances // regulate the B-C vertex distances
let vertices_bc = ["b", "c"].map( let vertices_bc = ["b", "c"].map(
|series| assembly.elements_by_id.with_untracked( |series| assembly.elements_by_id.with_untracked(
@ -370,10 +370,10 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
assembly.insert_regulator( assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(vertices_bc)) Rc::new(InversiveDistanceRegulator::new(vertices_bc))
); );
// get the pair of indices adjacent to `j` // get the pair of indices adjacent to `j`
let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1]; let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1];
for k in adjacent_indices.clone() { for k in adjacent_indices.clone() {
for series in ["b", "c"] { for series in ["b", "c"] {
// put each B and C vertex on the faces it belongs to // put each B and C vertex on the faces it belongs to
@ -383,14 +383,14 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]); let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]);
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(incidence)); assembly.insert_regulator(Rc::new(incidence));
// regulate the A-B and A-C vertex distances // regulate the A-B and A-C vertex distances
assembly.insert_regulator( assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex])) Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex]))
); );
} }
} }
// regulate the A-A and C-C vertex distances // regulate the A-A and C-C vertex distances
let adjacent_pairs = ["a", "c"].map( let adjacent_pairs = ["a", "c"].map(
|series| adjacent_indices.map( |series| adjacent_indices.map(
@ -409,27 +409,27 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
// to finish describing the dodecahedral circle packing, set the inversive // to finish describing the dodecahedral circle packing, set the inversive
// distance regulators to -1. some of the regulators have already been set // distance regulators to -1. some of the regulators have already been set
fn load_dodecahedral_packing(assembly: &Assembly) { fn load_dodeca_packing_assemb(assembly: &Assembly) {
// add the substrate // add the substrate
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Sphere::new( Sphere::new(
"substrate".to_string(), "substrate".to_string(),
"Substrate".to_string(), "Substrate".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0), engine::sphere(0.0, 0.0, 0.0, 1.0)
) )
); );
let substrate = assembly.elements_by_id.with_untracked( let substrate = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id["substrate"].clone() |elts_by_id| elts_by_id["substrate"].clone()
); );
// fix the substrate's curvature // fix the substrate's curvature
substrate.regulators().with_untracked( substrate.regulators().with_untracked(
|regs| regs.first().unwrap().clone() |regs| regs.first().unwrap().clone()
).set_point().set( ).set_point().set(
SpecifiedValue::try_from("0.5".to_string()).unwrap() SpecifiedValue::try_from("0.5".to_string()).unwrap()
); );
// add the circles to be packed // add the circles to be packed
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32]; const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32];
const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32]; const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32];
@ -445,10 +445,10 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
for k in 0..2 { for k in 0..2 {
let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0); let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0);
let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi; let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi;
let id_num = format!("{j}{k}"); let id_num = format!("{j}{k}");
let label_sub = format!("{}{}", subscripts[j], subscripts[k]); let label_sub = format!("{}{}", subscripts[j], subscripts[k]);
// add the A face // add the A face
let id_a = format!("a{id_num}"); let id_a = format!("a{id_num}");
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -456,7 +456,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
id_a.clone(), id_a.clone(),
format!("A{label_sub}"), format!("A{label_sub}"),
COLOR_A, COLOR_A,
engine::sphere(0.0, small_coord, big_coord, face_radii[k]), engine::sphere(0.0, small_coord, big_coord, face_radii[k])
) )
); );
faces.push( faces.push(
@ -464,7 +464,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|elts_by_id| elts_by_id[&id_a].clone() |elts_by_id| elts_by_id[&id_a].clone()
) )
); );
// add the B face // add the B face
let id_b = format!("b{id_num}"); let id_b = format!("b{id_num}");
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -472,7 +472,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
id_b.clone(), id_b.clone(),
format!("B{label_sub}"), format!("B{label_sub}"),
COLOR_B, COLOR_B,
engine::sphere(small_coord, big_coord, 0.0, face_radii[k]), engine::sphere(small_coord, big_coord, 0.0, face_radii[k])
) )
); );
faces.push( faces.push(
@ -480,7 +480,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|elts_by_id| elts_by_id[&id_b].clone() |elts_by_id| elts_by_id[&id_b].clone()
) )
); );
// add the C face // add the C face
let id_c = format!("c{id_num}"); let id_c = format!("c{id_num}");
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -488,7 +488,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
id_c.clone(), id_c.clone(),
format!("C{label_sub}"), format!("C{label_sub}"),
COLOR_C, COLOR_C,
engine::sphere(big_coord, 0.0, small_coord, face_radii[k]), engine::sphere(big_coord, 0.0, small_coord, face_radii[k])
) )
); );
faces.push( faces.push(
@ -498,14 +498,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
); );
} }
} }
// make each face sphere perpendicular to the substrate // make each face sphere perpendicular to the substrate
for face in faces { for face in faces {
let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]); let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]);
right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(right_angle)); assembly.insert_regulator(Rc::new(right_angle));
} }
// set up the tangencies that define the packing // set up the tangencies that define the packing
for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] { for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] {
for k in 0..2 { for k in 0..2 {
@ -524,14 +524,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
) )
) )
); );
// set up the short-edge tangency // set up the short-edge tangency
let short_tangency = InversiveDistanceRegulator::new(short_edge.clone()); let short_tangency = InversiveDistanceRegulator::new(short_edge.clone());
if k == 0 { if k == 0 {
short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
} }
assembly.insert_regulator(Rc::new(short_tangency)); assembly.insert_regulator(Rc::new(short_tangency));
// set up the side tangencies // set up the side tangencies
for i in 0..2 { for i in 0..2 {
for j in 0..2 { for j in 0..2 {
@ -550,7 +550,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
// the initial configuration of this test assembly deliberately violates the // the initial configuration of this test assembly deliberately violates the
// constraints, so loading the assembly will trigger a non-trivial realization // constraints, so loading the assembly will trigger a non-trivial realization
fn load_balanced(assembly: &Assembly) { fn load_balanced_assemb(assembly: &Assembly) {
// create the spheres // create the spheres
const R_OUTER: f64 = 10.0; const R_OUTER: f64 = 10.0;
const R_INNER: f64 = 4.0; const R_INNER: f64 = 4.0;
@ -559,37 +559,37 @@ fn load_balanced(assembly: &Assembly) {
"outer".to_string(), "outer".to_string(),
"Outer".to_string(), "Outer".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, R_OUTER), engine::sphere(0.0, 0.0, 0.0, R_OUTER)
), ),
Sphere::new( Sphere::new(
"a".to_string(), "a".to_string(),
"A".to_string(), "A".to_string(),
[1.00_f32, 0.00_f32, 0.25_f32], [1.00_f32, 0.00_f32, 0.25_f32],
engine::sphere(0.0, 4.0, 0.0, R_INNER), engine::sphere(0.0, 4.0, 0.0, R_INNER)
), ),
Sphere::new( Sphere::new(
"b".to_string(), "b".to_string(),
"B".to_string(), "B".to_string(),
[0.00_f32, 0.25_f32, 1.00_f32], [0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere(0.0, -4.0, 0.0, R_INNER), engine::sphere(0.0, -4.0, 0.0, R_INNER)
), ),
]; ];
for sphere in spheres { for sphere in spheres {
let _ = assembly.try_insert_element(sphere); let _ = assembly.try_insert_element(sphere);
} }
// get references to the spheres // get references to the spheres
let [outer, a, b] = ["outer", "a", "b"].map( let [outer, a, b] = ["outer", "a", "b"].map(
|id| assembly.elements_by_id.with_untracked( |id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[id].clone() |elts_by_id| elts_by_id[id].clone()
) )
); );
// fix the diameters of the outer, sun, and moon spheres // fix the diameters of the outer, sun, and moon spheres
for (sphere, radius) in [ for (sphere, radius) in [
(outer.clone(), R_OUTER), (outer.clone(), R_OUTER),
(a.clone(), R_INNER), (a.clone(), R_INNER),
(b.clone(), R_INNER), (b.clone(), R_INNER)
] { ] {
let curvature_regulator = sphere.regulators().with_untracked( let curvature_regulator = sphere.regulators().with_untracked(
|regs| regs.first().unwrap().clone() |regs| regs.first().unwrap().clone()
@ -599,7 +599,7 @@ fn load_balanced(assembly: &Assembly) {
SpecifiedValue::try_from(curvature.to_string()).unwrap() SpecifiedValue::try_from(curvature.to_string()).unwrap()
); );
} }
// set the inversive distances between the spheres. as described above, the // set the inversive distances between the spheres. as described above, the
// initial configuration deliberately violates these constraints // initial configuration deliberately violates these constraints
for inner in [a, b] { for inner in [a, b] {
@ -611,14 +611,14 @@ fn load_balanced(assembly: &Assembly) {
// the initial configuration of this test assembly deliberately violates the // the initial configuration of this test assembly deliberately violates the
// constraints, so loading the assembly will trigger a non-trivial realization // constraints, so loading the assembly will trigger a non-trivial realization
fn load_off_center(assembly: &Assembly) { fn load_off_center_assemb(assembly: &Assembly) {
// create a point almost at the origin and a sphere centered on the origin // create a point almost at the origin and a sphere centered on the origin
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Point::new( Point::new(
"point".to_string(), "point".to_string(),
"Point".to_string(), "Point".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::point(1e-9, 0.0, 0.0), engine::point(1e-9, 0.0, 0.0)
), ),
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
@ -626,17 +626,17 @@ fn load_off_center(assembly: &Assembly) {
"sphere".to_string(), "sphere".to_string(),
"Sphere".to_string(), "Sphere".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0), engine::sphere(0.0, 0.0, 0.0, 1.0)
), ),
); );
// get references to the elements // get references to the elements
let point_and_sphere = ["point", "sphere"].map( let point_and_sphere = ["point", "sphere"].map(
|id| assembly.elements_by_id.with_untracked( |id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[id].clone() |elts_by_id| elts_by_id[id].clone()
) )
); );
// put the point on the sphere // put the point on the sphere
let incidence = InversiveDistanceRegulator::new(point_and_sphere); let incidence = InversiveDistanceRegulator::new(point_and_sphere);
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
@ -648,9 +648,9 @@ fn load_off_center(assembly: &Assembly) {
// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an // sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an
// inversive distance of -1 between the insphere and each face, and then set an // inversive distance of -1 between the insphere and each face, and then set an
// inversive distance of 0 between the circumsphere and each vertex // inversive distance of 0 between the circumsphere and each vertex
fn load_radius_ratio(assembly: &Assembly) { fn load_radius_ratio_assemb(assembly: &Assembly) {
let index_range = 1..=4; let index_range = 1..=4;
// create the spheres // create the spheres
const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
let spheres = [ let spheres = [
@ -658,19 +658,19 @@ fn load_radius_ratio(assembly: &Assembly) {
"sphere_faces".to_string(), "sphere_faces".to_string(),
"Insphere".to_string(), "Insphere".to_string(),
GRAY, GRAY,
engine::sphere(0.0, 0.0, 0.0, 0.5), engine::sphere(0.0, 0.0, 0.0, 0.5)
), ),
Sphere::new( Sphere::new(
"sphere_vertices".to_string(), "sphere_vertices".to_string(),
"Circumsphere".to_string(), "Circumsphere".to_string(),
GRAY, GRAY,
engine::sphere(0.0, 0.0, 0.0, 0.25), engine::sphere(0.0, 0.0, 0.0, 0.25)
), )
]; ];
for sphere in spheres { for sphere in spheres {
let _ = assembly.try_insert_element(sphere); let _ = assembly.try_insert_element(sphere);
} }
// create the vertices // create the vertices
let vertices = izip!( let vertices = izip!(
index_range.clone(), index_range.clone(),
@ -678,13 +678,13 @@ fn load_radius_ratio(assembly: &Assembly) {
[1.00_f32, 0.50_f32, 0.75_f32], [1.00_f32, 0.50_f32, 0.75_f32],
[1.00_f32, 0.75_f32, 0.50_f32], [1.00_f32, 0.75_f32, 0.50_f32],
[1.00_f32, 1.00_f32, 0.50_f32], [1.00_f32, 1.00_f32, 0.50_f32],
[0.75_f32, 0.50_f32, 1.00_f32], [0.75_f32, 0.50_f32, 1.00_f32]
].into_iter(), ].into_iter(),
[ [
engine::point(-0.6, -0.8, -0.6), engine::point(-0.6, -0.8, -0.6),
engine::point(-0.6, 0.8, 0.6), engine::point(-0.6, 0.8, 0.6),
engine::point(0.6, -0.8, 0.6), engine::point(0.6, -0.8, 0.6),
engine::point(0.6, 0.8, -0.6), engine::point(0.6, 0.8, -0.6)
].into_iter() ].into_iter()
).map( ).map(
|(k, color, representation)| { |(k, color, representation)| {
@ -692,14 +692,14 @@ fn load_radius_ratio(assembly: &Assembly) {
format!("v{k}"), format!("v{k}"),
format!("Vertex {k}"), format!("Vertex {k}"),
color, color,
representation, representation
) )
} }
); );
for vertex in vertices { for vertex in vertices {
let _ = assembly.try_insert_element(vertex); let _ = assembly.try_insert_element(vertex);
} }
// create the faces // create the faces
let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize(); let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize();
let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6)); let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6));
@ -709,13 +709,13 @@ fn load_radius_ratio(assembly: &Assembly) {
[1.00_f32, 0.00_f32, 0.25_f32], [1.00_f32, 0.00_f32, 0.25_f32],
[1.00_f32, 0.25_f32, 0.00_f32], [1.00_f32, 0.25_f32, 0.00_f32],
[0.75_f32, 0.75_f32, 0.00_f32], [0.75_f32, 0.75_f32, 0.00_f32],
[0.25_f32, 0.00_f32, 1.00_f32], [0.25_f32, 0.00_f32, 1.00_f32]
].into_iter(), ].into_iter(),
[ [
engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0),
engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0),
engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0),
engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0), engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0)
].into_iter() ].into_iter()
).map( ).map(
|(k, color, representation)| { |(k, color, representation)| {
@ -723,7 +723,7 @@ fn load_radius_ratio(assembly: &Assembly) {
format!("f{k}"), format!("f{k}"),
format!("Face {k}"), format!("Face {k}"),
color, color,
representation, representation
) )
} }
); );
@ -731,18 +731,18 @@ fn load_radius_ratio(assembly: &Assembly) {
face.ghost().set(true); face.ghost().set(true);
let _ = assembly.try_insert_element(face); let _ = assembly.try_insert_element(face);
} }
// impose the constraints // impose the constraints
for j in index_range.clone() { for j in index_range.clone() {
let [face_j, vertex_j] = [ let [face_j, vertex_j] = [
format!("f{j}"), format!("f{j}"),
format!("v{j}"), format!("v{j}")
].map( ].map(
|id| assembly.elements_by_id.with_untracked( |id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id].clone() |elts_by_id| elts_by_id[&id].clone()
) )
); );
// make the faces planar // make the faces planar
let curvature_regulator = face_j.regulators().with_untracked( let curvature_regulator = face_j.regulators().with_untracked(
|regs| regs.first().unwrap().clone() |regs| regs.first().unwrap().clone()
@ -750,12 +750,12 @@ fn load_radius_ratio(assembly: &Assembly) {
curvature_regulator.set_point().set( curvature_regulator.set_point().set(
SpecifiedValue::try_from("0".to_string()).unwrap() SpecifiedValue::try_from("0".to_string()).unwrap()
); );
for k in index_range.clone().filter(|&index| index != j) { for k in index_range.clone().filter(|&index| index != j) {
let vertex_k = assembly.elements_by_id.with_untracked( let vertex_k = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("v{k}")].clone() |elts_by_id| elts_by_id[&format!("v{k}")].clone()
); );
// fix the distances between the vertices // fix the distances between the vertices
if j < k { if j < k {
let distance_regulator = InversiveDistanceRegulator::new( let distance_regulator = InversiveDistanceRegulator::new(
@ -763,7 +763,7 @@ fn load_radius_ratio(assembly: &Assembly) {
); );
assembly.insert_regulator(Rc::new(distance_regulator)); assembly.insert_regulator(Rc::new(distance_regulator));
} }
// put the vertices on the faces // put the vertices on the faces
let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]); let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]);
incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
@ -789,7 +789,7 @@ fn load_radius_ratio(assembly: &Assembly) {
// conditions are exactly representable as floats, unlike the analogous numbers // conditions are exactly representable as floats, unlike the analogous numbers
// in the scaled-up problem. the inexact representations might break the // in the scaled-up problem. the inexact representations might break the
// symmetry that's getting the engine stuck // symmetry that's getting the engine stuck
fn load_irisawa_hexlet(assembly: &Assembly) { fn load_irisawa_hexlet_assemb(assembly: &Assembly) {
let index_range = 1..=6; let index_range = 1..=6;
let colors = [ let colors = [
[1.00_f32, 0.00_f32, 0.25_f32], [1.00_f32, 0.00_f32, 0.25_f32],
@ -797,28 +797,28 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
[0.75_f32, 0.75_f32, 0.00_f32], [0.75_f32, 0.75_f32, 0.00_f32],
[0.25_f32, 1.00_f32, 0.00_f32], [0.25_f32, 1.00_f32, 0.00_f32],
[0.00_f32, 0.25_f32, 1.00_f32], [0.00_f32, 0.25_f32, 1.00_f32],
[0.25_f32, 0.00_f32, 1.00_f32], [0.25_f32, 0.00_f32, 1.00_f32]
].into_iter(); ].into_iter();
// create the spheres // create the spheres
let spheres = [ let spheres = [
Sphere::new( Sphere::new(
"outer".to_string(), "outer".to_string(),
"Outer".to_string(), "Outer".to_string(),
[0.5_f32, 0.5_f32, 0.5_f32], [0.5_f32, 0.5_f32, 0.5_f32],
engine::sphere(0.0, 0.0, 0.0, 1.5), engine::sphere(0.0, 0.0, 0.0, 1.5)
), ),
Sphere::new( Sphere::new(
"sun".to_string(), "sun".to_string(),
"Sun".to_string(), "Sun".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, -0.75, 0.0, 0.75), engine::sphere(0.0, -0.75, 0.0, 0.75)
), ),
Sphere::new( Sphere::new(
"moon".to_string(), "moon".to_string(),
"Moon".to_string(), "Moon".to_string(),
[0.25_f32, 0.25_f32, 0.25_f32], [0.25_f32, 0.25_f32, 0.25_f32],
engine::sphere(0.0, 0.75, 0.0, 0.75), engine::sphere(0.0, 0.75, 0.0, 0.75)
), ),
].into_iter().chain( ].into_iter().chain(
index_range.clone().zip(colors).map( index_range.clone().zip(colors).map(
@ -828,7 +828,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
format!("chain{k}"), format!("chain{k}"),
format!("Chain {k}"), format!("Chain {k}"),
color, color,
engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5), engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5)
) )
} }
) )
@ -836,7 +836,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
for sphere in spheres { for sphere in spheres {
let _ = assembly.try_insert_element(sphere); let _ = assembly.try_insert_element(sphere);
} }
// put the outer sphere in ghost mode and fix its curvature // put the outer sphere in ghost mode and fix its curvature
let outer = assembly.elements_by_id.with_untracked( let outer = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id["outer"].clone() |elts_by_id| elts_by_id["outer"].clone()
@ -848,7 +848,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
outer_curvature_regulator.set_point().set( outer_curvature_regulator.set_point().set(
SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap() SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap()
); );
// impose the desired tangencies // impose the desired tangencies
let [outer, sun, moon] = ["outer", "sun", "moon"].map( let [outer, sun, moon] = ["outer", "sun", "moon"].map(
|id| assembly.elements_by_id.with_untracked( |id| assembly.elements_by_id.with_untracked(
@ -865,18 +865,18 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
(outer.clone(), "1"), (outer.clone(), "1"),
(sun.clone(), "-1"), (sun.clone(), "-1"),
(moon.clone(), "-1"), (moon.clone(), "-1"),
(chain_sphere_next.clone(), "-1"), (chain_sphere_next.clone(), "-1")
] { ] {
let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]);
tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap());
assembly.insert_regulator(Rc::new(tangency)); assembly.insert_regulator(Rc::new(tangency));
} }
} }
let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]); let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]);
outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(outer_sun_tangency)); assembly.insert_regulator(Rc::new(outer_sun_tangency));
let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]); let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]);
outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(outer_moon_tangency)); assembly.insert_regulator(Rc::new(outer_moon_tangency));
@ -895,47 +895,53 @@ pub fn TestAssemblyChooser() -> View {
console::log_1( console::log_1(
&JsValue::from(format!("Showing assembly \"{}\"", name.clone())) &JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
); );
batch(|| { batch(|| {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let assembly = &state.assembly; let assembly = &state.assembly;
// pause realization
assembly.keep_realized.set(false);
// clear state // clear state
assembly.regulators.update(|regs| regs.clear()); assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear()); assembly.elements.update(|elts| elts.clear());
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
assembly.descent_history.set(DescentHistory::new()); assembly.descent_history.set(DescentHistory::new());
state.selection.update(|sel| sel.clear()); state.selection.update(|sel| sel.clear());
// load assembly // load assembly
match name.as_str() { match name.as_str() {
"general" => load_general(assembly), "general" => load_gen_assemb(assembly),
"low-curvature" => load_low_curvature(assembly), "low-curv" => load_low_curv_assemb(assembly),
"pointed" => load_pointed(assembly), "pointed" => load_pointed_assemb(assembly),
"tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly), "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly),
"dodecahedral-packing" => load_dodecahedral_packing(assembly), "dodeca-packing" => load_dodeca_packing_assemb(assembly),
"balanced" => load_balanced(assembly), "balanced" => load_balanced_assemb(assembly),
"off-center" => load_off_center(assembly), "off-center" => load_off_center_assemb(assembly),
"radius-ratio" => load_radius_ratio(assembly), "radius-ratio" => load_radius_ratio_assemb(assembly),
"irisawa-hexlet" => load_irisawa_hexlet(assembly), "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly),
_ => (), _ => ()
}; };
// resume realization
assembly.keep_realized.set(true);
}); });
}); });
// build the chooser // build the chooser
view! { view! {
select(bind:value = assembly_name) { select(bind:value=assembly_name) {
option(value = "general") { "General" } option(value="general") { "General" }
option(value = "low-curvature") { "Low-curvature" } option(value="low-curv") { "Low-curvature" }
option(value = "pointed") { "Pointed" } option(value="pointed") { "Pointed" }
option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" } option(value="tridim-icosahedron") { "Tridiminished icosahedron" }
option(value = "dodecahedral-packing") { "Dodecahedral packing" } option(value="dodeca-packing") { "Dodecahedral packing" }
option(value = "balanced") { "Balanced" } option(value="balanced") { "Balanced" }
option(value = "off-center") { "Off-center" } option(value="off-center") { "Off-center" }
option(value = "radius-ratio") { "Radius ratio" } option(value="radius-ratio") { "Radius ratio" }
option(value = "irisawa-hexlet") { "Irisawa hexlet" } option(value="irisawa-hexlet") { "Irisawa hexlet" }
option(value = "empty") { "Empty" } option(value="empty") { "Empty" }
} }
} }
} }

View file

@ -1,6 +1,7 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter}; use std::fmt::{Display, Error, Formatter};
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
// --- elements --- // --- elements ---
@ -16,7 +17,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect
center_y / radius, center_y / radius,
center_z / radius, center_z / radius,
0.5 / radius, 0.5 / radius,
0.5 * (center_norm_sq / radius - radius), 0.5 * (center_norm_sq / radius - radius)
]) ])
} }
@ -30,7 +31,7 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6
norm_sp * dir_y, norm_sp * dir_y,
norm_sp * dir_z, norm_sp * dir_z,
0.5 * curv, 0.5 * curv,
off * (1.0 + 0.5 * off * curv), off * (1.0 + 0.5 * off * curv)
]) ])
} }
@ -49,32 +50,66 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
rep.scale_mut(0.5 / rep[3]); rep.scale_mut(0.5 / rep[3]);
} }
// given a sphere's representation vector, change the sphere's half-curvature to
// `half-curv` and then restore normalization by contracting the representation
// vector toward the curvature axis
pub fn change_half_curvature(rep: &mut DVector<f64>, half_curv: f64) {
// set the sphere's half-curvature to the desired value
rep[3] = half_curv;
// restore normalization by contracting toward the curvature axis
const SIZE_THRESHOLD: f64 = 1e-9;
let half_q_lt = -2.0 * half_curv * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let mut spatial = rep.fixed_rows_mut::<3>(0);
let q_sp = spatial.norm_squared();
if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD {
spatial.copy_from_slice(
&[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()]
);
} else {
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
spatial.scale_mut(1.0 / scaling);
rep[4] /= scaling;
}
/* DEBUG */
// verify normalization
let rep_for_debug = rep.clone();
console::log_1(&JsValue::from(
format!(
"Sphere self-product after curvature change: {}",
rep_for_debug.dot(&(&*Q * &rep_for_debug))
)
));
}
// --- partial matrices --- // --- partial matrices ---
pub struct MatrixEntry { pub struct MatrixEntry {
pub index: (usize, usize), index: (usize, usize),
pub value: f64, value: f64
} }
pub struct PartialMatrix(Vec<MatrixEntry>); pub struct PartialMatrix(Vec<MatrixEntry>);
impl PartialMatrix { impl PartialMatrix {
pub fn new() -> Self { pub fn new() -> PartialMatrix {
Self(Vec::<MatrixEntry>::new()) PartialMatrix(Vec::<MatrixEntry>::new())
} }
pub fn push(&mut self, row: usize, col: usize, value: f64) { pub fn push(&mut self, row: usize, col: usize, value: f64) {
let Self(entries) = self; let PartialMatrix(entries) = self;
entries.push(MatrixEntry { index: (row, col), value }); entries.push(MatrixEntry { index: (row, col), value: value });
} }
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
self.push(row, col, value); self.push(row, col, value);
if row != col { if row != col {
self.push(col, row, value); self.push(col, row, value);
} }
} }
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> { fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = a.clone(); let mut result = a.clone();
for &MatrixEntry { index, value } in self { for &MatrixEntry { index, value } in self {
@ -82,7 +117,7 @@ impl PartialMatrix {
} }
result result
} }
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> { fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols()); let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
for &MatrixEntry { index, .. } in self { for &MatrixEntry { index, .. } in self {
@ -90,7 +125,7 @@ impl PartialMatrix {
} }
result result
} }
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> { fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols()); let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
for &MatrixEntry { index, value } in self { for &MatrixEntry { index, value } in self {
@ -112,9 +147,9 @@ impl Display for PartialMatrix {
impl IntoIterator for PartialMatrix { impl IntoIterator for PartialMatrix {
type Item = MatrixEntry; type Item = MatrixEntry;
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
let Self(entries) = self; let PartialMatrix(entries) = self;
entries.into_iter() entries.into_iter()
} }
} }
@ -122,7 +157,7 @@ impl IntoIterator for PartialMatrix {
impl<'a> IntoIterator for &'a PartialMatrix { impl<'a> IntoIterator for &'a PartialMatrix {
type Item = &'a MatrixEntry; type Item = &'a MatrixEntry;
type IntoIter = std::slice::Iter<'a, MatrixEntry>; type IntoIter = std::slice::Iter<'a, MatrixEntry>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
let PartialMatrix(entries) = self; let PartialMatrix(entries) = self;
entries.into_iter() entries.into_iter()
@ -135,26 +170,22 @@ impl<'a> IntoIterator for &'a PartialMatrix {
pub struct ConfigSubspace { pub struct ConfigSubspace {
assembly_dim: usize, assembly_dim: usize,
basis_std: Vec<DMatrix<f64>>, basis_std: Vec<DMatrix<f64>>,
basis_proj: Vec<DMatrix<f64>>, basis_proj: Vec<DMatrix<f64>>
} }
impl ConfigSubspace { impl ConfigSubspace {
pub fn zero(assembly_dim: usize) -> Self { pub fn zero(assembly_dim: usize) -> ConfigSubspace {
Self { ConfigSubspace {
assembly_dim, assembly_dim: assembly_dim,
basis_proj: Vec::new(), basis_proj: Vec::new(),
basis_std: Vec::new(), basis_std: Vec::new()
} }
} }
// approximate the kernel of a symmetric endomorphism of the configuration // approximate the kernel of a symmetric endomorphism of the configuration
// space for `assembly_dim` elements. we consider an eigenvector to be part // space for `assembly_dim` elements. we consider an eigenvector to be part
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
fn symmetric_kernel( fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
a: DMatrix<f64>,
proj_to_std: DMatrix<f64>,
assembly_dim: usize,
) -> Self {
// find a basis for the kernel. the basis is expressed in the projection // find a basis for the kernel. the basis is expressed in the projection
// coordinates, and it's orthonormal with respect to the projection // coordinates, and it's orthonormal with respect to the projection
// inner product // inner product
@ -167,14 +198,21 @@ impl ConfigSubspace {
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v) |(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
); );
/* DEBUG */
// print the eigenvalues
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
console::log_1(&JsValue::from(
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
));
// express the basis in the standard coordinates // express the basis in the standard coordinates
let basis_std = proj_to_std * &basis_proj; let basis_std = proj_to_std * &basis_proj;
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4; const UNIFORM_DIM: usize = 4;
Self { ConfigSubspace {
assembly_dim, assembly_dim: assembly_dim,
basis_std: basis_std.column_iter().map( basis_std: basis_std.column_iter().map(
|v| Into::<DMatrix<f64>>::into( |v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
@ -184,18 +222,18 @@ impl ConfigSubspace {
|v| Into::<DMatrix<f64>>::into( |v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
) )
).collect(), ).collect()
} }
} }
pub fn dim(&self) -> usize { pub fn dim(&self) -> usize {
self.basis_std.len() self.basis_std.len()
} }
pub fn assembly_dim(&self) -> usize { pub fn assembly_dim(&self) -> usize {
self.assembly_dim self.assembly_dim
} }
// find the projection onto this subspace of the motion where the element // find the projection onto this subspace of the motion where the element
// with the given column index has velocity `v`. the velocity is given in // with the given column index has velocity `v`. the velocity is given in
// projection coordinates, and the projection is done with respect to the // projection coordinates, and the projection is done with respect to the
@ -218,14 +256,14 @@ pub struct DescentHistory {
pub config: Vec<DMatrix<f64>>, pub config: Vec<DMatrix<f64>>,
pub scaled_loss: Vec<f64>, pub scaled_loss: Vec<f64>,
pub neg_grad: Vec<DMatrix<f64>>, pub neg_grad: Vec<DMatrix<f64>>,
pub hess_eigvals: Vec<DVector<f64>>, pub hess_eigvals: Vec::<DVector<f64>>,
pub base_step: Vec<DMatrix<f64>>, pub base_step: Vec<DMatrix<f64>>,
pub backoff_steps: Vec<i32>, pub backoff_steps: Vec<i32>
} }
impl DescentHistory { impl DescentHistory {
pub fn new() -> Self { pub fn new() -> DescentHistory {
Self { DescentHistory {
config: Vec::<DMatrix<f64>>::new(), config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(), scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(), neg_grad: Vec::<DMatrix<f64>>::new(),
@ -245,21 +283,21 @@ pub struct ConstraintProblem {
} }
impl ConstraintProblem { impl ConstraintProblem {
pub fn new(element_count: usize) -> Self { pub fn new(element_count: usize) -> ConstraintProblem {
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
Self { ConstraintProblem {
gram: PartialMatrix::new(), gram: PartialMatrix::new(),
frozen: PartialMatrix::new(), frozen: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count), guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count)
} }
} }
#[cfg(feature = "dev")] #[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self { pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem {
Self { ConstraintProblem {
gram: PartialMatrix::new(), gram: PartialMatrix::new(),
frozen: PartialMatrix::new(), frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns), guess: DMatrix::from_columns(guess_columns)
} }
} }
} }
@ -273,21 +311,25 @@ lazy_static! {
0.0, 1.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, 1.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0,
0.0, 0.0, 0.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0, 0.0
]); ]);
} }
struct SearchState { struct SearchState {
config: DMatrix<f64>, config: DMatrix<f64>,
err_proj: DMatrix<f64>, err_proj: DMatrix<f64>,
loss: f64, loss: f64
} }
impl SearchState { impl SearchState {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self { fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
let loss = err_proj.norm_squared(); let loss = err_proj.norm_squared();
Self { config, err_proj, loss } SearchState {
config: config,
err_proj: err_proj,
loss: loss
}
} }
} }
@ -314,7 +356,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0], curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1], 0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2], 0.0, 0.0, curv, 0.0, v[2],
0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0
]) ])
} else { } else {
// `v` represents a sphere. the normalization condition says that the // `v` represents a sphere. the normalization condition says that the
@ -323,7 +365,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0], curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1], 0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2], 0.0, 0.0, curv, 0.0, v[2],
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0, curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0
]) ])
} }
} }
@ -336,7 +378,7 @@ fn seek_better_config(
base_target_improvement: f64, base_target_improvement: f64,
min_efficiency: f64, min_efficiency: f64,
backoff: f64, backoff: f64,
max_backoff_steps: i32, max_backoff_steps: i32
) -> Option<(SearchState, i32)> { ) -> Option<(SearchState, i32)> {
let mut rate = 1.0; let mut rate = 1.0;
for backoff_steps in 0..max_backoff_steps { for backoff_steps in 0..max_backoff_steps {
@ -353,13 +395,13 @@ fn seek_better_config(
// a first-order neighborhood of a configuration // a first-order neighborhood of a configuration
pub struct ConfigNeighborhood { pub struct ConfigNeighborhood {
#[cfg(feature = "dev")] pub config: DMatrix<f64>, pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace, pub nbhd: ConfigSubspace
} }
pub struct Realization { pub struct Realization {
pub result: Result<ConfigNeighborhood, String>, pub result: Result<ConfigNeighborhood, String>,
pub history: DescentHistory, pub history: DescentHistory
} }
// seek a matrix `config` that matches the partial matrix `problem.frozen` and // seek a matrix `config` that matches the partial matrix `problem.frozen` and
@ -373,41 +415,30 @@ pub fn realize_gram(
backoff: f64, backoff: f64,
reg_scale: f64, reg_scale: f64,
max_descent_steps: i32, max_descent_steps: i32,
max_backoff_steps: i32, max_backoff_steps: i32
) -> Realization { ) -> Realization {
// destructure the problem data // destructure the problem data
let ConstraintProblem { gram, guess, frozen } = problem; let ConstraintProblem {
gram, guess, frozen
} = problem;
// start the descent history // start the descent history
let mut history = DescentHistory::new(); let mut history = DescentHistory::new();
// handle the case where the assembly is empty. our general realization
// routine can't handle this case because it builds the Hessian using
// `DMatrix::from_columns`, which panics when the list of columns is empty
let assembly_dim = guess.ncols();
if assembly_dim == 0 {
let result = Ok(
ConfigNeighborhood {
#[cfg(feature = "dev")] config: guess.clone(),
nbhd: ConfigSubspace::zero(0),
}
);
return Realization { result, history };
}
// find the dimension of the search space // find the dimension of the search space
let element_dim = guess.nrows(); let element_dim = guess.nrows();
let assembly_dim = guess.ncols();
let total_dim = element_dim * assembly_dim; let total_dim = element_dim * assembly_dim;
// scale the tolerance // scale the tolerance
let scale_adjustment = (gram.0.len() as f64).sqrt(); let scale_adjustment = (gram.0.len() as f64).sqrt();
let tol = scale_adjustment * scaled_tol; let tol = scale_adjustment * scaled_tol;
// convert the frozen indices to stacked format // convert the frozen indices to stacked format
let frozen_stacked: Vec<usize> = frozen.into_iter().map( let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|MatrixEntry { index: (row, col), .. }| col*element_dim + row |MatrixEntry { index: (row, col), .. }| col*element_dim + row
).collect(); ).collect();
// use a regularized Newton's method with backtracking // use a regularized Newton's method with backtracking
let mut state = SearchState::from_config(gram, frozen.freeze(guess)); let mut state = SearchState::from_config(gram, frozen.freeze(guess));
let mut hess = DMatrix::zeros(element_dim, assembly_dim); let mut hess = DMatrix::zeros(element_dim, assembly_dim);
@ -416,7 +447,7 @@ pub fn realize_gram(
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj; 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>); let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
history.neg_grad.push(neg_grad.clone()); history.neg_grad.push(neg_grad.clone());
// find the negative Hessian of the loss function // find the negative Hessian of the loss function
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim); let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
for col in 0..assembly_dim { for col in 0..assembly_dim {
@ -435,7 +466,7 @@ pub fn realize_gram(
} }
} }
hess = DMatrix::from_columns(hess_cols.as_slice()); hess = DMatrix::from_columns(hess_cols.as_slice());
// regularize the Hessian // regularize the Hessian
let hess_eigvals = hess.symmetric_eigenvalues(); let hess_eigvals = hess.symmetric_eigenvalues();
let min_eigval = hess_eigvals.min(); let min_eigval = hess_eigvals.min();
@ -443,7 +474,7 @@ pub fn realize_gram(
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
} }
history.hess_eigvals.push(hess_eigvals); history.hess_eigvals.push(hess_eigvals);
// project the negative gradient and negative Hessian onto the // project the negative gradient and negative Hessian onto the
// orthogonal complement of the frozen subspace // orthogonal complement of the frozen subspace
let zero_col = DVector::zeros(total_dim); let zero_col = DVector::zeros(total_dim);
@ -454,12 +485,12 @@ pub fn realize_gram(
hess.set_column(k, &zero_col); hess.set_column(k, &zero_col);
hess[(k, k)] = 1.0; hess[(k, k)] = 1.0;
} }
// stop if the loss is tolerably low // stop if the loss is tolerably low
history.config.push(state.config.clone()); history.config.push(state.config.clone());
history.scaled_loss.push(state.loss / scale_adjustment); history.scaled_loss.push(state.loss / scale_adjustment);
if state.loss < tol { break; } if state.loss < tol { break; }
// compute the Newton step // compute the Newton step
/* TO DO */ /* TO DO */
/* /*
@ -473,26 +504,26 @@ pub fn realize_gram(
Some(cholesky) => cholesky, Some(cholesky) => cholesky,
None => return Realization { None => return Realization {
result: Err("Cholesky decomposition failed".to_string()), result: Err("Cholesky decomposition failed".to_string()),
history, history
}, }
}; };
let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked);
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
history.base_step.push(base_step.clone()); history.base_step.push(base_step.clone());
// use backtracking line search to find a better configuration // use backtracking line search to find a better configuration
if let Some((better_state, backoff_steps)) = seek_better_config( if let Some((better_state, backoff_steps)) = seek_better_config(
gram, &state, &base_step, neg_grad.dot(&base_step), gram, &state, &base_step, neg_grad.dot(&base_step),
min_efficiency, backoff, max_backoff_steps, min_efficiency, backoff, max_backoff_steps
) { ) {
state = better_state; state = better_state;
history.backoff_steps.push(backoff_steps); history.backoff_steps.push(backoff_steps);
} else { } else {
return Realization { return Realization {
result: Err("Line search failed".to_string()), result: Err("Line search failed".to_string()),
history, history
}; }
} };
} }
let result = if state.loss < tol { let result = if state.loss < tol {
// express the uniform basis in the standard basis // express the uniform basis in the standard basis
@ -505,11 +536,11 @@ pub fn realize_gram(
.view_mut(block_start, (element_dim, UNIFORM_DIM)) .view_mut(block_start, (element_dim, UNIFORM_DIM))
.copy_from(&local_unif_to_std(state.config.column(n))); .copy_from(&local_unif_to_std(state.config.column(n)));
} }
// find the kernel of the Hessian. give it the uniform inner product // find the kernel of the Hessian. give it the uniform inner product
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent }) Ok(ConfigNeighborhood { config: state.config, nbhd: tangent })
} else { } else {
Err("Failed to reach target accuracy".to_string()) Err("Failed to reach target accuracy".to_string())
}; };
@ -521,9 +552,9 @@ pub fn realize_gram(
#[cfg(feature = "dev")] #[cfg(feature = "dev")]
pub mod examples { pub mod examples {
use std::f64::consts::PI; use std::f64::consts::PI;
use super::*; use super::*;
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
// below includes a nice translation of the problem statement, which was // below includes a nice translation of the problem statement, which was
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
@ -537,7 +568,7 @@ pub mod examples {
[ [
sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.0), sphere(0.0, 0.0, -9.0, 5.0),
sphere(0.0, 0.0, 11.0, 3.0), sphere(0.0, 0.0, 11.0, 3.0)
].into_iter().chain( ].into_iter().chain(
(1..=6).map( (1..=6).map(
|k| { |k| {
@ -547,40 +578,40 @@ pub mod examples {
) )
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
); );
for s in 0..9 { for s in 0..9 {
// each sphere is represented by a spacelike vector // each sphere is represented by a spacelike vector
problem.gram.push_sym(s, s, 1.0); problem.gram.push_sym(s, s, 1.0);
// the circumscribing sphere is tangent to all of the other // the circumscribing sphere is tangent to all of the other
// spheres, with matching orientation // spheres, with matching orientation
if s > 0 { if s > 0 {
problem.gram.push_sym(0, s, 1.0); problem.gram.push_sym(0, s, 1.0);
} }
if s > 2 { if s > 2 {
// each chain sphere is tangent to the "sun" and "moon" // each chain sphere is tangent to the "sun" and "moon"
// spheres, with opposing orientation // spheres, with opposing orientation
for n in 1..3 { for n in 1..3 {
problem.gram.push_sym(s, n, -1.0); problem.gram.push_sym(s, n, -1.0);
} }
// each chain sphere is tangent to the next chain sphere, // each chain sphere is tangent to the next chain sphere,
// with opposing orientation // with opposing orientation
let s_next = 3 + (s-2) % 6; let s_next = 3 + (s-2) % 6;
problem.gram.push_sym(s, s_next, -1.0); problem.gram.push_sym(s, s_next, -1.0);
} }
} }
// the frozen entries fix the radii of the circumscribing sphere, the // the frozen entries fix the radii of the circumscribing sphere, the
// "sun" and "moon" spheres, and one of the chain spheres // "sun" and "moon" spheres, and one of the chain spheres
for k in 0..4 { for k in 0..4 {
problem.frozen.push(3, k, problem.guess[(3, k)]); problem.frozen.push(3, k, problem.guess[(3, k)]);
} }
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
} }
// set up a kaleidocycle, made of points with fixed distances between them, // set up a kaleidocycle, made of points with fixed distances between them,
// and find its tangent space // and find its tangent space
pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization {
@ -596,12 +627,12 @@ pub mod examples {
point(0.0, 0.0, 0.0), point(0.0, 0.0, 0.0),
point(ang_hor.cos(), ang_hor.sin(), 0.0), point(ang_hor.cos(), ang_hor.sin(), 0.0),
point(x_vert, y_vert, -0.5), point(x_vert, y_vert, -0.5),
point(x_vert, y_vert, 0.5), point(x_vert, y_vert, 0.5)
] ]
} }
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
); );
const N_POINTS: usize = 2 * N_HINGES; const N_POINTS: usize = 2 * N_HINGES;
for block in (0..N_POINTS).step_by(2) { for block in (0..N_POINTS).step_by(2) {
let block_next = (block + 2) % N_POINTS; let block_next = (block + 2) % N_POINTS;
@ -610,18 +641,18 @@ pub mod examples {
for k in j..2 { for k in j..2 {
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
} }
// non-hinge edges // non-hinge edges
for k in 0..2 { for k in 0..2 {
problem.gram.push_sym(block + j, block_next + k, -0.625); problem.gram.push_sym(block + j, block_next + k, -0.625);
} }
} }
} }
for k in 0..N_POINTS { for k in 0..N_POINTS {
problem.frozen.push(3, k, problem.guess[(3, k)]) problem.frozen.push(3, k, problem.guess[(3, k)])
} }
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
} }
} }
@ -630,47 +661,47 @@ pub mod examples {
mod tests { mod tests {
use nalgebra::Vector3; use nalgebra::Vector3;
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter}; use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter};
use super::{*, examples::*}; use super::{*, examples::*};
#[test] #[test]
fn freeze_test() { fn freeze_test() {
let frozen = PartialMatrix(vec![ let frozen = PartialMatrix(vec![
MatrixEntry { index: (0, 0), value: 14.0 }, MatrixEntry { index: (0, 0), value: 14.0 },
MatrixEntry { index: (0, 2), value: 28.0 }, MatrixEntry { index: (0, 2), value: 28.0 },
MatrixEntry { index: (1, 1), value: 42.0 }, MatrixEntry { index: (1, 1), value: 42.0 },
MatrixEntry { index: (1, 2), value: 49.0 }, MatrixEntry { index: (1, 2), value: 49.0 }
]); ]);
let config = DMatrix::<f64>::from_row_slice(2, 3, &[ let config = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
4.0, 5.0, 6.0, 4.0, 5.0, 6.0
]); ]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[ let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
14.0, 2.0, 28.0, 14.0, 2.0, 28.0,
4.0, 42.0, 49.0, 4.0, 42.0, 49.0
]); ]);
assert_eq!(frozen.freeze(&config), expected_result); assert_eq!(frozen.freeze(&config), expected_result);
} }
#[test] #[test]
fn sub_proj_test() { fn sub_proj_test() {
let target = PartialMatrix(vec![ let target = PartialMatrix(vec![
MatrixEntry { index: (0, 0), value: 19.0 }, MatrixEntry { index: (0, 0), value: 19.0 },
MatrixEntry { index: (0, 2), value: 39.0 }, MatrixEntry { index: (0, 2), value: 39.0 },
MatrixEntry { index: (1, 1), value: 59.0 }, MatrixEntry { index: (1, 1), value: 59.0 },
MatrixEntry { index: (1, 2), value: 69.0 }, MatrixEntry { index: (1, 2), value: 69.0 }
]); ]);
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[ let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
4.0, 5.0, 6.0, 4.0, 5.0, 6.0
]); ]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[ let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
18.0, 0.0, 36.0, 18.0, 0.0, 36.0,
0.0, 54.0, 63.0, 0.0, 54.0, 63.0
]); ]);
assert_eq!(target.sub_proj(&attempt), expected_result); assert_eq!(target.sub_proj(&attempt), expected_result);
} }
#[test] #[test]
fn zero_loss_test() { fn zero_loss_test() {
let mut gram = PartialMatrix::new(); let mut gram = PartialMatrix::new();
@ -684,13 +715,13 @@ mod tests {
DMatrix::from_columns(&[ DMatrix::from_columns(&[
sphere(1.0, 0.0, 0.0, a), sphere(1.0, 0.0, 0.0, a),
sphere(-0.5, a, 0.0, a), sphere(-0.5, a, 0.0, a),
sphere(-0.5, -a, 0.0, a), sphere(-0.5, -a, 0.0, a)
]) ])
}; };
let state = SearchState::from_config(&gram, config); let state = SearchState::from_config(&gram, config);
assert!(state.loss.abs() < f64::EPSILON); assert!(state.loss.abs() < f64::EPSILON);
} }
/* TO DO */ /* TO DO */
// at the frozen indices, the optimization steps should have exact zeros, // at the frozen indices, the optimization steps should have exact zeros,
// and the realized configuration should have the desired values // and the realized configuration should have the desired values
@ -698,7 +729,7 @@ mod tests {
fn frozen_entry_test() { fn frozen_entry_test() {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
point(0.0, 0.0, 2.0), point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 0.95), sphere(0.0, 0.0, 0.0, 0.95)
]); ]);
for j in 0..2 { for j in 0..2 {
for k in j..2 { for k in j..2 {
@ -720,13 +751,13 @@ mod tests {
assert_eq!(config[index], value); assert_eq!(config[index], value);
} }
} }
#[test] #[test]
fn irisawa_hexlet_test() { fn irisawa_hexlet_test() {
// solve Irisawa's problem // solve Irisawa's problem
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config;
// check against Irisawa's solution // check against Irisawa's solution
let entry_tol = SCALED_TOL.sqrt(); 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]; 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];
@ -734,7 +765,7 @@ mod tests {
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
} }
} }
#[test] #[test]
fn tangent_test_three_spheres() { fn tangent_test_three_spheres() {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
@ -742,7 +773,7 @@ mod tests {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 0.0, -2.0),
sphere(0.0, 0.0, 1.0, 1.0), sphere(0.0, 0.0, 1.0, 1.0),
sphere(0.0, 0.0, -1.0, 1.0), sphere(0.0, 0.0, -1.0, 1.0)
]); ]);
for j in 0..3 { for j in 0..3 {
for k in j..3 { for k in j..3 {
@ -758,7 +789,7 @@ mod tests {
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
assert_eq!(config, problem.guess); assert_eq!(config, problem.guess);
assert_eq!(history.scaled_loss.len(), 1); assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of // list some motions that should form a basis for the tangent space of
// the solution variety // the solution variety
const UNIFORM_DIM: usize = 4; const UNIFORM_DIM: usize = 4;
@ -772,8 +803,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[ DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, -0.5, -0.5, 0.0, 0.0, -0.5, -0.5,
0.0, 0.0, -0.5, 0.5, 0.0, 0.0, -0.5, 0.5
]), ])
]; ];
let tangent_motions_std = vec![ let tangent_motions_std = vec![
basis_matrix((0, 1), element_dim, assembly_dim), basis_matrix((0, 1), element_dim, assembly_dim),
@ -783,14 +814,14 @@ mod tests {
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[ DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
0.0, 0.0, 0.0, 0.00, 0.0, 0.0, 0.0, 0.0, 0.00, 0.0,
0.0, 0.0, -1.0, -0.25, -1.0, 0.0, 0.0, -1.0, -0.25, -1.0,
0.0, 0.0, -1.0, 0.25, 1.0, 0.0, 0.0, -1.0, 0.25, 1.0
]), ])
]; ];
// confirm that the dimension of the tangent space is no greater than // confirm that the dimension of the tangent space is no greater than
// expected // expected
assert_eq!(tangent.basis_std.len(), tangent_motions_std.len()); assert_eq!(tangent.basis_std.len(), tangent_motions_std.len());
// confirm that the tangent space contains all the motions we expect it // confirm that the tangent space contains all the motions we expect it
// to. since we've already bounded the dimension of the tangent space, // to. since we've already bounded the dimension of the tangent space,
// this confirms that the tangent space is what we expect it to be // this confirms that the tangent space is what we expect it to be
@ -802,13 +833,13 @@ mod tests {
assert!((motion_std - motion_proj).norm_squared() < tol_sq); assert!((motion_std - motion_proj).norm_squared() < tol_sq);
} }
} }
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> { fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
let mut elt_motion = DVector::zeros(4); let mut elt_motion = DVector::zeros(4);
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel); elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
iter::repeat(elt_motion).take(assembly_dim).collect() iter::repeat(elt_motion).take(assembly_dim).collect()
} }
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> { fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
points.into_iter().map( points.into_iter().map(
|pt| { |pt| {
@ -819,7 +850,7 @@ mod tests {
} }
).collect() ).collect()
} }
#[test] #[test]
fn tangent_test_kaleidocycle() { fn tangent_test_kaleidocycle() {
// set up a kaleidocycle and find its tangent space // set up a kaleidocycle and find its tangent space
@ -827,7 +858,7 @@ mod tests {
let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); let Realization { result, history } = realize_kaleidocycle(SCALED_TOL);
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
assert_eq!(history.scaled_loss.len(), 1); assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of // list some motions that should form a basis for the tangent space of
// the solution variety // the solution variety
const N_HINGES: usize = 6; const N_HINGES: usize = 6;
@ -838,12 +869,12 @@ mod tests {
translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim),
translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim), translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim),
translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim),
// the rotations about the coordinate axes // the rotations about the coordinate axes
rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()), rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()),
rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()), rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()),
rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()), rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()),
// the twist motion. more precisely: a motion that keeps the center // the twist motion. more precisely: a motion that keeps the center
// of mass stationary and preserves the distances between the // of mass stationary and preserves the distances between the
// vertices to first order. this has to be the twist as long as: // vertices to first order. this has to be the twist as long as:
@ -860,10 +891,10 @@ mod tests {
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]), DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0])
] ]
} }
).collect::<Vec<_>>(), ).collect::<Vec<_>>()
]; ];
let tangent_motions_std = tangent_motions_unif.iter().map( let tangent_motions_std = tangent_motions_unif.iter().map(
|motion| DMatrix::from_columns( |motion| DMatrix::from_columns(
@ -872,11 +903,11 @@ mod tests {
).collect::<Vec<_>>() ).collect::<Vec<_>>()
) )
).collect::<Vec<_>>(); ).collect::<Vec<_>>();
// confirm that the dimension of the tangent space is no greater than // confirm that the dimension of the tangent space is no greater than
// expected // expected
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len()); assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
// confirm that the tangent space contains all the motions we expect it // confirm that the tangent space contains all the motions we expect it
// to. since we've already bounded the dimension of the tangent space, // to. since we've already bounded the dimension of the tangent space,
// this confirms that the tangent space is what we expect it to be // this confirms that the tangent space is what we expect it to be
@ -888,7 +919,7 @@ mod tests {
assert!((motion_std - motion_proj).norm_squared() < tol_sq); assert!((motion_std - motion_proj).norm_squared() < tol_sq);
} }
} }
fn translation(dis: Vector3<f64>) -> DMatrix<f64> { fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
@ -896,10 +927,10 @@ mod tests {
0.0, 1.0, 0.0, 0.0, dis[1], 0.0, 1.0, 0.0, 0.0, dis[1],
0.0, 0.0, 1.0, 0.0, dis[2], 0.0, 0.0, 1.0, 0.0, dis[2],
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(), 2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0
]) ])
} }
// confirm that projection onto a configuration subspace is equivariant with // confirm that projection onto a configuration subspace is equivariant with
// respect to Euclidean motions // respect to Euclidean motions
#[test] #[test]
@ -908,7 +939,7 @@ mod tests {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let mut problem_orig = ConstraintProblem::from_guess(&[ let mut problem_orig = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.5, 1.0), sphere(0.0, 0.0, 0.5, 1.0),
sphere(0.0, 0.0, -0.5, 1.0), sphere(0.0, 0.0, -0.5, 1.0)
]); ]);
problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(0, 0, 1.0);
problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(1, 1, 1.0);
@ -919,20 +950,20 @@ mod tests {
let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap();
assert_eq!(config_orig, problem_orig.guess); assert_eq!(config_orig, problem_orig.guess);
assert_eq!(history_orig.scaled_loss.len(), 1); assert_eq!(history_orig.scaled_loss.len(), 1);
// find another pair of spheres that meet at 120°. we'll think of this // find another pair of spheres that meet at 120°. we'll think of this
// solution as a transformed version of the original one // solution as a transformed version of the original one
let guess_tfm = { let guess_tfm = {
let a = 0.5 * FRAC_1_SQRT_2; let a = 0.5 * FRAC_1_SQRT_2;
DMatrix::from_columns(&[ DMatrix::from_columns(&[
sphere(a, 0.0, 7.0 + a, 1.0), sphere(a, 0.0, 7.0 + a, 1.0),
sphere(-a, 0.0, 7.0 - a, 1.0), sphere(-a, 0.0, 7.0 - a, 1.0)
]) ])
}; };
let problem_tfm = ConstraintProblem { let problem_tfm = ConstraintProblem {
gram: problem_orig.gram, gram: problem_orig.gram,
frozen: problem_orig.frozen,
guess: guess_tfm, guess: guess_tfm,
frozen: problem_orig.frozen
}; };
let Realization { result: result_tfm, history: history_tfm } = realize_gram( let Realization { result: result_tfm, history: history_tfm } = realize_gram(
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
@ -940,17 +971,17 @@ mod tests {
let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap();
assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(config_tfm, problem_tfm.guess);
assert_eq!(history_tfm.scaled_loss.len(), 1); assert_eq!(history_tfm.scaled_loss.len(), 1);
// project a nudge to the tangent space of the solution variety at the // project a nudge to the tangent space of the solution variety at the
// original solution // original solution
let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0); let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0);
// project the equivalent nudge to the tangent space of the solution // project the equivalent nudge to the tangent space of the solution
// variety at the transformed solution // variety at the transformed solution
let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]); let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]);
let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0); let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
// take the transformation that sends the original solution to the // take the transformation that sends the original solution to the
// transformed solution and apply it to the motion that the original // transformed solution and apply it to the motion that the original
// solution makes in response to the nudge // solution makes in response to the nudge
@ -960,11 +991,11 @@ mod tests {
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0, FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
0.0, 0.0, 0.0, 1.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, 1.0
]); ]);
let transl = translation(Vector3::new(0.0, 0.0, 7.0)); let transl = translation(Vector3::new(0.0, 0.0, 7.0));
let motion_proj_tfm = transl * rot * motion_orig_proj; let motion_proj_tfm = transl * rot * motion_orig_proj;
// confirm that the projection of the nudge is equivariant. we loosen // confirm that the projection of the nudge is equivariant. we loosen
// the comparison tolerance because the transformation seems to // the comparison tolerance because the transformation seems to
// introduce some numerical error // introduce some numerical error
@ -972,4 +1003,4 @@ mod tests {
let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
} }
} }

View file

@ -14,23 +14,23 @@ use components::{
add_remove::AddRemove, add_remove::AddRemove,
diagnostics::Diagnostics, diagnostics::Diagnostics,
display::Display, display::Display,
outline::Outline, outline::Outline
}; };
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
assembly: Assembly, assembly: Assembly,
selection: Signal<BTreeSet<Rc<dyn Element>>>, selection: Signal<BTreeSet<Rc<dyn Element>>>
} }
impl AppState { impl AppState {
fn new() -> Self { fn new() -> AppState {
Self { AppState {
assembly: Assembly::new(), assembly: Assembly::new(),
selection: create_signal(BTreeSet::default()), selection: create_signal(BTreeSet::default())
} }
} }
// in single-selection mode, select the given element. in multiple-selection // in single-selection mode, select the given element. in multiple-selection
// mode, toggle whether the given element is selected // mode, toggle whether the given element is selected
fn select(&self, element: &Rc<dyn Element>, multi: bool) { fn select(&self, element: &Rc<dyn Element>, multi: bool) {
@ -53,12 +53,12 @@ fn main() {
// set the console error panic hook // set the console error panic hook
#[cfg(feature = "console_error_panic_hook")] #[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
sycamore::render(|| { sycamore::render(|| {
provide_context(AppState::new()); provide_context(AppState::new());
view! { view! {
div(id = "sidebar") { div(id="sidebar") {
AddRemove {} AddRemove {}
Outline {} Outline {}
Diagnostics {} Diagnostics {}
@ -66,4 +66,4 @@ fn main() {
Display {} Display {}
} }
}); });
} }

View file

@ -13,43 +13,32 @@ use std::num::ParseFloatError;
#[readonly::make] #[readonly::make]
pub struct SpecifiedValue { pub struct SpecifiedValue {
pub spec: String, pub spec: String,
pub value: Option<f64>, pub value: Option<f64>
} }
impl SpecifiedValue { impl SpecifiedValue {
pub fn from_empty_spec() -> Self { pub fn from_empty_spec() -> SpecifiedValue {
Self { spec: String::new(), value: None } SpecifiedValue { spec: String::new(), value: None }
} }
pub fn is_present(&self) -> bool { pub fn is_present(&self) -> bool {
matches!(self.value, Some(_)) matches!(self.value, Some(_))
} }
} }
// a `SpecifiedValue` can be constructed from a floating-point option, which is
// given a canonical specification
impl From<Option<f64>> for SpecifiedValue {
fn from(value: Option<f64>) -> Self {
match value {
Some(x) => SpecifiedValue{ spec: x.to_string(), value },
None => SpecifiedValue::from_empty_spec(),
}
}
}
// a `SpecifiedValue` can be constructed from a specification string, formatted // a `SpecifiedValue` can be constructed from a specification string, formatted
// as described in the comment on the structure definition. the result is `Ok` // as described in the comment on the structure definition. the result is `Ok`
// if the specification is properly formatted, and `Error` if not // if the specification is properly formatted, and `Error` if not
impl TryFrom<String> for SpecifiedValue { impl TryFrom<String> for SpecifiedValue {
type Error = ParseFloatError; type Error = ParseFloatError;
fn try_from(spec: String) -> Result<Self, Self::Error> { fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() { if spec.is_empty() {
Ok(Self::from_empty_spec()) Ok(SpecifiedValue::from_empty_spec())
} else { } else {
spec.parse::<f64>().map( spec.parse::<f64>().map(
|value| Self { spec, value: Some(value) } |value| SpecifiedValue { spec: spec, value: Some(value) }
) )
} }
} }
} }

5
deploy/.gitignore vendored
View file

@ -1,5 +0,0 @@
/dyna3.zip
/dyna3/index.html
/dyna3/dyna3-*.js
/dyna3/dyna3-*.wasm
/dyna3/main-*.css

View file

@ -33,7 +33,7 @@ The unification of spheres/planes is indeed attractive for a project like Dyna3.
Discussed coordinates with Alex Kontorovich. He was suggesting "inversive coordinates" -- for a sphere, that's 1/coradius, 1/radius, center/radius (where coradius is radius of sphere inverted in the unit sphere.) The advantage is tangent to and perpendicular to are linear in these coordinates (in the sense that if one is known, the condition of being tangent to or perpendicular to that one are linear). Planes have 1/radius = 0, and in fact, you can take the coordinates to be (2s, 0, x, y, z) where s is the distance to the origin and x,y,z are the normal direction. (Note the normal direction is only determined up to a scalar multiple. So could always scale so that the first non-zero coordinate is 1, or if you like only allow x, y to vary and let z be determined as sqrt(1-x^2^-y^2^). ) Points can be given by (r^2,1,x,y,z) where x,y,z are the coordinates and r is the distance to the origin. Quadratic form that tells you if something is a sphere/plane, or in the boundary, or up in the hyperbolic plane above. There are some details, but not quite explicit for modeling R^3, at http://sites.math.rutgers.edu/~alexk/files/LetterToDuke.pdf -- all this emphasize need to be agnostic with respect to geometric model so that we can experiment. Not really sure exactly how this relates or not to conformal geometric algebra, and whether it can be combined with geometric algebra. As formulated, there are clear-ish reps for planes/spheres and for points, but not as clear for lines. Have to see how to compute distance and/or specify a given distance. To combine inversive coordinates and geometric algebra, maybe think dually; there should be a lift from a normal vector and distance from origin to the five-vector; bivectors would rep circles/lines; trivectors would rep point pairs/points. What is the signature of this algebra, i.e. how many coordinates square to +1, -1, or 0? But it doesn't seem worth it for three dimensions, because there is a natural representation of points, as follows: Discussed coordinates with Alex Kontorovich. He was suggesting "inversive coordinates" -- for a sphere, that's 1/coradius, 1/radius, center/radius (where coradius is radius of sphere inverted in the unit sphere.) The advantage is tangent to and perpendicular to are linear in these coordinates (in the sense that if one is known, the condition of being tangent to or perpendicular to that one are linear). Planes have 1/radius = 0, and in fact, you can take the coordinates to be (2s, 0, x, y, z) where s is the distance to the origin and x,y,z are the normal direction. (Note the normal direction is only determined up to a scalar multiple. So could always scale so that the first non-zero coordinate is 1, or if you like only allow x, y to vary and let z be determined as sqrt(1-x^2^-y^2^). ) Points can be given by (r^2,1,x,y,z) where x,y,z are the coordinates and r is the distance to the origin. Quadratic form that tells you if something is a sphere/plane, or in the boundary, or up in the hyperbolic plane above. There are some details, but not quite explicit for modeling R^3, at http://sites.math.rutgers.edu/~alexk/files/LetterToDuke.pdf -- all this emphasize need to be agnostic with respect to geometric model so that we can experiment. Not really sure exactly how this relates or not to conformal geometric algebra, and whether it can be combined with geometric algebra. As formulated, there are clear-ish reps for planes/spheres and for points, but not as clear for lines. Have to see how to compute distance and/or specify a given distance. To combine inversive coordinates and geometric algebra, maybe think dually; there should be a lift from a normal vector and distance from origin to the five-vector; bivectors would rep circles/lines; trivectors would rep point pairs/points. What is the signature of this algebra, i.e. how many coordinates square to +1, -1, or 0? But it doesn't seem worth it for three dimensions, because there is a natural representation of points, as follows:
The signature of Q will be (1,4), and in fact Q(I1,I2) = 1/2(ab+ba) - E1\dot E2, where a is the "first" or "coradius" coordinate, "b" is the "second" or "radius" coordinate, and E is the Euclidean part (x,y,z). Then the inversive coordinates of a sphere with center (x,y,z) and radius r will be I = (1/\hat{r},1/r,x/r,y/r,z/r) where \hat{r} = r/(|E|^2 -r^2). These coordinates satisfy Q(I,I) = -1. For this to make sense, of course r > 0, but we get planes by letting the radius of a tangent sphere to the plane go to infinity, and we get I = (2s, 0, x0, y0, z0) where (x0,y0,z0) is the unit normal to the plane and s is the perpendicular distance from the plane to the origin. Still Q(I,I) = -1. The signature of Q will be (1,4), and in fact Q(I1,I2) = 1/2(ab+ba) - E1\dot E2, where a is the "first" or "coradius" coordinate, "b" is the "second" or "radius" coordinate, and E is the Euclidean part (x,y,z). Then the inversive coordinates of a sphere with center (x,y,z) and radius r will be I = (1/\hat{r},1/r,x/r,y/r,z/r) where \hat{r} = r/(|E|^2 -r^2). These coordinates satisfy Q(I,I) = -1. For this to make sense, of course r > 0, but we get planes by letting the radius of a tangent sphere to the plane go to infinity, and we get I = (2s, 0, x0, y0, z0) where (x0,y0,z0) is the unit normal to the plane and s is the perpendicular distance from the plane to the origin. Still Q(I,I) = -1.
Since r>0, we can't represent individual points this way. Instead we will use some coordinates J for which Q(J,J) = 0. In particular, if you take for the Euclidean point E = (u,v,w) the coordinates J = (`|E|`^2,1,u,v,w) then Q(J,J) = 0 and moreover it comes out that Q(I,J) = 0 Since r>0, we can't represent individual points this way. Instead we will use some coordinates J for which Q(J,J) = 0. In particular, if you take for the Euclidean point E = (u,v,w) the coordinates J = (`|E|`^2,1,u,v,w) then Q(J,J) = 0 and moreover it comes out that Q(I,J) = 0
whenever E lies on the sphere or plane described by some I with Q(I,I) = -1. whenever E lies on the sphere or plane described by some I with Q(I,I) = -1.
The condition that two spheres I1 and I2 are tangent seems to be that Q(I1,I2) = 1. So given a fixed sphere, the condition that another sphere be tangent to it is linear in the coordinates of that other sphere. The condition that two spheres I1 and I2 are tangent seems to be that Q(I1,I2) = 1. So given a fixed sphere, the condition that another sphere be tangent to it is linear in the coordinates of that other sphere.
This system does seem promising for encoding points, spheres, and planes, and doing basic computations with them. I guess I would just encode a circle as the intersection of the concentric sphere and the containing plane, and a line as either a pair of points or a pair of planes (modulo some equivalence relation, since I can't see any canonical choice of either two planes or two points). Or actually as described below, there is a more canonical choice. This system does seem promising for encoding points, spheres, and planes, and doing basic computations with them. I guess I would just encode a circle as the intersection of the concentric sphere and the containing plane, and a line as either a pair of points or a pair of planes (modulo some equivalence relation, since I can't see any canonical choice of either two planes or two points). Or actually as described below, there is a more canonical choice.
@ -62,4 +62,4 @@ In the engine's coordinate conventions, a sphere with radius $r > 0$ centered on
$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$ $$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 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).$$ $$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. In the `engine` module, these formulas are encoded in the `sphere` and `point` functions.

View file

@ -24,7 +24,7 @@ His final mathematical advice was reasonably encouraging, however:
"But still I would consider it all more or less doable. One should very precisely think about a doable scope. "But still I would consider it all more or less doable. One should very precisely think about a doable scope.
I think three things are essential for the math no matter what you exactly plan. I think three things are essential for the math no matter what you exactly plan.
1. Think projectively. 1. Think projectively,
Use Projective Geometry, Homogeneous Coordinates (or to a certain extent Quaternions, and Clifford Algebras, which are more or less an elegant way to merge Complex numbers with projective concepts.) Use Projective Geometry, Homogeneous Coordinates (or to a certain extent Quaternions, and Clifford Algebras, which are more or less an elegant way to merge Complex numbers with projective concepts.)
2. Consider ambient complex spaces. 2. Consider ambient complex spaces.
The true nature of the objects can only be understood if embedded into a complex ambient space. The true nature of the objects can only be understood if embedded into a complex ambient space.
@ -37,8 +37,10 @@ I think three things are essential for the math no matter what you exactly plan.
It would be nice to see how Jürgen handled some of these issues in a 2D system that he designed. Unfortunately, Cinderella was and remains closed-source; it was distributed for profit for some stretch of time. However, (a part of?) it was reimplemented in JavaScript as CindyJS, which is open source. I took a relatively quick look at that source code at one point, and these were my observations: It would be nice to see how Jürgen handled some of these issues in a 2D system that he designed. Unfortunately, Cinderella was and remains closed-source; it was distributed for profit for some stretch of time. However, (a part of?) it was reimplemented in JavaScript as CindyJS, which is open source. I took a relatively quick look at that source code at one point, and these were my observations:
CindyJS uses very concrete basic objects: 2D points are represented via projective geometry as a list of three floating-point numbers, and everything is done numerically. There are no symbolic representations or exact algebraic numbers. (Not sure how a point on a circle or line is handled, that would take further investigation.) CindyJS uses very concrete basic objects: 2D points are represented via projective geometry as a list of three floating-point numbers, and everything is done numerically. There are no symbolic representations or exact algebraic numbers. (Not sure how a point on a circle or line is handled, that would take further investigation.)
Lines are given by explicit coordinates as well (not sure of the internal details/exact coordinatization, or of how a "LineThrough" is represented). Lines are given by explicit coordinates as well (not sure of the internal details/exact coordinatization, or of how a "LineThrough" is represented).
Was unclear to me how the complex parametrization for preserving continuity was handled in the code, even though Jürgen harps on complex ambient spaces; where are the complex numbers? Perhaps that part of Cinderella was never re-implemented? Was unclear to me how the complex parametrization for preserving continuity was handled in the code, even though Jürgen harps on complex ambient spaces; where are the complex numbers? Perhaps that part of Cinderella was never re-implemented?

View file

@ -7,3 +7,5 @@
<body><script type="module" src="dyna3.js"></script> <body><script type="module" src="dyna3.js"></script>
</body> </body>
</html> </html>

View file

@ -1,16 +0,0 @@
# set paths. this technique for getting the script location comes from
# `mklement0` on Stack Overflow
#
# https://stackoverflow.com/a/24114056
#
TOOLS=$(dirname -- $0)
SRC="$TOOLS/../app-proto/dist"
DEST="$TOOLS/../deploy/dyna3"
# remove the old hash-named files
[ -e "$DEST"/dyna3-*.js ] && rm "$DEST"/dyna3-*.js
[ -e "$DEST"/dyna3-*.wasm ] && rm "$DEST"/dyna3-*.wasm
[ -e "$DEST"/main-*.css ] && rm "$DEST"/main-*.css
# copy the distribution
cp -r "$SRC/." "$DEST"