Compare commits

..

15 commits

Author SHA1 Message Date
Aaron Fenyes
de4c2ef482 Correct mixed-up angles
The angle distortions at the second and third vertices in each triangle
seem to have been mixed up in the previous commit. I've checked, for a
few triangles, that the output of this commit is consistent with the law
of sines.
2025-09-29 17:37:17 -07:00
Aaron Fenyes
a309870968 Fix the order of the triangle list
I've confirmed that this commit's output matches the previous commit's
up to ordering.
2025-09-29 17:12:32 -07:00
Aaron Fenyes
5b331dbbee Calculate angle distortions 2025-09-29 16:31:36 -07:00
Aaron Fenyes
def40714a7 Add twice-augmented data 2025-09-29 16:30:02 -07:00
Aaron Fenyes
1054f4e85b Print the vertex coordinates 2025-09-22 12:30:38 -07:00
Aaron Fenyes
cc2da3406b Print the edge distortions 2025-09-19 14:38:14 -07:00
Aaron Fenyes
b74cbf10c1 Tighten the tolerances 2025-09-19 14:38:14 -07:00
Aaron Fenyes
bc17d71f4a Update the search state when the softness changes
Also, tighten the convergence requirements to account for how the
softness parameter affects the loss function.
2025-09-19 14:38:14 -07:00
Aaron Fenyes
a203f6bc1b Keep optimizing until the total loss is stationary 2025-09-19 14:38:14 -07:00
Aaron Fenyes
3664ea73b1 Introduce soft constraints
Use a penalty method as a quick & dirty way to get started.
2025-09-19 14:38:14 -07:00
Aaron Fenyes
9e74d4e837 Add more 5-5-4 near misses 2025-09-19 14:38:14 -07:00
Aaron Fenyes
0de32f5e11 Measure distortion 2025-09-19 14:38:14 -07:00
Aaron Fenyes
48a640605a Regulate all the diagonals of the 5-5-4 near miss
This should help us find the total distortion of an almost-realization.
2025-09-19 14:38:14 -07:00
Aaron Fenyes
8bedb0baf7 Sketch a 5-5-4 near miss test assembly 2025-09-19 14:38:14 -07:00
Aaron Fenyes
8a0d81d707 Rewind through the descent history 2025-09-19 14:38:14 -07:00
28 changed files with 3496 additions and 534 deletions

View file

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

View file

@ -24,6 +24,6 @@ jobs:
# workspace directory (action variable `github.workspace`, environment
# variable `$GITHUB_WORKSPACE`):
- uses: https://code.forgejo.org/actions/checkout@v4
- 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
* 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
@ -24,66 +24,64 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
### Install the prerequisites
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).
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.
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/).
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`.)
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)
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
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/)
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool
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.
- 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`.
- 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
### Play with 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.
- For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag.
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
- 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.
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.
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype.
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
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
### Run the engine on some example problems
1. Use `sh` to run the script `tools/run-examples.sh`.
- The script is location-independent, so you can do this from anywhere in the dyna3 repository.
1. Use `sh` to run the script `tools/run-examples.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/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
- 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
```julia
include("irisawa-hexlet.jl")
for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
println(rpad(step-1, 4), " | ", scaled_loss)
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
1. Go into the `app-proto` folder.
2. Call `cargo test`.
1. Go into the `app-proto` folder
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.
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 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`.
- 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.
- 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",
"console_error_panic_hook",
"dyna3",
"enum-iterator",
"itertools",
"js-sys",
"lazy_static",
@ -272,26 +271,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "equivalent"
version = "1.0.1"

View file

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

View file

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

View file

@ -14,7 +14,7 @@ fn main() {
// print the completed Gram matrix and the realized configuration
print::gram_matrix(&config);
print::config(&config);
// find the kaleidocycle's twist motion by projecting onto the tangent
// space
const N_POINTS: usize = 12;
@ -29,4 +29,4 @@ fn main() {
let normalization = 5.0 / twist_motion[(2, 0)];
println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end());
}
}
}

View file

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

View file

@ -227,6 +227,16 @@ details[open]:has(li) .element-switch::after {
border-radius: 8px;
}
#distortion-bar {
display: flex;
margin-top: 8px;
gap: 8px;
}
#distortion-gauge {
flex-grow: 1;
}
/* display */
#display {

View file

@ -1,11 +1,11 @@
use enum_iterator::{all, Sequence};
use nalgebra::{DMatrix, DVector, DVectorView};
use std::{
cell::Cell,
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
f64::consts::SQRT_2,
fmt,
fmt::{Debug, Display, Formatter},
fmt::{Debug, Formatter},
hash::{Hash, Hasher},
rc::Rc,
sync::{atomic, atomic::AtomicU64},
@ -27,7 +27,6 @@ use crate::{
ConfigSubspace,
ConstraintProblem,
DescentHistory,
MatrixEntry,
Realization,
},
specified::SpecifiedValue,
@ -45,7 +44,7 @@ static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);
pub trait Serial {
// a serial number that uniquely identifies this element
fn serial(&self) -> u64;
// take the next serial number, panicking if that was the last one left
fn next_serial() -> u64 where Self: Sized {
// 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 {
fn pose(&self, problem: &mut ConstraintProblem);
}
@ -101,42 +92,47 @@ pub trait ProblemPoser {
pub trait Element: Serial + ProblemPoser + DisplayItem {
// the default identifier for an element of this type
fn default_id() -> String where Self: Sized;
// the default example of an element of this type
fn default(id: String, id_num: u64) -> Self where Self: Sized;
// the default regulators that come with this element
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
Vec::new()
}
fn id(&self) -> &String;
fn label(&self) -> &String;
fn representation(&self) -> Signal<DVector<f64>>;
fn ghost(&self) -> Signal<bool>;
// the regulators the element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
// project a representation vector for this kind of element onto its
// normalization variety
fn project_to_normalized(&self, rep: &mut DVector<f64>);
// the configuration matrix column index that was assigned to the element
// last time the assembly was realized, or `None` if the element has never
// been through a realization
fn column_index(&self) -> Option<usize>;
// assign the element a configuration matrix column index. this method must
// be used carefully to preserve invariant (1), described in the comment on
// the `tangent` field of the `Assembly` structure
fn set_column_index(&self, index: usize);
/* KLUDGE */
fn is_point(&self) -> bool {
false
}
}
impl Debug for dyn Element {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.id(), f)
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
self.id().fmt(f)
}
}
@ -179,7 +175,7 @@ pub struct Sphere {
impl Sphere {
const CURVATURE_COMPONENT: usize = 3;
pub fn new(
id: String,
label: String,
@ -203,7 +199,7 @@ impl Element for Sphere {
fn default_id() -> String {
"sphere".to_string()
}
fn default(id: String, id_num: u64) -> Self {
Self::new(
id,
@ -212,39 +208,39 @@ impl Element for Sphere {
sphere(0.0, 0.0, 0.0, 1.0),
)
}
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
vec![Rc::new(HalfCurvatureRegulator::new(self))]
}
fn id(&self) -> &String {
&self.id
}
fn label(&self) -> &String {
&self.label
}
fn representation(&self) -> Signal<DVector<f64>> {
self.representation
}
fn ghost(&self) -> Signal<bool> {
self.ghost
}
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators
}
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
project_sphere_to_normalized(rep);
}
fn column_index(&self) -> Option<usize> {
self.column_index.get()
}
fn set_column_index(&self, index: usize) {
self.column_index.set(Some(index));
}
@ -259,7 +255,8 @@ impl Serial for Sphere {
impl ProblemPoser for Sphere {
fn pose(&self, problem: &mut ConstraintProblem) {
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.guess.set_column(index, &self.representation.get_clone_untracked());
}
@ -278,8 +275,7 @@ pub struct Point {
impl Point {
const WEIGHT_COMPONENT: usize = 3;
const NORM_COMPONENT: usize = 4;
pub fn new(
id: String,
label: String,
@ -303,7 +299,7 @@ impl Element for Point {
fn default_id() -> String {
"point".to_string()
}
fn default(id: String, id_num: u64) -> Self {
Self::new(
id,
@ -312,47 +308,42 @@ impl Element for Point {
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 {
&self.id
}
fn label(&self) -> &String {
&self.label
}
fn representation(&self) -> Signal<DVector<f64>> {
self.representation
}
fn ghost(&self) -> Signal<bool> {
self.ghost
}
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators
}
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
project_point_to_normalized(rep);
}
fn column_index(&self) -> Option<usize> {
self.column_index.get()
}
fn set_column_index(&self, index: usize) {
self.column_index.set(Some(index));
}
fn is_point(&self) -> bool {
true
}
}
impl Serial for Point {
@ -364,7 +355,8 @@ impl Serial for Point {
impl ProblemPoser for Point {
fn pose(&self, problem: &mut ConstraintProblem) {
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.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5);
problem.guess.set_column(index, &self.representation.get_clone_untracked());
@ -375,6 +367,12 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>;
fn soft(&self) -> Option<Signal<bool>> {
None
}
fn distortion(&self) -> Option<ReadSignal<f64>> { /* KLUDGE */
None
}
}
impl Hash for dyn Regulator {
@ -407,6 +405,8 @@ pub struct InversiveDistanceRegulator {
pub subjects: [Rc<dyn Element>; 2],
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
pub soft: Signal<bool>,
distortion: Option<ReadSignal<f64>>, /* KLUDGE */
serial: u64,
}
@ -420,11 +420,26 @@ impl InversiveDistanceRegulator {
)
)
});
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let distortion = if subjects.iter().all(|subj| subj.is_point()) {
Some(create_memo(move || {
let set_point_opt = set_point.with(|set_pt| set_pt.value);
let measurement_val = measurement.get();
match set_point_opt {
None => 0.0,
Some(set_point_val) => SQRT_2 * (
(-measurement_val).sqrt() - (-set_point_val).sqrt()
),
}
}))
} else {
None
};
let soft = create_signal(false);
let serial = Self::next_serial();
Self { subjects, measurement, set_point, serial }
Self { subjects, measurement, set_point, soft, distortion, serial }
}
}
@ -432,14 +447,22 @@ impl Regulator for InversiveDistanceRegulator {
fn subjects(&self) -> Vec<Rc<dyn Element>> {
self.subjects.clone().into()
}
fn measurement(&self) -> ReadSignal<f64> {
self.measurement
}
fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point
}
fn soft(&self) -> Option<Signal<bool>> {
Some(self.soft)
}
fn distortion(&self) -> Option<ReadSignal<f64>> {
self.distortion
}
}
impl Serial for InversiveDistanceRegulator {
@ -450,35 +473,40 @@ impl Serial for InversiveDistanceRegulator {
impl ProblemPoser for InversiveDistanceRegulator {
fn pose(&self, problem: &mut ConstraintProblem) {
let soft = self.soft.get_untracked();
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let [row, col] = self.subjects.each_ref().map(
|subj| subj.column_index().expect(
indexing_error("Subject", subj.id(),
"inversive distance regulator").as_str())
"Subjects should be indexed before inversive distance regulator writes problem data"
)
);
problem.gram.push_sym(row, col, val);
if soft {
problem.soft.push_sym(row, col, val);
} else {
problem.gram.push_sym(row, col, val);
}
}
});
}
}
pub struct HalfCurvatureRegulator {
pub subject: Rc<Sphere>,
pub subject: Rc<dyn Element>,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
serial: u64,
}
impl HalfCurvatureRegulator {
pub fn new(subject: Rc<Sphere>) -> Self {
pub fn new(subject: Rc<dyn Element>) -> Self {
let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT]
);
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
Self { subject, measurement, set_point, serial }
}
}
@ -487,11 +515,11 @@ impl Regulator for HalfCurvatureRegulator {
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
}
@ -508,84 +536,14 @@ impl ProblemPoser for HalfCurvatureRegulator {
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,
"half-curvature regulator").as_str());
"Subject should be indexed before half-curvature regulator writes problem data"
);
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
pub struct ElementMotion<'a> {
pub element: Rc<dyn Element>,
@ -600,7 +558,7 @@ pub struct Assembly {
// elements and regulators
pub elements: Signal<BTreeSet<Rc<dyn Element>>>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
// solution variety tangent space. the basis vectors are stored in
// configuration matrix format, ordered according to the elements' column
// indices. when you realize the assembly, every element that's present
@ -612,13 +570,13 @@ pub struct Assembly {
// in that column of the tangent space basis matrices
//
pub tangent: Signal<ConfigSubspace>,
// indexing
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization control
pub realization_trigger: Signal<()>,
// realization diagnostics
pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>,
@ -638,7 +596,7 @@ impl Assembly {
descent_history: create_signal(DescentHistory::new()),
step: create_signal(SpecifiedValue::from_empty_spec()),
};
// realize the assembly whenever the element list, the regulator list,
// a regulator's set point, or the realization trigger is updated
let assembly_for_realization = assembly.clone();
@ -652,7 +610,7 @@ impl Assembly {
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();
@ -664,12 +622,12 @@ impl Assembly {
assembly_for_step_selection.load_config(&config)
}
});
assembly
}
// --- inserting elements and regulators ---
// insert an element into the assembly without checking whether we already
// have an element with the same identifier. any element that does have the
// same identifier will get kicked out of the `elements_by_id` index
@ -679,13 +637,13 @@ impl Assembly {
let elt_rc = Rc::new(elt);
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()));
// create and insert the element's default regulators
for reg in elt_rc.default_regulators() {
self.insert_regulator(reg);
}
}
pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
let can_insert = self.elements_by_id.with_untracked(
|elts_by_id| !elts_by_id.contains_key(elt.id())
@ -695,7 +653,7 @@ impl Assembly {
}
can_insert
}
pub fn insert_element_default<T: Element + 'static>(&self) {
// find the next unused identifier in the default sequence
let default_id = T::default_id();
@ -707,17 +665,17 @@ impl Assembly {
id_num += 1;
id = format!("{default_id}{id_num}");
}
// create and insert the default example of `T`
let _ = self.insert_element_unchecked(T::default(id, id_num));
}
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
// add the regulator to the assembly's regulator list
self.regulators.update(
|regs| regs.insert(regulator.clone())
);
// add the regulator to each subject's regulator list
let subject_regulators: Vec<_> = regulator.subjects().into_iter().map(
|subj| subj.regulators()
@ -725,7 +683,7 @@ impl Assembly {
for regulators in subject_regulators {
regulators.update(|regs| regs.insert(regulator.clone()));
}
/* DEBUG */
// print an updated list of regulators
console_log!("Regulators:");
@ -748,9 +706,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(
@ -758,9 +716,9 @@ impl Assembly {
);
}
}
// --- realization ---
pub fn realize(&self) {
// index the elements
self.elements.update_silent(|elts| {
@ -768,7 +726,7 @@ impl Assembly {
elt.set_column_index(index);
}
});
// set up the constraint problem
let problem = self.elements.with_untracked(|elts| {
let mut problem = ConstraintProblem::new(elts.len());
@ -782,21 +740,20 @@ impl Assembly {
});
problem
});
/* DEBUG */
// log the Gram matrix
console_log!("Gram matrix:\n{}", problem.gram);
console_log!("Frozen entries:\n{}", problem.frozen);
/* DEBUG */
// log the initial configuration matrix
console_log!("Old configuration:{:>8.3}", problem.guess);
// look for a configuration with the given Gram matrix
let Realization { result, history } = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
&problem, 1.0e-20, 0.5, 0.9, 1.1, 400, 110
);
/* DEBUG */
// report the outcome of the search in the browser console
if let Err(ref message) = result {
@ -808,20 +765,20 @@ impl Assembly {
console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
}
// report the descent history
let step_cnt = history.config.len();
self.descent_history.set(history);
match result {
Ok(ConfigNeighborhood { nbhd: tangent, .. }) => {
/* DEBUG */
// report the tangent dimension
console_log!("Tangent dimension: {}", tangent.dim());
// report the realization status
self.realization_status.set(Ok(()));
// display the last realization step
self.step.set(
if step_cnt > 0 {
@ -831,7 +788,7 @@ impl Assembly {
SpecifiedValue::from_empty_spec()
}
);
// save the tangent space
self.tangent.set_silent(tangent);
},
@ -841,15 +798,15 @@ impl Assembly {
// `Err(message)` we received from the match: we're changing the
// `Ok` type from `Realization` to `()`
self.realization_status.set(Err(message));
// display the initial guess
self.step.set(SpecifiedValue::from(Some(0.0)));
},
}
}
// --- deformation ---
// 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)
// from above and the following additional invariant:
@ -866,7 +823,7 @@ impl Assembly {
if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) {
console::log_1(&JsValue::from("The assembly is rigid"));
}
// give a column index to each moving element that doesn't have one yet.
// this temporarily breaks invariant (1), but the invariant will be
// restored when we realize the assembly at the end of the deformation.
@ -884,7 +841,7 @@ impl Assembly {
}
next_column_index
};
// project the element motions onto the tangent space of the solution
// variety and sum them to get a deformation of the whole assembly. the
// matrix `motion_proj` that holds the deformation has extra columns for
@ -895,7 +852,7 @@ impl Assembly {
// we can unwrap the column index because we know that every moving
// element has one at this point
let column_index = elt_motion.element.column_index().unwrap();
if column_index < realized_dim {
// this element had a column index when we started, so by
// invariant (1), it's reflected in the tangent space
@ -913,7 +870,7 @@ impl Assembly {
target_column += unif_to_std * elt_motion.velocity;
}
}
// step the assembly along the deformation. this changes the elements'
// normalizations, so we restore those afterward
for elt in self.elements.get_clone_untracked() {
@ -931,7 +888,7 @@ impl Assembly {
};
});
}
// trigger a realization to bring the configuration back onto the
// solution variety. this also gets the elements' column indices and the
// saved tangent space back in sync
@ -942,22 +899,20 @@ impl Assembly {
#[cfg(test)]
mod tests {
use super::*;
use crate::engine;
#[test]
#[should_panic(expected =
"Sphere \"sphere\" must be indexed before it writes problem data")]
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
fn unindexed_element_test() {
let _ = create_root(|| {
let elt = Sphere::default("sphere".to_string(), 0);
elt.pose(&mut ConstraintProblem::new(1));
});
}
#[test]
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \
inversive distance regulator writes problem data")]
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| {
let subjects = [0, 1].map(
@ -972,7 +927,7 @@ mod tests {
}.pose(&mut ConstraintProblem::new(2));
});
}
#[test]
fn curvature_drift_test() {
const INITIAL_RADIUS: f64 = 0.25;
@ -992,7 +947,7 @@ mod tests {
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS),
)
);
// nudge the sphere repeatedly along the `z` axis
const STEP_SIZE: f64 = 0.0025;
const STEP_CNT: usize = 400;
@ -1008,7 +963,7 @@ mod tests {
]
);
}
// check how much the sphere's curvature has drifted
const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS;
const DRIFT_TOL: f64 = 0.015;
@ -1018,4 +973,4 @@ mod tests {
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
});
}
}
}

View file

@ -54,7 +54,7 @@ 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() {
@ -63,15 +63,15 @@ fn StepInput() -> View {
}
);
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" }
@ -98,7 +98,7 @@ fn StepInput() -> View {
|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);
@ -111,6 +111,94 @@ fn StepInput() -> View {
}
}
#[component]
fn DistortionGauge() -> View {
let state = use_context::<AppState>();
let total_distortion = create_memo(move || {
state.assembly.regulators.with(|regs| {
let mut total = 0.0;
for reg in regs {
if let Some(distortion) = reg.distortion() {
total += distortion.get().abs();
}
}
total
})
});
view! {
div(id = "distortion-gauge") {
"Distortion: " (total_distortion.with(|distort| distort.to_string()))
}
}
}
#[component]
fn PrintButton() -> View {
view! {
button(
on:click = |_| {
let state = use_context::<AppState>();
// print the edge length distortions
let mut hard_distortion_table = String::new();
let mut soft_distortion_table = String::new();
let mut highest_distortion = f64::NEG_INFINITY;
let mut lowest_distortion = f64::INFINITY;
let mut largest_hard_distortion = f64::NEG_INFINITY;
state.assembly.regulators.with_untracked(|regs| {
for reg in regs {
if let Some(distortion) = reg.distortion() {
let distortion_val = distortion.get();
let subjects = reg.subjects();
let distortion_line = format!(
"{}, {}: {distortion_val}\n",
subjects[0].id(),
subjects[1].id(),
);
match reg.soft() {
Some(soft) if soft.get() => {
soft_distortion_table += &distortion_line;
highest_distortion = highest_distortion.max(distortion_val);
lowest_distortion = lowest_distortion.min(distortion_val);
},
_ => {
hard_distortion_table += &distortion_line;
largest_hard_distortion = largest_hard_distortion.max(distortion_val.abs());
}
};
}
}
});
console_log!("\
=== Distortions of flexible edges (for labels) ===\n\n\
--- Range ---\n\n\
Highest: {highest_distortion}\n\
Lowest: {lowest_distortion}\n\n\
--- Table ---\n\n{soft_distortion_table}\n\
=== Distortions of rigid edges (for validation) ===\n\n\
These values should be small relative to the ones for the flexible edges\n\n\
--- Range ---\n\n\
Largest absolute: {largest_hard_distortion}\n\n\
--- Table ---\n\n{hard_distortion_table}\
");
// print the vertex coordinates
let mut coords_table = String::new();
state.assembly.elements.with_untracked(|elts| {
for elt in elts.iter().filter(|elt| elt.is_point()) {
let (x, y, z) = elt.representation().with(
|rep| (rep[0], rep[1], rep[2])
);
coords_table += &format!("{}: {x}, {y}, {z}\n", elt.id());
}
});
console_log!("=== Vertex coordinates ===\n\n{coords_table}");
},
) { "Print" }
}
}
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![
Some(step as f64),
@ -124,7 +212,7 @@ fn LossHistory() -> View {
const CONTAINER_ID: &str = "loss-history";
let state = use_context::<AppState>();
let renderer = WasmRenderer::new_opt(None, Some(178));
on_mount(move || {
create_effect(move || {
// get the loss history
@ -136,13 +224,13 @@ fn LossHistory() -> View {
.map(into_log10_time_point)
.collect()
);
// initialize the chart axes
let step_axis = Axis::new()
.type_(AxisType::Category)
.boundary_gap(false);
let scaled_loss_axis = Axis::new();
// 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
// load empty data vectors, but that turns out not to clear the
@ -164,7 +252,7 @@ fn LossHistory() -> View {
renderer.render(CONTAINER_ID, &chart).unwrap();
});
});
view! {
div(id = CONTAINER_ID, class = "diagnostics-chart")
}
@ -176,7 +264,7 @@ fn SpectrumHistory() -> View {
const CONTAINER_ID: &str = "spectrum-history";
let state = use_context::<AppState>();
let renderer = WasmRenderer::new(478, 178);
on_mount(move || {
create_effect(move || {
// get the spectrum of the Hessian at each step, split into its
@ -208,13 +296,13 @@ fn SpectrumHistory() -> View {
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
.into_iter()
.partition(|&(_, val)| val > 0.0);
// initialize the chart axes
let step_axis = Axis::new()
.type_(AxisType::Category)
.boundary_gap(false);
let eigval_axis = Axis::new();
// 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
// load empty data vectors, but that turns out not to clear the
@ -270,7 +358,7 @@ fn SpectrumHistory() -> View {
renderer.render(CONTAINER_ID, &chart).unwrap();
});
});
view! {
div(id = CONTAINER_ID, class = "diagnostics-chart")
}
@ -302,7 +390,7 @@ pub fn Diagnostics() -> View {
let diagnostics_state = DiagnosticsState::new("loss".to_string());
let active_tab = diagnostics_state.active_tab.clone();
provide_context(diagnostics_state);
view! {
div(id = "diagnostics") {
div(id = "diagnostics-bar") {
@ -315,6 +403,10 @@ pub fn Diagnostics() -> View {
}
DiagnosticsPanel(name = "loss") { LossHistory {} }
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
div(id = "distortion-bar") {
DistortionGauge {}
PrintButton {}
}
}
}
}
}

View file

@ -48,11 +48,11 @@ impl SceneSpheres {
highlights: Vec::new(),
}
}
fn len_i32(&self) -> i32 {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
}
fn push(
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32,
@ -79,7 +79,7 @@ impl ScenePoints {
selections: Vec::new(),
}
}
fn push(
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
@ -107,7 +107,7 @@ impl Scene {
pub trait DisplayItem {
fn show(&self, scene: &mut Scene, selected: bool);
// the smallest positive depth, represented as a multiple of `dir`, where
// the line generated by `dir` hits the element. returns `None` if the line
// misses the element
@ -125,14 +125,14 @@ impl DisplayItem for Sphere {
const DEFAULT_OPACITY: f32 = 0.5;
const GHOST_OPACITY: f32 = 0.2;
const HIGHLIGHT: f32 = 0.2;
let representation = self.representation.get_clone_untracked();
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 highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.spheres.push(representation, color, opacity, highlight);
}
// this method should be kept synchronized with `sphere_cast` in
// `spheres.frag`, which does essentially the same thing on the GPU side
fn cast(
@ -144,12 +144,12 @@ impl DisplayItem for Sphere {
// if `a/b` is less than this threshold, we approximate
// `a*u^2 + b*u + c` by the linear function `b*u + c`
const DEG_THRESHOLD: f64 = 1e-9;
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
let a = -rep[3] * dir.norm_squared();
let b = rep.rows_range(..3).dot(&dir);
let c = -rep[4];
let adjust = 4.0*a*c/(b*b);
if adjust < 1.0 {
// as long as `b` is non-zero, the linear approximation of
@ -184,14 +184,14 @@ impl DisplayItem for Point {
/* SCAFFOLDING */
const GHOST_OPACITY: f32 = 0.4;
const HIGHLIGHT: f32 = 0.5;
let representation = self.representation.get_clone_untracked();
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 highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.points.push(representation, color, opacity, highlight, selected);
}
/* SCAFFOLDING */
fn cast(
&self,
@ -203,16 +203,16 @@ impl DisplayItem for Point {
if rep[2] < 0.0 {
// this constant should be kept synchronized with `point.frag`
const POINT_RADIUS_PX: f64 = 4.0;
// find the radius of the point in screen projection units
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
// find the squared distance between the screen projections of the
// ray and the point
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
let dist_sq = (dir_proj - rep_proj).norm_squared();
// if the ray hits the point, return its depth
if dist_sq < point_radius_proj * point_radius_proj {
Some(rep[2] / dir[2])
@ -254,13 +254,13 @@ fn set_up_program(
WebGl2RenderingContext::FRAGMENT_SHADER,
fragment_shader_source,
);
// create the program and attach the shaders
let program = context.create_program().unwrap();
context.attach_shader(&program, &vertex_shader);
context.attach_shader(&program, &fragment_shader);
context.link_program(&program);
/* DEBUG */
// report whether linking succeeded
let link_status = context
@ -273,7 +273,7 @@ fn set_up_program(
"Linking failed"
};
console::log_1(&JsValue::from(link_msg));
program
}
@ -318,7 +318,7 @@ fn load_new_buffer(
// create a buffer and bind it to ARRAY_BUFFER
let buffer = context.create_buffer();
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
// load the given data into the buffer. this block is unsafe because
// `Float32Array::view` creates a raw view into our module's
// `WebAssembly.Memory` buffer. allocating more memory will change the
@ -332,7 +332,7 @@ fn load_new_buffer(
WebGl2RenderingContext::STATIC_DRAW,
);
}
buffer
}
@ -353,11 +353,11 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
let width = rect.width();
let height = rect.height();
let shortdim = width.min(height);
// this constant should be kept synchronized with `spheres.frag` and
// `point.vert`
const FOCAL_SLOPE: f64 = 0.3;
(
Vector3::new(
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
@ -373,13 +373,13 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
#[component]
pub fn Display() -> View {
let state = use_context::<AppState>();
// canvas
let display = create_node_ref();
// viewpoint
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
// navigation
let pitch_up = create_signal(0.0);
let pitch_down = create_signal(0.0);
@ -390,7 +390,7 @@ pub fn Display() -> View {
let zoom_in = create_signal(0.0);
let zoom_out = create_signal(0.0);
let turntable = create_signal(false); /* BENCHMARKING */
// manipulation
let translate_neg_x = create_signal(0.0);
let translate_pos_x = create_signal(0.0);
@ -400,7 +400,7 @@ pub fn Display() -> View {
let translate_pos_z = create_signal(0.0);
let shrink_neg = create_signal(0.0);
let shrink_pos = create_signal(0.0);
// change listener
let scene_changed = create_signal(true);
create_effect(move || {
@ -413,18 +413,18 @@ pub fn Display() -> View {
state.selection.track();
scene_changed.set(true);
});
/* INSTRUMENTS */
const SAMPLE_PERIOD: i32 = 60;
let mut last_sample_time = 0.0;
let mut frames_since_last_sample = 0;
let mean_frame_interval = create_signal(0.0);
let assembly_for_raf = state.assembly.clone();
on_mount(move || {
// timing
let mut last_time = 0.0;
// viewpoint
const ROT_SPEED: f64 = 0.4; // in radians per second
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
@ -432,18 +432,18 @@ pub fn Display() -> View {
let mut orientation = DMatrix::<f64>::identity(5, 5);
let mut rotation = DMatrix::<f64>::identity(5, 5);
let mut location_z: f64 = 5.0;
// manipulation
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
// display parameters
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
const DEBUG_MODE: i32 = 0; /* DEBUG */
/* INSTRUMENTS */
let performance = window().unwrap().performance().unwrap();
// get the display canvas
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
let ctx = canvas
@ -452,28 +452,28 @@ pub fn Display() -> View {
.unwrap()
.dyn_into::<WebGl2RenderingContext>()
.unwrap();
// disable depth testing
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
// set blend mode
ctx.enable(WebGl2RenderingContext::BLEND);
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
// set up the sphere rendering program
let sphere_program = set_up_program(
&ctx,
include_str!("identity.vert"),
include_str!("spheres.frag"),
);
// set up the point rendering program
let point_program = set_up_program(
&ctx,
include_str!("point.vert"),
include_str!("point.frag"),
);
/* DEBUG */
// print the maximum number of vectors that can be passed as
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
@ -490,10 +490,10 @@ pub fn Display() -> View {
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
&JsValue::from("uniform vectors available"),
);
// find the sphere program's vertex attribute
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
// find the sphere program's uniforms
const SPHERE_MAX: usize = 200;
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
@ -513,7 +513,7 @@ pub fn Display() -> View {
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
// load the viewport vertex positions into a new vertex buffer object
const VERTEX_CNT: usize = 6;
let viewport_positions: [f32; 3*VERTEX_CNT] = [
@ -527,20 +527,20 @@ pub fn Display() -> View {
1.0, -1.0, 0.0,
];
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
// find the point program's vertex attributes
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_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
// set up a repainting routine
let (_, start_animation_loop, _) = create_raf(move || {
// get the time step
let time = performance.now();
let time_step = 0.001*(time - last_time);
last_time = time;
// get the navigation state
let pitch_up_val = pitch_up.get();
let pitch_down_val = pitch_down.get();
@ -551,7 +551,7 @@ pub fn Display() -> View {
let zoom_in_val = zoom_in.get();
let zoom_out_val = zoom_out.get();
let turntable_val = turntable.get(); /* BENCHMARKING */
// get the manipulation state
let translate_neg_x_val = translate_neg_x.get();
let translate_pos_x_val = translate_pos_x.get();
@ -561,7 +561,7 @@ pub fn Display() -> View {
let translate_pos_z_val = translate_pos_z.get();
let shrink_neg_val = shrink_neg.get();
let shrink_pos_val = shrink_pos.get();
// update the assembly's orientation
let ang_vel = {
let pitch = pitch_up_val - pitch_down_val;
@ -582,11 +582,11 @@ pub fn Display() -> View {
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
);
orientation = &rotation * &orientation;
// update the assembly's location
let zoom = zoom_out_val - zoom_in_val;
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
// manipulate the assembly
/* KLUDGE */
// to avoid the complexity of making tangent space projection
@ -597,16 +597,16 @@ pub fn Display() -> View {
|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) {
if
state.selection.with(|sel| sel.len() == 1)
&& realization_successful
&& on_last_step
{
let sel = state.selection.with(
|sel| sel.into_iter().next().unwrap().clone()
);
@ -642,11 +642,11 @@ pub fn Display() -> View {
scene_changed.set(true);
}
}
if scene_changed.get() {
const SPACE_DIM: usize = 3;
const COLOR_SIZE: usize = 3;
/* INSTRUMENTS */
// measure mean frame interval
frames_since_last_sample += 1;
@ -655,11 +655,11 @@ pub fn Display() -> View {
last_sample_time = time;
frames_since_last_sample = 0;
}
// --- get the assembly ---
let mut scene = Scene::new();
// find the map from assembly space to world space
let location = {
let u = -location_z;
@ -672,7 +672,7 @@ pub fn Display() -> View {
])
};
let asm_to_world = &location * &orientation;
// set up the scene
state.assembly.elements.with_untracked(
|elts| for elt in elts {
@ -681,26 +681,26 @@ pub fn Display() -> View {
}
);
let sphere_cnt = scene.spheres.len_i32();
// --- draw the spheres ---
// use the sphere rendering program
ctx.use_program(Some(&sphere_program));
// enable the sphere program's vertex attribute
ctx.enable_vertex_attrib_array(viewport_position_attr);
// write the spheres in world coordinates
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|rep| (&asm_to_world * rep).cast::<f32>()
).collect();
// set the resolution
let width = canvas.width() as f32;
let height = canvas.height() as f32;
ctx.uniform2f(resolution_loc.as_ref(), width, height);
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
// pass the scene data
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
for n in 0..sphere_reps_world.len() {
@ -722,33 +722,33 @@ pub fn Display() -> View {
scene.spheres.highlights[n],
);
}
// pass the display parameters
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
// bind the viewport vertex position buffer to the position
// attribute in the vertex shader
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
// draw the scene
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
// disable the sphere program's vertex attribute
ctx.disable_vertex_attrib_array(viewport_position_attr);
// --- draw the points ---
if !scene.points.representations.is_empty() {
// use the point rendering program
ctx.use_program(Some(&point_program));
// enable the point program's vertex attributes
ctx.enable_vertex_attrib_array(point_position_attr);
ctx.enable_vertex_attrib_array(point_color_attr);
ctx.enable_vertex_attrib_array(point_highlight_attr);
ctx.enable_vertex_attrib_array(point_selection_attr);
// write the points in world coordinates
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
let point_positions = DMatrix::from_columns(
@ -756,7 +756,7 @@ pub fn Display() -> View {
|rep| &asm_to_world_sp * rep
).collect::<Vec<_>>().as_slice()
).cast::<f32>();
// load the point positions and colors into new buffers and
// bind them to the corresponding attributes in the vertex
// shader
@ -764,22 +764,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_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());
// draw the scene
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
// disable the point program's vertex attributes
ctx.disable_vertex_attrib_array(point_position_attr);
ctx.disable_vertex_attrib_array(point_color_attr);
ctx.disable_vertex_attrib_array(point_highlight_attr);
ctx.disable_vertex_attrib_array(point_selection_attr);
}
// --- update the display state ---
// update the viewpoint
assembly_to_world.set(asm_to_world);
// clear the scene change flag
scene_changed.set(
pitch_up_val != 0.0
@ -799,7 +799,7 @@ pub fn Display() -> View {
});
start_animation_loop();
});
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
let mut navigating = true;
let shift = event.shift_key();
@ -819,7 +819,7 @@ pub fn Display() -> View {
event.prevent_default();
}
};
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
let mut manipulating = true;
let shift = event.shift_key();
@ -838,7 +838,7 @@ pub fn Display() -> View {
event.prevent_default();
}
};
view! {
/* TO DO */
// switch back to integer-valued parameters when that becomes possible
@ -860,7 +860,7 @@ pub fn Display() -> View {
yaw_left.set(0.0);
pitch_up.set(0.0);
pitch_down.set(0.0);
// swap manipulation inputs
translate_pos_z.set(translate_neg_y.get());
translate_neg_z.set(translate_pos_y.get());
@ -886,7 +886,7 @@ pub fn Display() -> View {
roll_ccw.set(0.0);
zoom_in.set(0.0);
zoom_out.set(0.0);
// swap manipulation inputs
translate_pos_y.set(translate_neg_z.get());
translate_neg_y.set(translate_pos_z.get());
@ -927,7 +927,7 @@ pub fn Display() -> View {
None => (),
};
}
// if we clicked something, select it
match clicked {
Some((elt, _)) => state.select(&elt, event.shift_key()),
@ -936,4 +936,4 @@ pub fn Display() -> View {
},
)
}
}
}

View file

@ -9,7 +9,6 @@ use crate::{
Element,
HalfCurvatureRegulator,
InversiveDistanceRegulator,
PointCoordinateRegulator,
Regulator,
},
specified::SpecifiedValue
@ -21,16 +20,16 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
// get the regulator's measurement and set point signals
let measurement = regulator.measurement();
let set_point = regulator.set_point();
// the `valid` signal tracks whether the last entered value is a valid set
// point specification
let valid = create_signal(true);
// the `value` signal holds the current set point specification
let value = create_signal(
set_point.with_untracked(|set_pt| set_pt.spec.clone())
);
// this `reset_value` closure resets the input value to the regulator's set
// point specification
let reset_value = move || {
@ -39,11 +38,11 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
})
};
// reset the input value whenever the regulator's set point specification
// is updated
create_effect(reset_value);
view! {
input(
r#type = "text",
@ -120,20 +119,6 @@ impl OutlineItem for HalfCurvatureRegulator {
}
}
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")
}
}
}
}
// a list item that shows an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
@ -241,7 +226,7 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
#[component]
pub fn Outline() -> View {
let state = use_context::<AppState>();
// list the elements alphabetically by ID
/* TO DO */
// this code is designed to generalize easily to other sort keys. if we only
@ -254,7 +239,7 @@ pub fn Outline() -> View {
.sorted_by_key(|elt| elt.id().clone())
.collect::<Vec<_>>()
);
view! {
ul(
id = "outline",
@ -272,4 +257,4 @@ pub fn Outline() -> View {
)
}
}
}
}

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter};
use sycamore::prelude::console_log; /* DEBUG */
// --- elements ---
@ -52,8 +53,8 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
// --- partial matrices ---
pub struct MatrixEntry {
pub index: (usize, usize),
pub value: f64,
index: (usize, usize),
value: f64,
}
pub struct PartialMatrix(Vec<MatrixEntry>);
@ -62,19 +63,19 @@ impl PartialMatrix {
pub fn new() -> Self {
Self(Vec::<MatrixEntry>::new())
}
pub fn push(&mut self, row: usize, col: usize, value: f64) {
let Self(entries) = self;
entries.push(MatrixEntry { index: (row, col), value });
}
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
self.push(row, col, value);
if row != col {
self.push(col, row, value);
}
}
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = a.clone();
for &MatrixEntry { index, value } in self {
@ -82,7 +83,7 @@ impl PartialMatrix {
}
result
}
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
for &MatrixEntry { index, .. } in self {
@ -90,7 +91,7 @@ impl PartialMatrix {
}
result
}
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
for &MatrixEntry { index, value } in self {
@ -112,7 +113,7 @@ impl Display for PartialMatrix {
impl IntoIterator for PartialMatrix {
type Item = MatrixEntry;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let Self(entries) = self;
entries.into_iter()
@ -122,7 +123,7 @@ impl IntoIterator for PartialMatrix {
impl<'a> IntoIterator for &'a PartialMatrix {
type Item = &'a MatrixEntry;
type IntoIter = std::slice::Iter<'a, MatrixEntry>;
fn into_iter(self) -> Self::IntoIter {
let PartialMatrix(entries) = self;
entries.into_iter()
@ -146,7 +147,7 @@ impl ConfigSubspace {
basis_std: Vec::new(),
}
}
// approximate the kernel of a symmetric endomorphism of the configuration
// space for `assembly_dim` elements. we consider an eigenvector to be part
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
@ -167,10 +168,10 @@ impl ConfigSubspace {
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
).collect::<Vec<_>>().as_slice()
);
// express the basis in the standard coordinates
let basis_std = proj_to_std * &basis_proj;
const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4;
Self {
@ -187,15 +188,15 @@ impl ConfigSubspace {
).collect(),
}
}
pub fn dim(&self) -> usize {
self.basis_std.len()
}
pub fn assembly_dim(&self) -> usize {
self.assembly_dim
}
// 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
// projection coordinates, and the projection is done with respect to the
@ -240,6 +241,7 @@ impl DescentHistory {
pub struct ConstraintProblem {
pub gram: PartialMatrix,
pub soft: PartialMatrix,
pub frozen: PartialMatrix,
pub guess: DMatrix<f64>,
}
@ -249,15 +251,17 @@ impl ConstraintProblem {
const ELEMENT_DIM: usize = 5;
Self {
gram: PartialMatrix::new(),
soft: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
}
}
#[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
Self {
gram: PartialMatrix::new(),
soft: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns),
}
@ -280,14 +284,18 @@ lazy_static! {
struct SearchState {
config: DMatrix<f64>,
err_proj: DMatrix<f64>,
loss_hard: f64,
loss: f64,
}
impl SearchState {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self {
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
fn from_config(gram: &PartialMatrix, soft: &PartialMatrix, softness: f64, config: DMatrix<f64>) -> Self {
let config_gram = &(config.tr_mul(&*Q) * &config);
let err_proj_hard = gram.sub_proj(config_gram);
let err_proj = &err_proj_hard + softness * soft.sub_proj(config_gram);
let loss_hard = err_proj_hard.norm_squared();
let loss = err_proj.norm_squared();
Self { config, err_proj, loss }
Self { config, err_proj, loss_hard, loss }
}
}
@ -331,6 +339,8 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
// use backtracking line search to find a better configuration
fn seek_better_config(
gram: &PartialMatrix,
soft: &PartialMatrix,
softness: f64,
state: &SearchState,
base_step: &DMatrix<f64>,
base_target_improvement: f64,
@ -341,7 +351,7 @@ fn seek_better_config(
let mut rate = 1.0;
for backoff_steps in 0..max_backoff_steps {
let trial_config = &state.config + rate * base_step;
let trial_state = SearchState::from_config(gram, trial_config);
let trial_state = SearchState::from_config(gram, soft, softness, trial_config);
let improvement = state.loss - trial_state.loss;
if improvement >= min_efficiency * rate * base_target_improvement {
return Some((trial_state, backoff_steps));
@ -376,11 +386,11 @@ pub fn realize_gram(
max_backoff_steps: i32,
) -> Realization {
// destructure the problem data
let ConstraintProblem { gram, guess, frozen } = problem;
let ConstraintProblem { gram, soft, guess, frozen } = problem;
// start the descent history
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
@ -394,29 +404,34 @@ pub fn realize_gram(
);
return Realization { result, history };
}
// find the dimension of the search space
let element_dim = guess.nrows();
let total_dim = element_dim * assembly_dim;
// scale the tolerance
let scale_adjustment = (gram.0.len() as f64).sqrt();
let tol = scale_adjustment * scaled_tol;
// set up constants and variables related to minimizing the soft loss
const GRAD_TOL: f64 = 1e-12;
let mut grad_size = f64::INFINITY;
let mut softness = 1.0;
// convert the frozen indices to stacked format
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|MatrixEntry { index: (row, col), .. }| col*element_dim + row
).collect();
// 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, soft, softness, frozen.freeze(guess));
let mut hess = DMatrix::zeros(element_dim, assembly_dim);
for _ in 0..max_descent_steps {
// find the negative gradient of the loss function
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
history.neg_grad.push(neg_grad.clone());
// find the negative Hessian of the loss function
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
for col in 0..assembly_dim {
@ -426,7 +441,7 @@ pub fn realize_gram(
let neg_d_err =
basis_mat.tr_mul(&*Q) * &state.config
+ state.config.tr_mul(&*Q) * &basis_mat;
let neg_d_err_proj = gram.proj(&neg_d_err);
let neg_d_err_proj = gram.proj(&neg_d_err) + softness * soft.proj(&neg_d_err);
let deriv_grad = 4.0 * &*Q * (
-&basis_mat * &state.err_proj
+ &state.config * &neg_d_err_proj
@ -435,7 +450,7 @@ pub fn realize_gram(
}
}
hess = DMatrix::from_columns(hess_cols.as_slice());
// regularize the Hessian
let hess_eigvals = hess.symmetric_eigenvalues();
let min_eigval = hess_eigvals.min();
@ -443,7 +458,7 @@ pub fn realize_gram(
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
}
history.hess_eigvals.push(hess_eigvals);
// project the negative gradient and negative Hessian onto the
// orthogonal complement of the frozen subspace
let zero_col = DVector::zeros(total_dim);
@ -454,12 +469,15 @@ pub fn realize_gram(
hess.set_column(k, &zero_col);
hess[(k, k)] = 1.0;
}
// stop if the loss is tolerably low
// stop if the hard loss is tolerably low and the total loss is close to
// stationary. we use `neg_grad_stacked` to measure the size of the
// gradient because it's been projected onto the frozen subspace
history.config.push(state.config.clone());
history.scaled_loss.push(state.loss / scale_adjustment);
if state.loss < tol { break; }
history.scaled_loss.push(state.loss_hard / scale_adjustment);
grad_size = neg_grad_stacked.norm_squared();
if state.loss_hard < tol && grad_size < softness * GRAD_TOL { break; }
// compute the Newton step
/* TO DO */
/*
@ -479,10 +497,10 @@ pub fn realize_gram(
let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked);
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
history.base_step.push(base_step.clone());
// use backtracking line search to find a better configuration
if let Some((better_state, backoff_steps)) = seek_better_config(
gram, &state, &base_step, neg_grad.dot(&base_step),
gram, soft, softness, &state, &base_step, neg_grad.dot(&base_step),
min_efficiency, backoff, max_backoff_steps,
) {
state = better_state;
@ -493,8 +511,18 @@ pub fn realize_gram(
history,
};
}
// if we're near a minimum of the total loss, but the hard loss still
// isn't tolerably low, make the soft constraints softer
const SOFTNESS_BACKOFF_THRESHOLD: f64 = 1e-6;
const SOFTNESS_BACKOFF: f64 = 0.95;
if state.loss_hard >= tol && grad_size < softness * SOFTNESS_BACKOFF_THRESHOLD {
softness *= SOFTNESS_BACKOFF;
state = SearchState::from_config(gram, soft, softness, state.config);
console_log!("Softness decreased to {softness}");
}
}
let result = if state.loss < tol {
let result = if state.loss_hard < tol && grad_size < softness * GRAD_TOL {
// express the uniform basis in the standard basis
const UNIFORM_DIM: usize = 4;
let total_dim_unif = UNIFORM_DIM * assembly_dim;
@ -505,10 +533,10 @@ pub fn realize_gram(
.view_mut(block_start, (element_dim, UNIFORM_DIM))
.copy_from(&local_unif_to_std(state.config.column(n)));
}
// find the kernel of the Hessian. give it the uniform inner product
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent })
} else {
Err("Failed to reach target accuracy".to_string())
@ -521,9 +549,9 @@ pub fn realize_gram(
#[cfg(feature = "dev")]
pub mod examples {
use std::f64::consts::PI;
use super::*;
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
// below includes a nice translation of the problem statement, which was
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
@ -547,40 +575,40 @@ pub mod examples {
)
).collect::<Vec<_>>().as_slice()
);
for s in 0..9 {
// each sphere is represented by a spacelike vector
problem.gram.push_sym(s, s, 1.0);
// the circumscribing sphere is tangent to all of the other
// spheres, with matching orientation
if s > 0 {
problem.gram.push_sym(0, s, 1.0);
}
if s > 2 {
// each chain sphere is tangent to the "sun" and "moon"
// spheres, with opposing orientation
for n in 1..3 {
problem.gram.push_sym(s, n, -1.0);
}
// each chain sphere is tangent to the next chain sphere,
// with opposing orientation
let s_next = 3 + (s-2) % 6;
problem.gram.push_sym(s, s_next, -1.0);
}
}
// the frozen entries fix the radii of the circumscribing sphere, the
// "sun" and "moon" spheres, and one of the chain spheres
for k in 0..4 {
problem.frozen.push(3, k, problem.guess[(3, k)]);
}
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,
// and find its tangent space
pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization {
@ -601,7 +629,7 @@ pub mod examples {
}
).collect::<Vec<_>>().as_slice()
);
const N_POINTS: usize = 2 * N_HINGES;
for block in (0..N_POINTS).step_by(2) {
let block_next = (block + 2) % N_POINTS;
@ -610,18 +638,18 @@ pub mod examples {
for k in j..2 {
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
}
// non-hinge edges
for k in 0..2 {
problem.gram.push_sym(block + j, block_next + k, -0.625);
}
}
}
for k in 0..N_POINTS {
problem.frozen.push(3, k, problem.guess[(3, k)])
}
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
}
}
@ -630,9 +658,9 @@ pub mod examples {
mod tests {
use nalgebra::Vector3;
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter};
use super::{*, examples::*};
#[test]
fn freeze_test() {
let frozen = PartialMatrix(vec![
@ -651,7 +679,7 @@ mod tests {
]);
assert_eq!(frozen.freeze(&config), expected_result);
}
#[test]
fn sub_proj_test() {
let target = PartialMatrix(vec![
@ -670,7 +698,7 @@ mod tests {
]);
assert_eq!(target.sub_proj(&attempt), expected_result);
}
#[test]
fn zero_loss_test() {
let mut gram = PartialMatrix::new();
@ -690,7 +718,7 @@ mod tests {
let state = SearchState::from_config(&gram, config);
assert!(state.loss.abs() < f64::EPSILON);
}
/* TO DO */
// at the frozen indices, the optimization steps should have exact zeros,
// and the realized configuration should have the desired values
@ -720,13 +748,13 @@ mod tests {
assert_eq!(config[index], value);
}
}
#[test]
fn irisawa_hexlet_test() {
// solve Irisawa's problem
const SCALED_TOL: f64 = 1.0e-12;
let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config;
// check against Irisawa's solution
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];
@ -734,7 +762,7 @@ mod tests {
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
}
}
#[test]
fn tangent_test_three_spheres() {
const SCALED_TOL: f64 = 1.0e-12;
@ -758,7 +786,7 @@ mod tests {
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
assert_eq!(config, problem.guess);
assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of
// the solution variety
const UNIFORM_DIM: usize = 4;
@ -786,11 +814,11 @@ mod tests {
0.0, 0.0, -1.0, 0.25, 1.0,
]),
];
// confirm that the dimension of the tangent space is no greater than
// expected
assert_eq!(tangent.basis_std.len(), tangent_motions_std.len());
// confirm that the tangent space contains all the motions we expect it
// 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
@ -802,13 +830,13 @@ mod tests {
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
}
}
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
let mut elt_motion = DVector::zeros(4);
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
iter::repeat(elt_motion).take(assembly_dim).collect()
}
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
points.into_iter().map(
|pt| {
@ -819,7 +847,7 @@ mod tests {
}
).collect()
}
#[test]
fn tangent_test_kaleidocycle() {
// set up a kaleidocycle and find its tangent space
@ -827,7 +855,7 @@ mod tests {
let Realization { result, history } = realize_kaleidocycle(SCALED_TOL);
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of
// the solution variety
const N_HINGES: usize = 6;
@ -838,12 +866,12 @@ mod tests {
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, 0.0, 1.0), assembly_dim),
// 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(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()),
// the twist motion. more precisely: a motion that keeps the center
// of mass stationary and preserves the distances between the
// vertices to first order. this has to be the twist as long as:
@ -872,11 +900,11 @@ mod tests {
).collect::<Vec<_>>()
)
).collect::<Vec<_>>();
// confirm that the dimension of the tangent space is no greater than
// expected
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
// confirm that the tangent space contains all the motions we expect it
// 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
@ -888,7 +916,7 @@ mod tests {
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
}
}
fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
const ELEMENT_DIM: usize = 5;
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
@ -899,7 +927,7 @@ mod tests {
0.0, 0.0, 0.0, 0.0, 1.0,
])
}
// confirm that projection onto a configuration subspace is equivariant with
// respect to Euclidean motions
#[test]
@ -919,7 +947,7 @@ mod tests {
let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap();
assert_eq!(config_orig, problem_orig.guess);
assert_eq!(history_orig.scaled_loss.len(), 1);
// find another pair of spheres that meet at 120°. we'll think of this
// solution as a transformed version of the original one
let guess_tfm = {
@ -940,17 +968,17 @@ mod tests {
let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap();
assert_eq!(config_tfm, problem_tfm.guess);
assert_eq!(history_tfm.scaled_loss.len(), 1);
// project a nudge to the tangent space of the solution variety at the
// original solution
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);
// project the equivalent nudge to the tangent space of the 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_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
// take the transformation that sends the original solution to the
// transformed solution and apply it to the motion that the original
// solution makes in response to the nudge
@ -964,7 +992,7 @@ mod tests {
]);
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
let motion_proj_tfm = transl * rot * motion_orig_proj;
// confirm that the projection of the nudge is equivariant. we loosen
// the comparison tolerance because the transformation seems to
// introduce some numerical error
@ -972,4 +1000,4 @@ mod tests {
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);
}
}
}

View file

@ -30,7 +30,7 @@ impl AppState {
selection: create_signal(BTreeSet::default()),
}
}
// in single-selection mode, select the given element. in multiple-selection
// mode, toggle whether the given element is selected
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
@ -53,10 +53,10 @@ fn main() {
// set the console error panic hook
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
sycamore::render(|| {
provide_context(AppState::new());
view! {
div(id = "sidebar") {
AddRemove {}
@ -66,4 +66,4 @@ fn main() {
Display {}
}
});
}
}

View file

@ -20,7 +20,7 @@ impl SpecifiedValue {
pub fn from_empty_spec() -> Self {
Self { spec: String::new(), value: None }
}
pub fn is_present(&self) -> bool {
matches!(self.value, Some(_))
}
@ -42,7 +42,7 @@ impl From<Option<f64>> for SpecifiedValue {
// if the specification is properly formatted, and `Error` if not
impl TryFrom<String> for SpecifiedValue {
type Error = ParseFloatError;
fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() {
Ok(Self::from_empty_spec())
@ -52,4 +52,4 @@ impl TryFrom<String> for SpecifiedValue {
)
}
}
}
}

View file

@ -0,0 +1,59 @@
import collections
import math
import sys
def read_edge_distortions(filename):
vertices = set()
distortions = {}
with open(filename, 'r') as edge_file:
while edge_line := edge_file.readline():
line_parts = edge_line.rstrip().split(': ')
endpoints = tuple(sorted(line_parts[0].split(', ')))
vertices.update(endpoints)
if len(line_parts) > 1:
distortions[endpoints] = float(line_parts[1])
else:
distortions[endpoints] = 0
return (vertices, distortions)
def find_triangles(vertices, edges):
triangles = []
for e in sorted(edges):
for v in sorted(vertices):
if e[1] < v:
if (e[0], v) in edges and (e[1], v) in edges:
triangles.append((e[0], e[1], v))
return triangles
# use the law of cosines to get the angle distortion
def angle_distortion(edge_distortions):
a, b, c = list(edge_distortions)
cos_angle_a = (1 + 2*(b + c - a) + b*b + c*c - a*a) / (2*(1 + b)*(1 + c))
return math.degrees(math.acos(cos_angle_a)) - 60
if __name__ == '__main__':
if len(sys.argv) <= 1:
print('Pass the path to the file that lists the edge distortions')
else:
vertices, distortions = read_edge_distortions(sys.argv[1])
triangles = find_triangles(vertices, distortions.keys())
total_angle_distortion = 0
highest_angle_distortion = -math.inf
lowest_angle_distortion = math.inf
print('{} triangles\n'.format(len(triangles)))
for t in triangles:
print('Triangle {0}, {1}, {2}'.format(t[0], t[1], t[2]))
edge_distortions = collections.deque(
[distortions[(t[j], t[k])] for (j, k) in [(1, 2), (0, 1), (0, 2)]]
)
for k in range(3):
ang_distort = angle_distortion(edge_distortions)
total_angle_distortion += abs(ang_distort)
highest_angle_distortion = max(highest_angle_distortion, ang_distort)
lowest_angle_distortion = min(lowest_angle_distortion, ang_distort)
print(' {0}: {1}°'.format(t[k], ang_distort))
edge_distortions.rotate()
print()
print('Total angle distortion: {}°'.format(total_angle_distortion))
print('Highest angle distortion: {}°'.format(highest_angle_distortion))
print('Lowest angle distortion: {}°'.format(lowest_angle_distortion))

150
impossolid/twice-augmented Normal file
View file

@ -0,0 +1,150 @@
=== Distortions of flexible edges (for labels) ===
--- Range ---
Highest: 0.000039581305103782685
Lowest: -0.00003959022952266363
--- Table ---
a_SE, b_SE: 0.000015249448583054653
b_SW, c_S: 0.000006918255000202575
b_SE, c_S: 0.00001022525369434997
b_SE, c_E: 0.000010225282824746383
b_NE, c_E: 0.000006918450075555058
z_S, a_SW: -0.0000219346808875234
z_S, a_SE: 0.00002021118621352795
z_E, a_SE: 0.00002021122888534378
z_E, a_NE: -0.00002193465480671757
z_S, b_SW: 0.000015276737515369292
z_S, b_SE: -0.000010771908316892743
z_E, b_SE: -0.00001077200999765046
z_E, b_NE: 0.000015276631362360217
z_S, c_S: -0.0000027853351723499016
z_E, c_E: -0.000002785228607191555
c_N, d_NE: -0.000007632775579087326
c_N, d_NW: 0.00001402754980516112
c_W, d_NW: 0.000014028001282264439
c_W, d_SW: -0.00000763316944431703
c_S, d_SW: 0.000006566974972180328
c_S, d_SE: -0.0000033161321458747414
c_E, d_SE: -0.00000331586236646513
c_E, d_NE: 0.000006566624318564243
y_NE, b_NE: -0.0000017608392620486498
y_NW, b_NW: -0.00003959022952266363
y_SW, b_SW: -0.0000017608153313274316
y_SE, b_SE: 0.000005637007821097203
y_NE, c_N: 0.000001760310254250749
y_NW, c_N: 0.00003958128591112948
y_NW, c_W: 0.000039581305103782685
y_SW, c_W: 0.0000017603065237110672
y_SW, c_S: 0.0000017602552603493003
y_SE, c_S: -0.00000563497752606038
y_SE, c_E: -0.0000056349905677193694
y_NE, c_E: 0.0000017602528935919282
y_NE, d_NE: -0.0000017608481082635805
y_NW, d_NW: -0.0000395891283971962
y_SW, d_SW: -0.0000017608381747596222
y_SE, d_SE: 0.0000056371661583272735
d_NE, e_N: 0.000007303314580670295
d_NW, e_N: -0.000016255656816635924
d_NW, e_W: -0.000016256069869023512
d_SW, e_W: 0.00000730369455466398
d_SW, e_S: -0.000006000901856373712
d_SE, e_S: 0.000003879307405619109
d_SE, e_E: 0.000003879053499059208
d_NE, e_E: -0.00000600059436381427
c_N, e_N: 0.00000899767908293228
c_W, e_W: 0.000008997328779446816
c_S, e_S: 0.0000042757526759396545
c_E, e_E: 0.000004275945999382972
e_N, f_NE: 0.0000026971057195376348
e_N, f_NW: 0.000006498321003978716
e_W, f_NW: 0.000006498333167484994
e_W, f_SW: 0.000002697065747495785
e_S, f_SW: 0.000001359196116957704
e_S, f_SE: 0.0000010550443264554641
e_E, f_SE: 0.0000010550540852081317
e_E, f_NE: 0.000001359177866673992
d_NE, f_NE: -0.0000029405886467824407
d_NW, f_NW: -0.000009421855439304757
d_SW, f_SW: -0.000002940615976753832
d_SE, f_SE: -0.000001529705558996715
f_NE, g_ENE: -0.00000011649787286240574
f_NE, g_NNE: -0.000002135681696645076
f_NW, g_NNW: -0.000003607774033318148
f_NW, g_WNW: -0.000003607785721714447
f_SW, g_WSW: -0.0000021356459140809334
f_SW, g_SSW: -0.00000011652747271646458
f_SE, g_SSE: -0.0000005858121892925972
f_SE, g_ESE: -0.0000005858113552594831
e_N, g_NNE: -0.0000034483169191011346
e_N, g_NNW: 0.0000034059972640161084
e_W, g_WNW: 0.000003405934362501001
e_W, g_WSW: -0.00000344858668736309
e_S, g_SSW: 0.0000029689592297656908
e_S, g_SSE: -0.0000006279732347365118
e_E, g_ESE: -0.0000006278110244023648
e_E, g_ENE: 0.0000029689801689897473
=== Distortions of rigid edges (for validation) ===
These values should be small relative to the ones for the flexible edges
--- Range ---
Largest absolute: 0.00000000002916368237382178
--- Table ---
a_NE, a_NW: 0.000000000024039999653399018
a_NW, a_SW: 0.00000000002451793579782236
a_SW, a_SE: -0.000000000028570972470668657
a_SE, a_NE: -0.00000000002916368237382178
a_NE, b_NE: -0.000000000022712800498073622
a_NW, b_NW: 0.0000000000010783395006240168
a_SW, b_SW: -0.00000000002146677512286218
b_NE, c_N: 0.00000000000907905964233893
b_NW, c_N: -0.00000000002003437977280497
b_NW, c_W: -0.000000000019747523880603447
b_SW, c_W: 0.000000000009824853560213723
g_NNE, g_NNW: 0.0000000000003400820265509057
g_NNW, g_WNW: -0.0000000000004886127731423908
g_WNW, g_WSW: 0.00000000000005762239323369454
g_WSW, g_SSW: -0.00000000000020348398264541725
g_SSW, g_SSE: -0.0000000000010258984125039788
g_SSE, g_ESE: -0.0000000000001306316925624901
g_ESE, g_ENE: -0.0000000000011334497459238174
g_ENE, g_NNE: -0.00000000000007913265991766227
a_SE, a_NW: 0.00000000001299251509560824
a_SW, a_NE: -0.000000000019852249047597653
a_NW, c_N: 0.00000000000757318396521532
b_NW, b_NE: -0.000000000015613313427643198
c_N, a_NE: -0.000000000013250324277324116
a_NW, b_NE: 0.0000000000145795645528458
b_NW, a_NE: 0.000000000004191204809210469
a_SW, c_W: -0.000000000012779924576702457
b_SW, b_NW: -0.000000000015326928563179278
c_W, a_NW: 0.00000000000786836134744787
a_SW, b_NW: 0.000000000004368311238549999
b_SW, a_NW: 0.00000000001505750069726914
g_NNE, g_WNW: 0.0000000000005718276734526309
g_NNW, g_WSW: 0.0000000000004788781998985515
g_WNW, g_SSW: -0.00000000000035703902510469045
g_WSW, g_SSE: 0.0000000000002254652770669901
g_SSW, g_ESE: -0.0000000000002515288118811408
g_SSE, g_ENE: -0.00000000000030679606642680965
g_NNE, g_WSW: 0.0000000000003206128800632269
g_NNW, g_SSW: 0.00000000000032846334235664577
g_WNW, g_SSE: 0.00000000000012278123026907122
g_WSW, g_ESE: 0.00000000000014507654318238083
g_SSW, g_ENE: 0.000000000000033599978615832786
g_NNE, g_SSW: 0.0000000000001815026882238444
g_NNW, g_SSE: 0.0000000000002631474960754007
g_WNW, g_ESE: 0.00000000000031684465816238584
g_WSW, g_ENE: 0.0000000000002348858318190928
g_NNE, g_SSE: 0.00000000000007913265991766227
g_NNW, g_ESE: 0.0000000000001438204692154338
g_WNW, g_ENE: 0.0000000000004248670193198296
g_NNE, g_ESE: 0.00000000000019783164979415566
g_NNW, g_ENE: -0.0000000000002364559242777765

View file

@ -0,0 +1,295 @@
58 triangles
Triangle a_NE, a_SE, z_E
a_NE: 0.0020627780860564826°
a_SE: -0.0021197445448706276°
z_E: 5.696645881414497e-05°
Triangle a_NE, b_NE, z_E
a_NE: 0.0017363010366011622°
b_NE: -0.001956513180971342°
z_E: 0.00022021214437017989°
Triangle a_SE, a_SW, z_S
a_SE: -0.0021197448587955137°
a_SW: 0.0020627761256122312°
z_S: 5.696873318328244e-05°
Triangle a_SE, b_SE, z_E
a_SE: -0.0018856584331388149°
b_SE: 0.0011890357645540917°
z_E: 0.0006966226686131449°
Triangle a_SE, b_SE, z_S
a_SE: -0.0018856502946889009°
b_SE: 0.0011890295778727022°
z_S: 0.0006966207168233041°
Triangle a_SW, b_SW, z_S
a_SW: 0.0017363089225312933°
b_SW: -0.001956518417863151°
z_S: 0.00022020949535317413°
Triangle b_NE, c_E, y_NE
b_NE: -5.4156720231901545e-05°
c_E: -0.00040358386964811643°
y_NE: 0.00045774058987291255°
Triangle b_NE, c_E, z_E
b_NE: -0.0009184660407299816°
c_E: 0.0008739657117544652°
z_E: 4.450032897551637e-05°
Triangle b_NE, c_N, y_NE
b_NE: 0.00017470943667774463°
c_N: -0.0001747266285079263°
y_NE: 1.7191837287100498e-08°
Triangle b_NW, c_N, y_NW
b_NW: 0.003928388803437599°
c_N: -0.0039285291447015425°
y_NW: 1.4034125683792809e-07°
Triangle b_NW, c_W, y_NW
b_NW: 0.0039283900732698385°
c_W: -0.0039285297795643714°
y_NW: 1.3970630163839814e-07°
Triangle b_SE, c_E, y_SE
b_SE: -0.000897519700536975°
c_E: 0.0002210891647536073°
y_SE: 0.0006764305357691569°
Triangle b_SE, c_E, z_E
b_SE: -0.0001661945662831954°
c_E: -0.0009587837744717831°
z_E: 0.0011249783407549785°
Triangle b_SE, c_S, y_SE
b_SE: -0.0008975178741081891°
c_S: 0.00022108969701406522°
y_SE: 0.0006764281771083347°
Triangle b_SE, c_S, z_S
b_SE: -0.00016620401637368332°
c_S: -0.0009587725587678619°
z_S: 0.0011249765751415453°
Triangle b_SW, c_S, y_SW
b_SW: -5.415090214455631e-05°
c_S: -0.00040357591168316276°
y_SW: 0.00045772681384903535°
Triangle b_SW, c_S, z_S
b_SW: -0.0009184701495854597°
c_S: 0.00087398271318051°
z_S: 4.448743641916053e-05°
Triangle b_SW, c_W, y_SW
b_SW: 0.00017470839824795803°
c_W: -0.0001747249218624347°
y_SW: 1.6523628687536984e-08°
Triangle c_E, d_NE, e_E
c_E: -0.0007556600615572506°
d_NE: 0.0002641663727089849°
e_E: 0.0004914936888269494°
Triangle c_E, d_NE, y_NE
c_E: -0.0003919462083246117°
d_NE: -4.2518017124848484e-05°
y_NE: 0.00043446422546367103°
Triangle c_E, d_SE, e_E
c_E: 0.00022487539229842923°
d_SE: 0.000264262914654978°
e_E: -0.0004891383069605126°
Triangle c_E, d_SE, y_SE
c_E: 0.0006690477311224186°
d_SE: -0.0004495970440387964°
y_SE: -0.000219450687097833°
Triangle c_N, d_NE, e_N
c_N: 0.00043802608161769285°
d_NE: 0.0006061756293931353°
e_N: -0.0010442017109895119°
Triangle c_N, d_NE, y_NE
c_N: 7.77608707309696e-05°
d_NE: 0.0004272013700656885°
y_NE: -0.0005049622408037635°
Triangle c_N, d_NW, e_N
c_N: -0.0018371050143599632°
d_NW: 0.0006689659636691658°
e_N: 0.0011681390506907974°
Triangle c_N, d_NW, y_NW
c_N: -0.004392411525167006°
d_NW: 0.0034642502022776966°
y_NW: 0.0009281613228964147°
Triangle c_S, d_SE, e_S
c_S: 0.00022490750998827025°
d_SE: 0.00026425064935864384°
e_S: -0.0004891581593469141°
Triangle c_S, d_SE, y_SE
c_S: 0.0006690562241331577°
d_SE: -0.000449587257172368°
y_SE: -0.00021946896695368423°
Triangle c_S, d_SW, e_S
c_S: -0.0007556856092989506°
d_SW: 0.0002641521543509384°
e_S: 0.0004915334549480121°
Triangle c_S, d_SW, y_SW
c_S: -0.0003919572288921813°
d_SW: -4.252978876451152e-05°
y_SW: 0.00043448701768511455°
Triangle c_W, d_NW, e_W
c_W: -0.0018371356879498535°
d_NW: 0.0006689415152791867°
e_W: 0.0011681941726706668°
Triangle c_W, d_NW, y_NW
c_W: -0.00439242709246912°
d_NW: 0.00346423653491712°
y_NW: 0.000928190557552°
Triangle c_W, d_SW, e_W
c_W: 0.0004380758375859273°
d_SW: 0.0006061529123826404°
e_W: -0.0010442287499685676°
Triangle c_W, d_SW, y_SW
c_W: 7.777468017877709e-05°
d_SW: 0.00042721382367005845°
y_SW: -0.0005049885038488355°
Triangle d_NE, e_E, f_NE
d_NE: 0.00038569630375207°
e_E: -4.1012391825745453e-05°
f_NE: -0.0003446839119192191°
Triangle d_NE, e_N, f_NE
d_NE: 3.4118590889420375e-05°
e_N: -0.0005253562237612641°
f_NE: 0.0004912376328576329°
Triangle d_NW, e_N, f_NW
d_NW: 0.0012793501149417352°
e_N: -0.0003005889518448157°
f_NW: -0.0009787611630969195°
Triangle d_NW, e_W, f_NW
d_NW: 0.0012793645837376744°
e_W: -0.00030057569104258164°
f_NW: -0.0009787888926879873°
Triangle d_SE, e_E, f_SE
d_SE: -7.914704212907964e-06°
e_E: -0.00026442283958516555°
f_SE: 0.0002723375437980735°
Triangle d_SE, e_S, f_SE
d_SE: -7.92374907376825e-06°
e_S: -0.00026443091589101186°
f_SE: 0.0002723546649647801°
Triangle d_SW, e_S, f_SW
d_SW: 0.0003857085871175059°
e_S: -4.10046320098445e-05°
f_SW: -0.00034470395510055596°
Triangle d_SW, e_W, f_SW
d_SW: 3.4104280807412124e-05°
e_W: -0.0005253692789679576°
f_SW: 0.0004912649981676509°
Triangle e_E, f_NE, g_ENE
e_E: -0.00015088143299379908°
f_NE: 0.0001553185337925811°
g_ENE: -4.437100798782012e-06°
Triangle e_E, f_SE, g_ESE
e_E: -5.289010589137888e-05°
f_SE: -5.705811280876105e-05°
g_ESE: 0.00010994821870013993°
Triangle e_E, g_ENE, g_ESE
e_E: -7.744546709176348e-05°
g_ENE: -0.000139748678492424°
g_ESE: 0.0002171941456126092°
Triangle e_N, f_NE, g_NNE
e_N: -0.00011644664380128233°
f_NE: -0.0002467109250190447°
g_NNE: 0.000363157568820327°
Triangle e_N, f_NW, g_NNW
e_N: -0.0005663172445693476°
f_NW: 0.00012971776202874707°
g_NNW: 0.000436599482547706°
Triangle e_N, g_NNE, g_NNW
e_N: 1.3987573481699656e-06°
g_NNE: 0.0003394089509995979°
g_NNW: -0.00034080770834776786°
Triangle e_S, f_SE, g_SSE
e_S: -5.28844724030364e-05°
f_SE: -5.706849415076931e-05°
g_SSE: 0.00010995296653959485°
Triangle e_S, f_SW, g_SSW
e_S: -0.00015088330234647174°
f_SW: 0.00015531752390529618°
g_SSW: -4.434221558824447e-06°
Triangle e_S, g_SSE, g_SSW
e_S: -7.743940860649445e-05°
g_SSE: 0.00021719812617959633°
g_SSW: -0.00013975871755889102°
Triangle e_W, f_NW, g_WNW
e_W: -0.0005663163395084325°
f_NW: 0.00012971358477642525°
g_WNW: 0.00043660275473200727°
Triangle e_W, f_SW, g_WSW
e_W: -0.00011643403040295652°
f_SW: -0.000246728634124338°
g_WSW: 0.0003631626645272945°
Triangle e_W, g_WNW, g_WSW
e_W: 1.4097619143171869e-06°
g_WNW: -0.00034082347526265266°
g_WSW: 0.0003394137133483355°
Triangle f_NE, g_ENE, g_NNE
f_NE: 7.450149718835064e-05°
g_ENE: -0.00013744180584041032°
g_NNE: 6.294030865205968e-05°
Triangle f_NW, g_NNW, g_WNW
f_NW: 0.00023868980004237983°
g_NNW: -0.00011934547999459255°
g_WNW: -0.00011934432004778728°
Triangle f_SE, g_ESE, g_SSE
f_SE: 3.87570213717936e-05°
g_ESE: -1.9378552060800303e-05°
g_SSE: -1.937846930388787e-05°
Triangle f_SW, g_SSW, g_WSW
f_SW: 7.450129267994043e-05°
g_SSW: -0.00013743845931912801°
g_WSW: 6.293716665339844e-05°
Total angle distortion: 0.11233054610809035°
Highest angle distortion: 0.0039283900732698385°
Lowest angle distortion: -0.00439242709246912°

View file

@ -0,0 +1,98 @@
a_SE, b_SE: 0.000015249448583054653
b_SW, c_S: 0.000006918255000202575
b_SE, c_S: 0.00001022525369434997
b_SE, c_E: 0.000010225282824746383
b_NE, c_E: 0.000006918450075555058
z_S, a_SW: -0.0000219346808875234
z_S, a_SE: 0.00002021118621352795
z_E, a_SE: 0.00002021122888534378
z_E, a_NE: -0.00002193465480671757
z_S, b_SW: 0.000015276737515369292
z_S, b_SE: -0.000010771908316892743
z_E, b_SE: -0.00001077200999765046
z_E, b_NE: 0.000015276631362360217
z_S, c_S: -0.0000027853351723499016
z_E, c_E: -0.000002785228607191555
c_N, d_NE: -0.000007632775579087326
c_N, d_NW: 0.00001402754980516112
c_W, d_NW: 0.000014028001282264439
c_W, d_SW: -0.00000763316944431703
c_S, d_SW: 0.000006566974972180328
c_S, d_SE: -0.0000033161321458747414
c_E, d_SE: -0.00000331586236646513
c_E, d_NE: 0.000006566624318564243
y_NE, b_NE: -0.0000017608392620486498
y_NW, b_NW: -0.00003959022952266363
y_SW, b_SW: -0.0000017608153313274316
y_SE, b_SE: 0.000005637007821097203
y_NE, c_N: 0.000001760310254250749
y_NW, c_N: 0.00003958128591112948
y_NW, c_W: 0.000039581305103782685
y_SW, c_W: 0.0000017603065237110672
y_SW, c_S: 0.0000017602552603493003
y_SE, c_S: -0.00000563497752606038
y_SE, c_E: -0.0000056349905677193694
y_NE, c_E: 0.0000017602528935919282
y_NE, d_NE: -0.0000017608481082635805
y_NW, d_NW: -0.0000395891283971962
y_SW, d_SW: -0.0000017608381747596222
y_SE, d_SE: 0.0000056371661583272735
d_NE, e_N: 0.000007303314580670295
d_NW, e_N: -0.000016255656816635924
d_NW, e_W: -0.000016256069869023512
d_SW, e_W: 0.00000730369455466398
d_SW, e_S: -0.000006000901856373712
d_SE, e_S: 0.000003879307405619109
d_SE, e_E: 0.000003879053499059208
d_NE, e_E: -0.00000600059436381427
c_N, e_N: 0.00000899767908293228
c_W, e_W: 0.000008997328779446816
c_S, e_S: 0.0000042757526759396545
c_E, e_E: 0.000004275945999382972
e_N, f_NE: 0.0000026971057195376348
e_N, f_NW: 0.000006498321003978716
e_W, f_NW: 0.000006498333167484994
e_W, f_SW: 0.000002697065747495785
e_S, f_SW: 0.000001359196116957704
e_S, f_SE: 0.0000010550443264554641
e_E, f_SE: 0.0000010550540852081317
e_E, f_NE: 0.000001359177866673992
d_NE, f_NE: -0.0000029405886467824407
d_NW, f_NW: -0.000009421855439304757
d_SW, f_SW: -0.000002940615976753832
d_SE, f_SE: -0.000001529705558996715
f_NE, g_ENE: -0.00000011649787286240574
f_NE, g_NNE: -0.000002135681696645076
f_NW, g_NNW: -0.000003607774033318148
f_NW, g_WNW: -0.000003607785721714447
f_SW, g_WSW: -0.0000021356459140809334
f_SW, g_SSW: -0.00000011652747271646458
f_SE, g_SSE: -0.0000005858121892925972
f_SE, g_ESE: -0.0000005858113552594831
e_N, g_NNE: -0.0000034483169191011346
e_N, g_NNW: 0.0000034059972640161084
e_W, g_WNW: 0.000003405934362501001
e_W, g_WSW: -0.00000344858668736309
e_S, g_SSW: 0.0000029689592297656908
e_S, g_SSE: -0.0000006279732347365118
e_E, g_ESE: -0.0000006278110244023648
e_E, g_ENE: 0.0000029689801689897473
a_NE, a_NW
a_NW, a_SW
a_SW, a_SE
a_SE, a_NE
a_NE, b_NE
a_NW, b_NW
a_SW, b_SW
b_NE, c_N
b_NW, c_N
b_NW, c_W
b_SW, c_W
g_NNE, g_NNW
g_NNW, g_WNW
g_WNW, g_WSW
g_WSW, g_SSW
g_SSW, g_SSE
g_SSE, g_ESE
g_ESE, g_ENE
g_ENE, g_NNE

View file

@ -0,0 +1,38 @@
a_NE: 0.4974660839869507, 0.5025284618118182, -0.21962917587407324
a_NW: -0.5025160085784002, 0.5025084403663106, -0.21364467368968995
a_SW: -0.5025317897634418, -0.4974736643225916, -0.21962716242129268
a_SE: 0.49745030267915796, -0.4974536427543331, -0.22561166444858374
z_S: 0.0023385661484333744, -0.22870402619169428, 0.60063190420276
z_E: 0.22870024445875012, -0.0023413854336990325, 0.6006314485234188
b_NE: 0.8118652156106724, 0.8061651522670251, 0.6797917189944392
b_NW: -0.8061397981609031, 0.8061327572110616, 0.6894748470124282
b_SW: -0.8061653323313128, -0.8118722761768307, 0.679794976829472
b_SE: 0.8117686673287107, -0.8117688670778128, 0.6701787153604973
y_NE: 0.1103960413184542, 0.10342511854710223, 0.798509539299725
y_NW: -0.10337789916924411, 0.1033740688261596, 0.7998232055607494
y_SW: -0.10342804260149929, -0.11039987437192703, 0.7985099696006345
y_SE: 0.11038804399025821, -0.11039097027269067, 0.7972438212830046
c_N: 0.006192472465608106, 0.9938029259455414, 1.2416489044968044
c_W: -0.9938054007689278, -0.006199199673009326, 1.2416509179495838
c_S: 0.006109939936188693, -1.0059122097709343, 1.229859721030557
c_E: 1.0059097110061566, -0.00610818737102383, 1.2298577082098059
d_NE: 0.19998199287503826, 0.18129904829618806, 1.7914377777202868
d_NW: -0.18132979121792334, 0.1813276309223748, 1.7936881702060035
d_SW: -0.18129959231726273, -0.19998415749541185, 1.791438545222019
d_SE: 0.1999582803508571, -0.1999588290387209, 1.7891944479793136
e_N: 0.011969023209503969, 1.0551851488412447, 2.239755536903205
e_W: -1.0551855894711406, -0.011973997889437291, 2.2397576852440584
e_S: 0.011933151451289365, -1.0790685618433953, 2.2271674919335998
e_E: 1.0790680956836711, -0.011929083518777098, 2.2271653437223433
f_NE: 0.29554147977141343, 0.26516582064069727, 2.7833192564200235
f_NW: -0.26515631709994025, 0.26515579997276006, 2.786626792661052
f_SW: -0.26516396217611815, -0.2955420036217133, 2.783320385102162
f_SE: 0.2955327295186172, -0.29553087779401516, 2.780016555503281
g_NNE: 0.5169980642139731, 1.1901085117178565, 3.092242755151354
g_NNW: -0.4829845300027995, 1.1900889935835326, 3.0981428299427223
g_WNW: -1.190089809078002, 0.4829807102891247, 3.0981442533320376
g_WSW: -1.1901050905533348, -0.5170018958762016, 3.0922461915171433
g_SSW: -0.48302142275001, -1.2240825763590955, 3.083903649106097
g_SSE: 0.516961171466923, -1.224063058223441, 3.0780035743129184
g_ESE: 1.224066450541603, -0.516954774929556, 3.078002150923603
g_ENE: 1.2240817320182662, 0.4830278312359312, 3.083900212740308

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:
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.
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.
@ -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),$$
which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector
$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$
In the `engine` module, these formulas are encoded in the `sphere` and `point` functions.
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.
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.)
2. Consider ambient complex spaces.
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:
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).
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>
</html>