forked from StudioInfinity/dyna3
Compare commits
2 commits
rewind-his
...
main
Author | SHA1 | Date | |
---|---|---|---|
2c8c09d20d | |||
978f70aac7 |
10 changed files with 332 additions and 68 deletions
64
README.md
64
README.md
|
@ -12,11 +12,11 @@ Note that currently this is just the barest beginnings of the project, more of a
|
|||
|
||||
### Implementation goals
|
||||
|
||||
* Comfortable, intuitive UI
|
||||
* Provide a comfortable, intuitive UI
|
||||
|
||||
* Able to run in browser (so implemented in WASM-compatible language)
|
||||
* Allow execution 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,38 +24,40 @@ 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
|
||||
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`.)
|
||||
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
|
||||
- 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`.
|
||||
|
||||
### 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
|
||||
- For each example problem, the engine will print the value of the loss function at each optimization step.
|
||||
- The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then execute
|
||||
|
||||
```julia
|
||||
include("irisawa-hexlet.jl")
|
||||
|
@ -64,24 +66,24 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
|
|||
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
21
app-proto/Cargo.lock
generated
|
@ -255,6 +255,7 @@ dependencies = [
|
|||
"charming",
|
||||
"console_error_panic_hook",
|
||||
"dyna3",
|
||||
"enum-iterator",
|
||||
"itertools",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
|
@ -271,6 +272,26 @@ 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"
|
||||
|
|
|
@ -10,6 +10,7 @@ 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"
|
||||
|
|
|
@ -184,6 +184,7 @@ details[open]:has(li) .element-switch::after {
|
|||
|
||||
#diagnostics-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#realization-status {
|
||||
|
@ -207,6 +208,14 @@ details[open]:has(li) .element-switch::after {
|
|||
content: '⚠';
|
||||
}
|
||||
|
||||
#step-input > label {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
#step-input > input {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.diagnostics-panel {
|
||||
margin-top: 10px;
|
||||
min-height: 180px;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use enum_iterator::{all, Sequence};
|
||||
use nalgebra::{DMatrix, DVector, DVectorView};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt,
|
||||
fmt::{Debug, Formatter},
|
||||
fmt::{Debug, Display, Formatter},
|
||||
hash::{Hash, Hasher},
|
||||
rc::Rc,
|
||||
sync::{atomic, atomic::AtomicU64},
|
||||
|
@ -26,6 +27,7 @@ use crate::{
|
|||
ConfigSubspace,
|
||||
ConstraintProblem,
|
||||
DescentHistory,
|
||||
MatrixEntry,
|
||||
Realization,
|
||||
},
|
||||
specified::SpecifiedValue,
|
||||
|
@ -84,6 +86,14 @@ 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);
|
||||
}
|
||||
|
@ -125,8 +135,8 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
|
|||
}
|
||||
|
||||
impl Debug for dyn Element {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
self.id().fmt(f)
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Debug::fmt(&self.id(), f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,8 +259,7 @@ impl Serial for Sphere {
|
|||
impl ProblemPoser for Sphere {
|
||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||
let index = self.column_index().expect(
|
||||
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||
);
|
||||
indexing_error("Sphere", &self.id, "it").as_str());
|
||||
problem.gram.push_sym(index, index, 1.0);
|
||||
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
||||
}
|
||||
|
@ -269,6 +278,7 @@ pub struct Point {
|
|||
|
||||
impl Point {
|
||||
const WEIGHT_COMPONENT: usize = 3;
|
||||
const NORM_COMPONENT: usize = 4;
|
||||
|
||||
pub fn new(
|
||||
id: String,
|
||||
|
@ -303,6 +313,15 @@ impl Element for Point {
|
|||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -345,8 +364,7 @@ impl Serial for Point {
|
|||
impl ProblemPoser for Point {
|
||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||
let index = self.column_index().expect(
|
||||
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||
);
|
||||
indexing_error("Point", &self.id, "it").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());
|
||||
|
@ -436,8 +454,8 @@ impl ProblemPoser for InversiveDistanceRegulator {
|
|||
if let Some(val) = set_pt.value {
|
||||
let [row, col] = self.subjects.each_ref().map(
|
||||
|subj| subj.column_index().expect(
|
||||
"Subjects should be indexed before inversive distance regulator writes problem data"
|
||||
)
|
||||
indexing_error("Subject", subj.id(),
|
||||
"inversive distance regulator").as_str())
|
||||
);
|
||||
problem.gram.push_sym(row, col, val);
|
||||
}
|
||||
|
@ -446,14 +464,14 @@ impl ProblemPoser for InversiveDistanceRegulator {
|
|||
}
|
||||
|
||||
pub struct HalfCurvatureRegulator {
|
||||
pub subject: Rc<dyn Element>,
|
||||
pub subject: Rc<Sphere>,
|
||||
pub measurement: ReadSignal<f64>,
|
||||
pub set_point: Signal<SpecifiedValue>,
|
||||
serial: u64,
|
||||
}
|
||||
|
||||
impl HalfCurvatureRegulator {
|
||||
pub fn new(subject: Rc<dyn Element>) -> Self {
|
||||
pub fn new(subject: Rc<Sphere>) -> Self {
|
||||
let measurement = subject.representation().map(
|
||||
|rep| rep[Sphere::CURVATURE_COMPONENT]
|
||||
);
|
||||
|
@ -490,14 +508,85 @@ 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(
|
||||
"Subject should be indexed before half-curvature regulator writes problem data"
|
||||
);
|
||||
indexing_error("Subject", &self.subject.id,
|
||||
"half-curvature regulator").as_str());
|
||||
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>,
|
||||
|
@ -534,6 +623,7 @@ pub struct Assembly {
|
|||
// realization diagnostics
|
||||
pub realization_status: Signal<Result<(), String>>,
|
||||
pub descent_history: Signal<DescentHistory>,
|
||||
pub step: Signal<SpecifiedValue>,
|
||||
}
|
||||
|
||||
impl Assembly {
|
||||
|
@ -547,20 +637,33 @@ impl Assembly {
|
|||
realization_trigger: create_signal(()),
|
||||
realization_status: create_signal(Ok(())),
|
||||
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_effect = assembly.clone();
|
||||
let assembly_for_realization = assembly.clone();
|
||||
create_effect(move || {
|
||||
assembly_for_effect.elements.track();
|
||||
assembly_for_effect.regulators.with(
|
||||
assembly_for_realization.elements.track();
|
||||
assembly_for_realization.regulators.with(
|
||||
|regs| for reg in regs {
|
||||
reg.set_point().track();
|
||||
}
|
||||
);
|
||||
assembly_for_effect.realization_trigger.track();
|
||||
assembly_for_effect.realize();
|
||||
assembly_for_realization.realization_trigger.track();
|
||||
assembly_for_realization.realize();
|
||||
});
|
||||
|
||||
// load a configuration from the descent history whenever the active
|
||||
// step is updated
|
||||
let assembly_for_step_selection = assembly.clone();
|
||||
create_effect(move || {
|
||||
if let Some(step) = assembly.step.with(|n| n.value) {
|
||||
let config = assembly.descent_history.with_untracked(
|
||||
|history| history.config[step as usize].clone()
|
||||
);
|
||||
assembly_for_step_selection.load_config(&config)
|
||||
}
|
||||
});
|
||||
|
||||
assembly
|
||||
|
@ -647,6 +750,16 @@ impl Assembly {
|
|||
});
|
||||
}
|
||||
|
||||
// --- updating the configuration ---
|
||||
|
||||
pub fn load_config(&self, config: &DMatrix<f64>) {
|
||||
for elt in self.elements.get_clone_untracked() {
|
||||
elt.representation().update(
|
||||
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- realization ---
|
||||
|
||||
pub fn realize(&self) {
|
||||
|
@ -674,6 +787,7 @@ impl Assembly {
|
|||
/* 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
|
||||
|
@ -696,11 +810,12 @@ impl Assembly {
|
|||
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
|
||||
}
|
||||
|
||||
// report the loss history
|
||||
// report the descent history
|
||||
let step_cnt = history.config.len();
|
||||
self.descent_history.set(history);
|
||||
|
||||
match result {
|
||||
Ok(ConfigNeighborhood { config, nbhd: tangent }) => {
|
||||
Ok(ConfigNeighborhood { nbhd: tangent, .. }) => {
|
||||
/* DEBUG */
|
||||
// report the tangent dimension
|
||||
console_log!("Tangent dimension: {}", tangent.dim());
|
||||
|
@ -708,12 +823,15 @@ impl Assembly {
|
|||
// report the realization status
|
||||
self.realization_status.set(Ok(()));
|
||||
|
||||
// read out the solution
|
||||
for elt in self.elements.get_clone_untracked() {
|
||||
elt.representation().update(
|
||||
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
|
||||
);
|
||||
// display the last realization step
|
||||
self.step.set(
|
||||
if step_cnt > 0 {
|
||||
let last_step = step_cnt - 1;
|
||||
SpecifiedValue::try_from(last_step.to_string()).unwrap()
|
||||
} else {
|
||||
SpecifiedValue::from_empty_spec()
|
||||
}
|
||||
);
|
||||
|
||||
// save the tangent space
|
||||
self.tangent.set_silent(tangent);
|
||||
|
@ -723,7 +841,10 @@ impl Assembly {
|
|||
// setting the status to has a different type than the
|
||||
// `Err(message)` we received from the match: we're changing the
|
||||
// `Ok` type from `Realization` to `()`
|
||||
self.realization_status.set(Err(message))
|
||||
self.realization_status.set(Err(message));
|
||||
|
||||
// display the initial guess
|
||||
self.step.set(SpecifiedValue::from(Some(0.0)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -826,7 +947,8 @@ mod tests {
|
|||
use crate::engine;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
|
||||
#[should_panic(expected =
|
||||
"Sphere \"sphere\" must be indexed before it writes problem data")]
|
||||
fn unindexed_element_test() {
|
||||
let _ = create_root(|| {
|
||||
let elt = Sphere::default("sphere".to_string(), 0);
|
||||
|
@ -835,7 +957,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
|
||||
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \
|
||||
inversive distance regulator writes problem data")]
|
||||
fn unindexed_subject_test_inversive_distance() {
|
||||
let _ = create_root(|| {
|
||||
let subjects = [0, 1].map(
|
||||
|
|
|
@ -7,7 +7,7 @@ use charming::{
|
|||
};
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::{AppState, specified::SpecifiedValue};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DiagnosticsState {
|
||||
|
@ -48,6 +48,69 @@ fn RealizationStatus() -> View {
|
|||
}
|
||||
}
|
||||
|
||||
// history step input
|
||||
#[component]
|
||||
fn StepInput() -> View {
|
||||
// get the assembly
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = state.assembly;
|
||||
|
||||
// the `last_step` signal holds the index of the last step
|
||||
let last_step = assembly.descent_history.map(
|
||||
|history| match history.config.len() {
|
||||
0 => None,
|
||||
n => Some(n - 1),
|
||||
}
|
||||
);
|
||||
let input_max = last_step.map(|last| last.unwrap_or(0));
|
||||
|
||||
// these signals hold the entered step number
|
||||
let value = create_signal(String::new());
|
||||
let value_as_number = create_signal(0.0);
|
||||
|
||||
create_effect(move || {
|
||||
value.set(assembly.step.with(|n| n.spec.clone()));
|
||||
});
|
||||
|
||||
view! {
|
||||
div(id = "step-input") {
|
||||
label { "Step" }
|
||||
input(
|
||||
r#type = "number",
|
||||
min = "0",
|
||||
max = input_max.with(|max| max.to_string()),
|
||||
bind:value = value,
|
||||
bind:valueAsNumber = value_as_number,
|
||||
on:change = move |_| {
|
||||
if last_step.with(|last| last.is_some()) {
|
||||
// clamp the step within its allowed range. the lower
|
||||
// bound is redundant on browsers that make it
|
||||
// impossible to type negative values into a number
|
||||
// input with a non-negative `min`, but there's no harm
|
||||
// in being careful
|
||||
let step_raw = value.with(
|
||||
|val| SpecifiedValue::try_from(val.clone())
|
||||
.unwrap_or(SpecifiedValue::from_empty_spec()
|
||||
)
|
||||
);
|
||||
let step = SpecifiedValue::from(
|
||||
step_raw.value.map(
|
||||
|val| val.clamp(0.0, input_max.get() as f64)
|
||||
)
|
||||
);
|
||||
|
||||
// set the input string and the assembly's active step
|
||||
value.set(step.spec.clone());
|
||||
assembly.step.set(step);
|
||||
} else {
|
||||
value.set(String::new());
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
|
||||
vec![
|
||||
Some(step as f64),
|
||||
|
@ -248,6 +311,7 @@ pub fn Diagnostics() -> View {
|
|||
option(value = "loss") { "Loss" }
|
||||
option(value = "spectrum") { "Spectrum" }
|
||||
}
|
||||
StepInput {}
|
||||
}
|
||||
DiagnosticsPanel(name = "loss") { LossHistory {} }
|
||||
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
|
||||
|
|
|
@ -588,7 +588,25 @@ pub fn Display() -> View {
|
|||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||
|
||||
// manipulate the assembly
|
||||
if state.selection.with(|sel| sel.len() == 1) {
|
||||
/* KLUDGE */
|
||||
// to avoid the complexity of making tangent space projection
|
||||
// conditional and dealing with unnormalized representation vectors,
|
||||
// we only allow manipulation when we're looking at the last step of
|
||||
// a successful realization
|
||||
let realization_successful = state.assembly.realization_status.with(
|
||||
|status| status.is_ok()
|
||||
);
|
||||
let step_val = state.assembly.step.with_untracked(|step| step.value);
|
||||
let on_init_step = step_val.is_some_and(|n| n == 0.0);
|
||||
let on_last_step = step_val.is_some_and(
|
||||
|n| state.assembly.descent_history.with_untracked(
|
||||
|history| n as usize + 1 == history.config.len().max(1)
|
||||
)
|
||||
);
|
||||
let on_manipulable_step =
|
||||
!realization_successful && on_init_step
|
||||
|| realization_successful && on_last_step;
|
||||
if on_manipulable_step && state.selection.with(|sel| sel.len() == 1) {
|
||||
let sel = state.selection.with(
|
||||
|sel| sel.into_iter().next().unwrap().clone()
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
Element,
|
||||
HalfCurvatureRegulator,
|
||||
InversiveDistanceRegulator,
|
||||
PointCoordinateRegulator,
|
||||
Regulator,
|
||||
},
|
||||
specified::SpecifiedValue
|
||||
|
@ -119,6 +120,20 @@ 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 {
|
||||
|
|
|
@ -52,8 +52,8 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
|
|||
// --- partial matrices ---
|
||||
|
||||
pub struct MatrixEntry {
|
||||
index: (usize, usize),
|
||||
value: f64,
|
||||
pub index: (usize, usize),
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
pub struct PartialMatrix(Vec<MatrixEntry>);
|
||||
|
@ -353,7 +353,7 @@ fn seek_better_config(
|
|||
|
||||
// a first-order neighborhood of a configuration
|
||||
pub struct ConfigNeighborhood {
|
||||
pub config: DMatrix<f64>,
|
||||
#[cfg(feature = "dev")] pub config: DMatrix<f64>,
|
||||
pub nbhd: ConfigSubspace,
|
||||
}
|
||||
|
||||
|
@ -388,7 +388,7 @@ pub fn realize_gram(
|
|||
if assembly_dim == 0 {
|
||||
let result = Ok(
|
||||
ConfigNeighborhood {
|
||||
config: guess.clone(),
|
||||
#[cfg(feature = "dev")] config: guess.clone(),
|
||||
nbhd: ConfigSubspace::zero(0),
|
||||
}
|
||||
);
|
||||
|
@ -509,7 +509,7 @@ pub fn realize_gram(
|
|||
// 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 { config: state.config, nbhd: tangent })
|
||||
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent })
|
||||
} else {
|
||||
Err("Failed to reach target accuracy".to_string())
|
||||
};
|
||||
|
|
|
@ -26,6 +26,17 @@ impl SpecifiedValue {
|
|||
}
|
||||
}
|
||||
|
||||
// a `SpecifiedValue` can be constructed from a floating-point option, which is
|
||||
// given a canonical specification
|
||||
impl From<Option<f64>> for SpecifiedValue {
|
||||
fn from(value: Option<f64>) -> Self {
|
||||
match value {
|
||||
Some(x) => SpecifiedValue{ spec: x.to_string(), value },
|
||||
None => SpecifiedValue::from_empty_spec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a `SpecifiedValue` can be constructed from a specification string, formatted
|
||||
// as described in the comment on the structure definition. the result is `Ok`
|
||||
// if the specification is properly formatted, and `Error` if not
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue