Compare commits

..

9 commits

Author SHA1 Message Date
360ce12d8b feat: Curvature regulators ()
Prior to this commit, there's only one kind of regulator: the one that regulates the inversive distance between two spheres (or, more generally, the Lorentz product between two element representation vectors). Adds a new kind of regulator, which regulates the curvature of a sphere (issue ). In the process, introduces a general framework based on new traits for organizing and sharing code between different kinds of regulators.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-04-21 23:40:42 +00:00
23ba5acad7 Add a top-level run command to the "play with prototype" in README ()
It's convenient to stay in the top-level directory of a project. This change to the README explains how to run the prototype from the top level.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Co-authored-by: glen <glen@studioinfinity.org>
Co-committed-by: glen <glen@studioinfinity.org>
2025-04-18 04:34:30 +00:00
b86f176151 feat: Continuous integration via Forgejo Actions/runners ()
Adds a continuous integration workflow to the repository, using the [Forgejo Actions](https://forgejo.org/docs/next/user/actions/) framework.

Concurrently, Aaron added a [wiki page](https://code.studioinfinity.org/glen/dyna3/wiki/Continuous-integration) to document the continuous integration system. In particular, this page explains how to [run continuous integration checks on a development machine](wiki/Continuous-integration#execution), either directly or in a container.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-04-02 20:31:42 +00:00
2c4fd39c1f refactor: Tidy up engine tests ()
### `zero_loss_test`
  - Drop the redundant type hint in the definition of `a`.

  ### `tangent_test_three_spheres`
  - Get the dimension from the expected basis, rather than putting it in by hand.

  ### `tangent_test_kaleidocycle`
  - Factor out the realization code, in the same style as `realize_irisawa_hexlet`.
  - Rename the `irisawa` submodule to `examples`.

  ### `frozen_entry_test`
  - Move up into the section for simpler tests, between `zero_loss_test` and `irisawa_hexlet_test`.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Reviewed-by: Glen Whitney <glen@nobody@nowhere.net>
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-03-12 21:54:56 +00:00
da28bc99d2 Generalize constraints to observables ()
Unifies the interface elements for measuring and constraining real-valued observables, as proposed in issue . The resulting combination is called a "Regulator," at least in the code. They are presented as text inputs in the table view. When a Regulatore is in measurement mode (has no "set point"), the text field displays its value. Entering a desired value into the text field creates a set point, and then the Regulator acts to (attempt to) constrain the value. Setting the desired value to the empty string switches the observable back to measurement mode. If you enter a desired value that can't be parsed as a floating point number, the regulator input is flagged as invalid and it has no effect on the state of the regulator. The set point can in this case be restored to its previous value (or to no set point if that was its prior state) by pressing the "Esc" key.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Co-authored-by: glen <glen@studioinfinity.org>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-03-10 23:43:24 +00:00
46324fecc6 Use workaround to keep representation coordinates in order ()
This fixes  by rendering representation vectors with a static list view rather than an `Indexed` view. The Sycamore maintainer has confirmed that `Indexed` is always supposed to display list items in order, so I think  is likely caused by a bug in `Indexed`. We should consider reverting this pull request when the bug is fixed.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-02-08 06:08:36 +00:00
25017176fd Adjust normalization step of nudge routine ()
The brach to be merged partially addresses issue  by changing the way we normalize element representations after stepping them in a straight line through configuration space during a nudge. On the main branch, we rescale the whole representation vector. On the branch to be merged, we instead contract the representation vector toward the last coordinate axis by rescaling the spatial and curvature components.

### Improvement in leakage

This change reduces the directional leakage described in . For a quantitative comparison, I used the [reproduction prodcedure](issues/42#user-content-leakage) from that issue, holding **W** until the second coordinate of Deimos had increased by 4 units (from 0.6 to 4.6). During this motion, the third coordinate changed by 0.158 units on the main branch, but only 0.007 units on the branch to be merged. In other words, this pull request decreased drift by roughly a factor of 20.

### Neutral changes in oscillation and jitter

This change makes oscillation and jitter happen differently during the reproduction procedures from , but I wouldn't describe them as being better or worse.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-02-06 22:53:41 +00:00
817a446fad Switch to Euclidean-invariant projection onto tangent space of solution variety ()
This pull request addresses issues  and  by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.

### Definition of the uniform inner product

For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.

The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.

#### For spheres and planes

If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.

The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.

#### For points

If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.

The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.

### Confirmation of intended behavior

Two new tests confirm that we've corrected the misbehaviors described in issues  and .

Issue | Test
---|---
 | `proj_equivar_test`
 | `tangent_test_kaleidocycle`

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
22870342f3 Manipulate the assembly ()
feat: Find tangent space of solution variety, use for perturbations

### Tangent space

#### Implementation

The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.

At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.

After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:

1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.

The comments in `assembly.rs` state the invariants and describe how they're enforced.

#### Automated testing

The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.

#### Limitations

The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.

### Deformation

#### Implementation

The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.

For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.

The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
   * This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety

#### Manual testing

To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
   * **A**/**D** for $x$ translation
   * **W**/**S** for $y$ translation
   * **shift**+**W**/**S** for $z$ translation

#### Limitations

Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.

Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.

When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.

During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: 
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
20 changed files with 2090 additions and 435 deletions

View file

@ -0,0 +1,22 @@
# set up the Trunk web build system
#
# https://trunkrs.dev
#
# the `curl` call is based on David Tolnay's `rust-toolchain` action
#
# https://github.com/dtolnay/rust-toolchain
#
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
# available by placing the fully qualified path to `ci-bin` on the
# workflow's search path
- run: mkdir -p ci-bin
- run: curl --output - --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail 'https://github.com/trunk-rs/trunk/releases/download/v0.21.12/trunk-x86_64-unknown-linux-gnu.tar.gz' | tar --gunzip --extract --file -
working-directory: ci-bin
- run: echo "${{ github.workspace }}/ci-bin" >> $GITHUB_PATH

View file

@ -0,0 +1,29 @@
on:
pull_request:
push:
branches: [main]
jobs:
# run the automated tests, reporting success if the tests pass and were built
# without warnings. the examples are run as tests, because we've configured
# each example target with `test = true` and `harness = false` in Cargo.toml.
# Trunk build failures caused by problems outside the Rust source code, like
# missing assets, should be caught by `trunk_build_test`
test:
runs-on: docker
container:
image: cimg/rust:1.85-node
defaults:
run:
# set the default working directory for each `run` step, relative to the
# workspace directory. this default only affects `run` steps (and if we
# tried to set the `working-directory` label for any other kind of step,
# it wouldn't be recognized anyway)
working-directory: app-proto
steps:
# Check out the repository so that its top-level directory is the
# 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

8
.gitignore vendored
View file

@ -1,8 +1,2 @@
node_modules
site
docbuild
__tests__
coverage
dyna3.zip
tmpproj
ci-bin
*~

View file

@ -37,10 +37,10 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
### Play with the prototype
1. Go into the `app-proto` folder
2. Call `trunk serve --release` to build and serve the prototype
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype
* *The crates the prototype depends on will be downloaded and served automatically*
* *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

788
app-proto/Cargo.lock generated Normal file
View file

@ -0,0 +1,788 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "dyna3"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"dyna3",
"itertools",
"js-sys",
"lazy_static",
"nalgebra",
"readonly",
"rustc-hash",
"slab",
"sycamore",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "html-escape"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
name = "indexmap"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "matrixmultiply"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "minicov"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169"
dependencies = [
"cc",
"walkdir",
]
[[package]]
name = "nalgebra"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4"
dependencies = [
"approx",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "readonly"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rustc-hash"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "safe_arch"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simba"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "sycamore"
version = "0.9.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd"
dependencies = [
"hashbrown",
"indexmap",
"paste",
"sycamore-core",
"sycamore-macro",
"sycamore-reactive",
"sycamore-web",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "sycamore-core"
version = "0.9.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a"
dependencies = [
"hashbrown",
"paste",
"sycamore-reactive",
]
[[package]]
name = "sycamore-macro"
version = "0.9.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"rand",
"sycamore-view-parser",
"syn",
]
[[package]]
name = "sycamore-reactive"
version = "0.9.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1"
dependencies = [
"paste",
"slotmap",
"smallvec",
]
[[package]]
name = "sycamore-view-parser"
version = "0.9.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sycamore-web"
version = "0.9.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475"
dependencies = [
"html-escape",
"js-sys",
"once_cell",
"paste",
"smallvec",
"sycamore-core",
"sycamore-macro",
"sycamore-reactive",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9"
dependencies = [
"console_error_panic_hook",
"js-sys",
"minicov",
"scoped-tls",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
]
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "web-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wide"
version = "0.7.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -13,6 +13,7 @@ itertools = "0.13.0"
js-sys = "0.3.70"
lazy_static = "1.5.0"
nalgebra = "0.33.0"
readonly = "0.2.12"
rustc-hash = "2.0.0"
slab = "0.4.9"
sycamore = "0.9.0-beta.3"
@ -49,3 +50,23 @@ wasm-bindgen-test = "0.3.34"
[profile.release]
opt-level = "s" # optimize for small code size
debug = true # include debug symbols
[[example]]
name = "irisawa-hexlet"
test = true
harness = false
[[example]]
name = "kaleidocycle"
test = true
harness = false
[[example]]
name = "point-on-sphere"
test = true
harness = false
[[example]]
name = "three-spheres"
test = true
harness = false

View file

@ -1,4 +1,4 @@
use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet};
use dyna3::engine::{Q, examples::realize_irisawa_hexlet};
fn main() {
const SCALED_TOL: f64 = 1.0e-12;

View file

@ -0,0 +1,30 @@
use nalgebra::{DMatrix, DVector};
use dyna3::engine::{Q, examples::realize_kaleidocycle};
fn main() {
const SCALED_TOL: f64 = 1.0e-12;
let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config);
print!("Configuration:{}", config);
if success {
println!("Target accuracy achieved!");
} else {
println!("Failed to reach target accuracy");
}
println!("Steps: {}", history.scaled_loss.len() - 1);
println!("Loss: {}\n", history.scaled_loss.last().unwrap());
// find the kaleidocycle's twist motion by projecting onto the tangent space
const N_POINTS: usize = 12;
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
let down = -&up;
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|n| [
tangent.proj(&up.as_view(), n),
tangent.proj(&down.as_view(), n+1)
]
).sum();
let normalization = 5.0 / twist_motion[(2, 0)];
print!("Twist motion:{}", normalization * twist_motion);
}

View file

@ -1,26 +1,19 @@
use nalgebra::DMatrix;
use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix};
use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem};
fn main() {
let gram = {
let mut gram_to_be = PartialMatrix::new();
for j in 0..2 {
for k in j..2 {
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
gram_to_be
};
let guess = DMatrix::from_columns(&[
let mut problem = ConstraintProblem::from_guess(&[
point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 1.0)
]);
let frozen = [(3, 0)];
for j in 0..2 {
for k in j..2 {
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
println!();
let (config, _, success, history) = realize_gram(
&gram, guess, &frozen,
1.0e-12, 0.5, 0.9, 1.1, 200, 110
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
print!("Configuration:{}", config);

View file

@ -1,29 +1,22 @@
use nalgebra::DMatrix;
use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix};
use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem};
fn main() {
let gram = {
let mut gram_to_be = PartialMatrix::new();
for j in 0..3 {
for k in j..3 {
gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
gram_to_be
};
let guess = {
let mut problem = ConstraintProblem::from_guess({
let a: f64 = 0.75_f64.sqrt();
DMatrix::from_columns(&[
&[
sphere(1.0, 0.0, 0.0, 1.0),
sphere(-0.5, a, 0.0, 1.0),
sphere(-0.5, -a, 0.0, 1.0)
])
};
]
});
for j in 0..3 {
for k in j..3 {
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
println!();
let (config, _, success, history) = realize_gram(
&gram, guess, &[],
1.0e-12, 0.5, 0.9, 1.1, 200, 110
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
if success {

View file

@ -3,7 +3,8 @@
--text-bright: white;
--text-invalid: #f58fc2; /* bright pink */
--border: #555; /* light gray */
--border-focus: #aaa; /* bright gray */
--border-focus-dark: #aaa; /* bright gray */
--border-focus-light: white;
--border-invalid: #70495c; /* dusky pink */
--selection-highlight: #444; /* medium gray */
--page-background: #222; /* dark gray */
@ -23,7 +24,7 @@ body {
display: flex;
flex-direction: column;
float: left;
width: 450px;
width: 500px;
height: 100vh;
margin: 0px;
padding: 0px;
@ -77,12 +78,12 @@ summary.selected {
background-color: var(--selection-highlight);
}
summary > div, .constraint {
summary > div, .regulator {
padding-top: 4px;
padding-bottom: 4px;
}
.element, .constraint {
.element, .regulator {
display: flex;
flex-grow: 1;
padding-left: 8px;
@ -107,7 +108,7 @@ details[open]:has(li) .element-switch::after {
flex-grow: 1;
}
.constraint-label {
.regulator-label {
flex-grow: 1;
}
@ -123,26 +124,34 @@ details[open]:has(li) .element-switch::after {
width: 56px;
}
.constraint {
.regulator {
font-style: italic;
}
.constraint.invalid {
color: var(--text-invalid);
.regulator-type {
padding: 2px 8px 0px 8px;
font-size: 10pt;
}
.constraint > input[type=checkbox] {
margin: 0px 8px 0px 0px;
}
.constraint > input[type=text] {
.regulator-input {
color: inherit;
background-color: inherit;
border: 1px solid var(--border);
border-radius: 2px;
}
.constraint.invalid > input[type=text] {
.regulator-input::placeholder {
color: inherit;
opacity: 54%;
font-style: italic;
}
.regulator-input.constraint {
background-color: var(--display-background);
}
.regulator-input.invalid {
color: var(--text-invalid);
border-color: var(--border-invalid);
}
@ -154,7 +163,7 @@ details[open]:has(li) .element-switch::after {
font-style: normal;
}
.invalid > .status::after, details:has(.invalid):not([open]) .status::after {
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
content: '⚠';
color: var(--text-invalid);
}
@ -171,5 +180,11 @@ canvas {
}
canvas:focus {
border-color: var(--border-focus);
border-color: var(--border-focus-dark);
outline: none;
}
input:focus {
border-color: var(--border-focus-light);
outline: none;
}

View file

@ -9,3 +9,4 @@
cargo run --example irisawa-hexlet
cargo run --example three-spheres
cargo run --example point-on-sphere
cargo run --example kaleidocycle

View file

@ -1,13 +1,17 @@
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
use crate::{
engine,
AppState,
assembly::{Assembly, Element, InversiveDistanceRegulator}
};
/* DEBUG */
// load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system
fn load_gen_assemb(assembly: &Assembly) {
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("gemini_a"),
String::from("Castor"),
@ -15,7 +19,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(0.5, 0.5, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("gemini_b"),
String::from("Pollux"),
@ -23,7 +27,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(-0.5, -0.5, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("ursa_major"),
String::from("Ursa major"),
@ -31,7 +35,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(-0.5, 0.5, 0.0, 0.75)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("ursa_minor"),
String::from("Ursa minor"),
@ -39,7 +43,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(0.5, -0.5, 0.0, 0.5)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("moon_deimos"),
String::from("Deimos"),
@ -47,7 +51,7 @@ fn load_gen_assemb(assembly: &Assembly) {
engine::sphere(0.0, 0.15, 1.0, 0.25)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("moon_phobos"),
String::from("Phobos"),
@ -62,7 +66,7 @@ fn load_gen_assemb(assembly: &Assembly) {
// built a more formal test assembly system
fn load_low_curv_assemb(assembly: &Assembly) {
let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"central".to_string(),
"Central".to_string(),
@ -70,7 +74,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere(0.0, 0.0, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"assemb_plane".to_string(),
"Assembly plane".to_string(),
@ -78,7 +82,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"side1".to_string(),
"Side 1".to_string(),
@ -86,7 +90,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"side2".to_string(),
"Side 2".to_string(),
@ -94,7 +98,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"side3".to_string(),
"Side 3".to_string(),
@ -102,7 +106,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"corner1".to_string(),
"Corner 1".to_string(),
@ -110,7 +114,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
"corner2".to_string(),
"Corner 2".to_string(),
@ -118,7 +122,7 @@ fn load_low_curv_assemb(assembly: &Assembly) {
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
)
);
let _ = assembly.try_insert_element(
let _ = assembly.try_insert_sphere(
Element::new(
String::from("corner3"),
String::from("Corner 3"),
@ -144,6 +148,7 @@ pub fn AddRemove() -> View {
let assembly = &state.assembly;
// clear state
assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear());
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
state.selection.update(|sel| sel.clear());
@ -162,18 +167,7 @@ pub fn AddRemove() -> View {
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_new_element();
/* DEBUG */
// print updated list of elements by identifier
console::log_1(&JsValue::from("elements by identifier:"));
for (id, key) in state.assembly.elements_by_id.get_clone().iter() {
console::log_3(
&JsValue::from(" "),
&JsValue::from(id),
&JsValue::from(*key)
);
}
state.assembly.insert_new_sphere();
}
) { "+" }
button(
@ -184,50 +178,21 @@ pub fn AddRemove() -> View {
},
on:click=|_| {
let state = use_context::<AppState>();
let subjects = state.selection.with(
|sel| {
let subject_vec: Vec<_> = sel.into_iter().collect();
(subject_vec[0].clone(), subject_vec[1].clone())
}
let subjects: [_; 2] = state.selection.with(
// the button is only enabled when two elements are
// selected, so we know the cast to a two-element array
// will succeed
|sel| sel
.clone()
.into_iter()
.collect::<Vec<_>>()
.try_into()
.unwrap()
);
state.assembly.insert_regulator(
InversiveDistanceRegulator::new(subjects, &state.assembly)
);
let lorentz_prod = create_signal(0.0);
let lorentz_prod_valid = create_signal(false);
let active = create_signal(true);
state.assembly.insert_constraint(Constraint {
subjects: subjects,
lorentz_prod: lorentz_prod,
lorentz_prod_text: create_signal(String::new()),
lorentz_prod_valid: lorentz_prod_valid,
active: active,
});
state.selection.update(|sel| sel.clear());
/* DEBUG */
// print updated constraint list
console::log_1(&JsValue::from("Constraints:"));
state.assembly.constraints.with(|csts| {
for (_, cst) in csts.into_iter() {
console::log_5(
&JsValue::from(" "),
&JsValue::from(cst.subjects.0),
&JsValue::from(cst.subjects.1),
&JsValue::from(":"),
&JsValue::from(cst.lorentz_prod.get_untracked())
);
}
});
// update the realization when the constraint becomes active
// and valid, or is edited while active and valid
create_effect(move || {
console::log_1(&JsValue::from(
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
));
lorentz_prod.track();
if active.get() && lorentz_prod_valid.get() {
state.assembly.realize();
}
});
}
) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser

View file

@ -1,15 +1,27 @@
use nalgebra::{DMatrix, DVector, DVectorView, Vector3};
use rustc_hash::FxHashMap;
use slab::Slab;
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::engine::{realize_gram, ConfigSubspace, PartialMatrix, Q};
use crate::{
engine::{
Q,
change_half_curvature,
local_unif_to_std,
realize_gram,
sphere,
ConfigSubspace,
ConstraintProblem
},
outline::OutlineItem,
specified::SpecifiedValue
};
// the types of the keys we use to access an assembly's elements and constraints
// the types of the keys we use to access an assembly's elements and regulators
pub type ElementKey = usize;
pub type ConstraintKey = usize;
pub type RegulatorKey = usize;
pub type ElementColor = [f32; 3];
@ -20,13 +32,20 @@ pub type ElementColor = [f32; 3];
// each assembly has a key that identifies it within the sesssion
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
pub trait ProblemPoser {
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>);
}
#[derive(Clone, PartialEq)]
pub struct Element {
pub id: String,
pub label: String,
pub color: ElementColor,
pub representation: Signal<DVector<f64>>,
pub constraints: Signal<BTreeSet<ConstraintKey>>,
// the regulators this element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date
pub regulators: Signal<BTreeSet<RegulatorKey>>,
// a serial number, assigned by `Element::new`, that uniquely identifies
// each element
@ -39,6 +58,8 @@ pub struct Element {
}
impl Element {
const CURVATURE_COMPONENT: usize = 3;
pub fn new(
id: String,
label: String,
@ -61,7 +82,7 @@ impl Element {
label: label,
color: color,
representation: create_signal(representation),
constraints: create_signal(BTreeSet::default()),
regulators: create_signal(BTreeSet::default()),
serial: serial,
column_index: None
}
@ -111,15 +132,149 @@ impl Element {
}
}
#[derive(Clone)]
pub struct Constraint {
pub subjects: (ElementKey, ElementKey),
pub lorentz_prod: Signal<f64>,
pub lorentz_prod_text: Signal<String>,
pub lorentz_prod_valid: Signal<bool>,
pub active: Signal<bool>
impl ProblemPoser for Element {
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Element>) {
let index = self.column_index.expect(
format!("Element \"{}\" 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());
}
}
pub trait Regulator: ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<ElementKey>;
fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>;
// this method is used to responsively precondition the assembly for
// realization when the regulator becomes a constraint, or is edited while
// acting as a constraint. it should track the set point, do any desired
// preconditioning when the set point is present, and use its return value
// to report whether the set is present. the default implementation does no
// preconditioning
fn try_activate(&self, _assembly: &Assembly) -> bool {
self.set_point().with(|set_pt| set_pt.is_present())
}
}
pub struct InversiveDistanceRegulator {
pub subjects: [ElementKey; 2],
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>
}
impl InversiveDistanceRegulator {
pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator {
let measurement = assembly.elements.map(
move |elts| {
let representations = subjects.map(|subj| elts[subj].representation);
representations[0].with(|rep_0|
representations[1].with(|rep_1|
rep_0.dot(&(&*Q * rep_1))
)
)
}
);
let set_point = create_signal(SpecifiedValue::from_empty_spec());
InversiveDistanceRegulator { subjects, measurement, set_point }
}
}
impl Regulator for InversiveDistanceRegulator {
fn subjects(&self) -> Vec<ElementKey> {
self.subjects.into()
}
fn measurement(&self) -> ReadSignal<f64> {
self.measurement
}
fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point
}
}
impl ProblemPoser for InversiveDistanceRegulator {
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let [row, col] = self.subjects.map(
|subj| elts[subj].column_index.expect(
"Subjects should be indexed before inversive distance regulator writes problem data"
)
);
problem.gram.push_sym(row, col, val);
}
});
}
}
pub struct HalfCurvatureRegulator {
pub subject: ElementKey,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>
}
impl HalfCurvatureRegulator {
pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator {
let measurement = assembly.elements.map(
move |elts| elts[subject].representation.with(
|rep| rep[Element::CURVATURE_COMPONENT]
)
);
let set_point = create_signal(SpecifiedValue::from_empty_spec());
HalfCurvatureRegulator { subject, measurement, set_point }
}
}
impl Regulator for HalfCurvatureRegulator {
fn subjects(&self) -> Vec<ElementKey> {
vec![self.subject]
}
fn measurement(&self) -> ReadSignal<f64> {
self.measurement
}
fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point
}
fn try_activate(&self, assembly: &Assembly) -> bool {
match self.set_point.with(|set_pt| set_pt.value) {
Some(half_curv) => {
let representation = assembly.elements.with_untracked(
|elts| elts[self.subject].representation
);
representation.update(
|rep| change_half_curvature(rep, half_curv)
);
true
}
None => false
}
}
}
impl ProblemPoser for HalfCurvatureRegulator {
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let col = elts[self.subject].column_index.expect(
"Subject should be indexed before half-curvature regulator writes problem data"
);
problem.frozen.push(Element::CURVATURE_COMPONENT, col, val);
}
});
}
}
// the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> {
pub key: ElementKey,
pub velocity: DVectorView<'a, f64>
@ -130,9 +285,9 @@ type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
// a complete, view-independent description of an assembly
#[derive(Clone)]
pub struct Assembly {
// elements and constraints
// elements and regulators
pub elements: Signal<Slab<Element>>,
pub constraints: Signal<Slab<Constraint>>,
pub regulators: Signal<Slab<Rc<dyn Regulator>>>,
// solution variety tangent space. the basis vectors are stored in
// configuration matrix format, ordered according to the elements' column
@ -154,34 +309,41 @@ impl Assembly {
pub fn new() -> Assembly {
Assembly {
elements: create_signal(Slab::new()),
constraints: create_signal(Slab::new()),
regulators: create_signal(Slab::new()),
tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(FxHashMap::default())
}
}
// --- inserting elements and constraints ---
// --- inserting elements and regulators ---
// insert an element into the assembly without checking whether we already
// insert a sphere into the assembly without checking whether we already
// have an element with the same identifier. any element that does have the
// same identifier will get kicked out of the `elements_by_id` index
fn insert_element_unchecked(&self, elt: Element) {
fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey {
// insert the sphere
let id = elt.id.clone();
let key = self.elements.update(|elts| elts.insert(elt));
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key));
// regulate the sphere's curvature
self.insert_regulator(HalfCurvatureRegulator::new(key, &self));
key
}
pub fn try_insert_element(&self, elt: Element) -> bool {
pub fn try_insert_sphere(&self, elt: Element) -> Option<ElementKey> {
let can_insert = self.elements_by_id.with_untracked(
|elts_by_id| !elts_by_id.contains_key(&elt.id)
);
if can_insert {
self.insert_element_unchecked(elt);
Some(self.insert_sphere_unchecked(elt))
} else {
None
}
can_insert
}
pub fn insert_new_element(&self) {
pub fn insert_new_sphere(&self) {
// find the next unused identifier in the default sequence
let mut id_num = 1;
let mut id = format!("sphere{}", id_num);
@ -192,25 +354,71 @@ impl Assembly {
id = format!("sphere{}", id_num);
}
// create and insert a new element
self.insert_element_unchecked(
// create and insert a sphere
let _ = self.insert_sphere_unchecked(
Element::new(
id,
format!("Sphere {}", id_num),
[0.75_f32, 0.75_f32, 0.75_f32],
DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5])
sphere(0.0, 0.0, 0.0, 1.0)
)
);
}
pub fn insert_constraint(&self, constraint: Constraint) {
let subjects = constraint.subjects;
let key = self.constraints.update(|csts| csts.insert(constraint));
let subject_constraints = self.elements.with(
|elts| (elts[subjects.0].constraints, elts[subjects.1].constraints)
pub fn insert_regulator<T: Regulator + 'static>(&self, regulator: T) {
// add the regulator to the assembly's regulator list
let regulator_rc = Rc::new(regulator);
let key = self.regulators.update(
|regs| regs.insert(regulator_rc.clone())
);
subject_constraints.0.update(|csts| csts.insert(key));
subject_constraints.1.update(|csts| csts.insert(key));
// add the regulator to each subject's regulator list
let subjects = regulator_rc.subjects();
let subject_regulators: Vec<_> = self.elements.with_untracked(
|elts| subjects.into_iter().map(
|subj| elts[subj].regulators
).collect()
);
for regulators in subject_regulators {
regulators.update(|regs| regs.insert(key));
}
// update the realization when the regulator becomes a constraint, or is
// edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || {
/* DEBUG */
// log the regulator update
console::log_1(&JsValue::from(
format!("Updated regulator with subjects {:?}", regulator_rc.subjects())
));
if regulator_rc.try_activate(&self_for_effect) {
self_for_effect.realize();
}
});
/* DEBUG */
// print an updated list of regulators
console::log_1(&JsValue::from("Regulators:"));
self.regulators.with_untracked(|regs| {
for (_, reg) in regs.into_iter() {
console::log_1(&JsValue::from(format!(
" {:?}: {}",
reg.subjects(),
reg.set_point().with_untracked(
|set_pt| {
let spec = &set_pt.spec;
if spec.is_empty() {
"__".to_string()
} else {
spec.clone()
}
}
)
)));
}
});
}
// --- realization ---
@ -223,53 +431,39 @@ impl Assembly {
}
});
// set up the Gram matrix and the initial configuration matrix
let (gram, guess) = self.elements.with_untracked(|elts| {
// set up the off-diagonal part of the Gram matrix
let mut gram_to_be = PartialMatrix::new();
self.constraints.with_untracked(|csts| {
for (_, cst) in csts {
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() {
let subjects = cst.subjects;
let row = elts[subjects.0].column_index.unwrap();
let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked());
}
// set up the constraint problem
let problem = self.elements.with_untracked(|elts| {
let mut problem = ConstraintProblem::new(elts.len());
for (_, elt) in elts {
elt.pose(&mut problem, elts);
}
self.regulators.with_untracked(|regs| {
for (_, reg) in regs {
reg.pose(&mut problem, elts);
}
});
// set up the initial configuration matrix and the diagonal of the
// Gram matrix
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
for (_, elt) in elts {
let index = elt.column_index.unwrap();
gram_to_be.push_sym(index, index, 1.0);
guess_to_be.set_column(index, &elt.representation.get_clone_untracked());
}
(gram_to_be, guess_to_be)
problem
});
/* DEBUG */
// log the Gram matrix
console::log_1(&JsValue::from("Gram matrix:"));
gram.log_to_console();
problem.gram.log_to_console();
/* DEBUG */
// log the initial configuration matrix
console::log_1(&JsValue::from("Old configuration:"));
for j in 0..guess.nrows() {
for j in 0..problem.guess.nrows() {
let mut row_str = String::new();
for k in 0..guess.ncols() {
row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str());
for k in 0..problem.guess.ncols() {
row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str());
}
console::log_1(&JsValue::from(row_str));
}
// look for a configuration with the given Gram matrix
let (config, tangent, success, history) = realize_gram(
&gram, guess, &[],
1.0e-12, 0.5, 0.9, 1.1, 200, 110
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
/* DEBUG */
@ -359,12 +553,19 @@ impl Assembly {
// this element didn't have a column index when we started, so
// by invariant (2), it's unconstrained
let mut target_column = motion_proj.column_mut(column_index);
target_column += elt_motion.velocity;
let unif_to_std = self.elements.with_untracked(
|elts| {
elts[elt_motion.key].representation.with_untracked(
|rep| local_unif_to_std(rep.as_view())
)
}
);
target_column += unif_to_std * elt_motion.velocity;
}
}
// step each element along the mass shell geodesic that matches its
// velocity in the deformation found above
// step the assembly along the deformation. this changes the elements'
// normalizations, so we restore those afterward
/* KLUDGE */
// since our test assemblies only include spheres, we assume that every
// element is on the 1 mass shell
@ -372,9 +573,16 @@ impl Assembly {
elt.representation.update_silent(|rep| {
match elt.column_index {
Some(column_index) => {
let rep_next = &*rep + motion_proj.column(column_index);
let normalizer = rep_next.dot(&(&*Q * &rep_next));
rep.set_column(0, &(rep_next / normalizer));
// step the assembly along the deformation
*rep += motion_proj.column(column_index);
// restore normalization by contracting toward the last
// coordinate axis
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
let half_q_lt = -2.0 * rep[3] * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
},
None => {
console::log_1(&JsValue::from(
@ -391,3 +599,47 @@ impl Assembly {
self.realize();
}
}
#[cfg(test)]
mod tests {
use crate::engine;
use super::*;
#[test]
#[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")]
fn unindexed_element_test() {
let _ = create_root(|| {
Element::new(
"sphere".to_string(),
"Sphere".to_string(),
[1.0_f32, 1.0_f32, 1.0_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0)
).pose(&mut ConstraintProblem::new(1), &Slab::new());
});
}
#[test]
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| {
let mut elts = Slab::new();
let subjects = [0, 1].map(|k| {
elts.insert(
Element::new(
format!("sphere{k}"),
format!("Sphere {k}"),
[1.0_f32, 1.0_f32, 1.0_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0)
)
)
});
elts[subjects[0]].column_index = Some(0);
InversiveDistanceRegulator {
subjects: subjects,
measurement: create_memo(|| 0.0),
set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap())
}.pose(&mut ConstraintProblem::new(2), &elts);
});
}
}

View file

@ -130,6 +130,8 @@ pub fn Display() -> View {
let translate_pos_y = create_signal(0.0);
let translate_neg_z = create_signal(0.0);
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);
@ -164,6 +166,7 @@ pub fn Display() -> View {
// 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 OPACITY: f32 = 0.5; /* SCAFFOLDING */
@ -292,6 +295,8 @@ pub fn Display() -> View {
let translate_pos_y_val = translate_pos_y.get();
let translate_neg_z_val = translate_neg_z.get();
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 = {
@ -323,24 +328,27 @@ pub fn Display() -> View {
let sel = state.selection.with(
|sel| *sel.into_iter().next().unwrap()
);
let rep = state.assembly.elements.with_untracked(
|elts| elts[sel].representation.get_clone_untracked()
);
let translate_x = translate_pos_x_val - translate_neg_x_val;
let translate_y = translate_pos_y_val - translate_neg_y_val;
let translate_z = translate_pos_z_val - translate_neg_z_val;
if translate_x != 0.0 || translate_y != 0.0 || translate_z != 0.0 {
let vel_field = {
let u = Vector3::new(translate_x, translate_y, translate_z).normalize();
DMatrix::from_column_slice(5, 5, &[
0.0, 0.0, 0.0, 0.0, u[0],
0.0, 0.0, 0.0, 0.0, u[1],
0.0, 0.0, 0.0, 0.0, u[2],
2.0*u[0], 2.0*u[1], 2.0*u[2], 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0
])
let shrink = shrink_pos_val - shrink_neg_val;
let translating =
translate_x != 0.0
|| translate_y != 0.0
|| translate_z != 0.0;
if translating || shrink != 0.0 {
let elt_motion = {
let u = if translating {
TRANSLATION_SPEED * Vector3::new(
translate_x, translate_y, translate_z
).normalize()
} else {
Vector3::zeros()
};
time_step * DVector::from_column_slice(
&[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
)
};
let elt_motion: DVector<f64> = time_step * TRANSLATION_SPEED * vel_field * rep;
assembly_for_raf.deform(
vec![
ElementMotion {
@ -501,6 +509,8 @@ pub fn Display() -> View {
"s" | "S" if shift => translate_pos_z.set(value),
"w" | "W" => translate_pos_y.set(value),
"s" | "S" => translate_neg_y.set(value),
"]" | "}" => shrink_neg.set(value),
"[" | "{" => shrink_pos.set(value),
_ => manipulating = false
};
if manipulating {

View file

@ -35,9 +35,43 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6
])
}
// given a sphere's representation vector, change the sphere's half-curvature to
// `half-curv` and then restore normalization by contracting the representation
// vector toward the curvature axis
pub fn change_half_curvature(rep: &mut DVector<f64>, half_curv: f64) {
// set the sphere's half-curvature to the desired value
rep[3] = half_curv;
// restore normalization by contracting toward the curvature axis
const SIZE_THRESHOLD: f64 = 1e-9;
let half_q_lt = -2.0 * half_curv * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let mut spatial = rep.fixed_rows_mut::<3>(0);
let q_sp = spatial.norm_squared();
if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD {
spatial.copy_from_slice(
&[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()]
);
} else {
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
spatial.scale_mut(1.0 / scaling);
rep[4] /= scaling;
}
/* DEBUG */
// verify normalization
let rep_for_debug = rep.clone();
console::log_1(&JsValue::from(
format!(
"Sphere self-product after curvature change: {}",
rep_for_debug.dot(&(&*Q * &rep_for_debug))
)
));
}
// --- partial matrices ---
struct MatrixEntry {
pub struct MatrixEntry {
index: (usize, usize),
value: f64
}
@ -49,73 +83,105 @@ impl PartialMatrix {
PartialMatrix(Vec::<MatrixEntry>::new())
}
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
pub fn push(&mut self, row: usize, col: usize, value: f64) {
let PartialMatrix(entries) = self;
entries.push(MatrixEntry { index: (row, col), value: value });
}
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
self.push(row, col, value);
if row != col {
entries.push(MatrixEntry { index: (col, row), value: value });
self.push(col, row, value);
}
}
/* DEBUG */
pub fn log_to_console(&self) {
let PartialMatrix(entries) = self;
for ent in entries {
let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value);
console::log_1(&JsValue::from(ent_str.as_str()));
for &MatrixEntry { index: (row, col), value } in self {
console::log_1(&JsValue::from(
format!(" {} {} {}", row, col, value)
));
}
}
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = a.clone();
for &MatrixEntry { index, value } in self {
result[index] = value;
}
result
}
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
let PartialMatrix(entries) = self;
for ent in entries {
result[ent.index] = a[ent.index];
for &MatrixEntry { index, .. } in self {
result[index] = a[index];
}
result
}
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
let PartialMatrix(entries) = self;
for ent in entries {
result[ent.index] = ent.value - rhs[ent.index];
for &MatrixEntry { index, value } in self {
result[index] = value - rhs[index];
}
result
}
}
impl IntoIterator for PartialMatrix {
type Item = MatrixEntry;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let PartialMatrix(entries) = self;
entries.into_iter()
}
}
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()
}
}
// --- configuration subspaces ---
#[derive(Clone)]
pub struct ConfigSubspace {
assembly_dim: usize,
basis: Vec<DMatrix<f64>>
basis_std: Vec<DMatrix<f64>>,
basis_proj: Vec<DMatrix<f64>>
}
impl ConfigSubspace {
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
ConfigSubspace {
assembly_dim: assembly_dim,
basis: Vec::new()
basis_proj: Vec::new(),
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`
fn symmetric_kernel(a: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
const ELEMENT_DIM: usize = 5;
const THRESHOLD: f64 = 1.0e-4;
let eig = SymmetricEigen::new(a);
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
// find a basis for the kernel. the basis is expressed in the projection
// coordinates, and it's orthonormal with respect to the projection
// inner product
const THRESHOLD: f64 = 0.1;
let eig = SymmetricEigen::new(proj_to_std.tr_mul(&a) * &proj_to_std);
let eig_vecs = eig.eigenvectors.column_iter();
let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs);
let basis = eig_pairs.filter_map(
|(λ, v)| (λ.abs() < THRESHOLD).then_some(
Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
)
)
let basis_proj = DMatrix::from_columns(
eig_pairs.filter_map(
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
).collect::<Vec<_>>().as_slice()
);
/* DEBUG */
@ -125,30 +191,45 @@ impl ConfigSubspace {
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
));
// 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;
ConfigSubspace {
assembly_dim: assembly_dim,
basis: basis.collect()
basis_std: basis_std.column_iter().map(
|v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
)
).collect(),
basis_proj: basis_proj.column_iter().map(
|v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
)
).collect()
}
}
pub fn dim(&self) -> usize {
self.basis.len()
self.basis_std.len()
}
pub fn assembly_dim(&self) -> usize {
self.assembly_dim
}
// find the projection onto this subspace, with respect to the Euclidean
// inner product, of the motion where the element with the given column
// index has velocity `v`
// 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
// projection inner product
pub fn proj(&self, v: &DVectorView<f64>, column_index: usize) -> DMatrix<f64> {
if self.dim() == 0 {
const ELEMENT_DIM: usize = 5;
DMatrix::zeros(ELEMENT_DIM, self.assembly_dim)
} else {
self.basis.iter().map(
|b| b.column(column_index).dot(&v) * b
self.basis_proj.iter().zip(self.basis_std.iter()).map(
|(b_proj, b_std)| b_proj.column(column_index).dot(&v) * b_std
).sum()
}
}
@ -178,6 +259,34 @@ impl DescentHistory {
}
}
// --- constraint problems ---
pub struct ConstraintProblem {
pub gram: PartialMatrix,
pub frozen: PartialMatrix,
pub guess: DMatrix<f64>,
}
impl ConstraintProblem {
pub fn new(element_count: usize) -> ConstraintProblem {
const ELEMENT_DIM: usize = 5;
ConstraintProblem {
gram: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count)
}
}
#[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem {
ConstraintProblem {
gram: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns)
}
}
}
// --- gram matrix realization ---
// the Lorentz form
@ -215,6 +324,37 @@ fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix<f6
result
}
// given a normalized vector `v` representing an element, build a basis for the
// element's linear configuration space consisting of:
// - the unit translation motions of the element
// - the unit shrinking motion of the element, if it's a sphere
// - one or two vectors whose coefficients vanish on the tangent space of the
// normalization variety
pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4;
let curv = 2.0*v[3];
if v.dot(&(&*Q * v)) < 0.5 {
// `v` represents a point. the normalization condition says that the
// curvature component of `v` is 1/2
DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[
curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2],
0.0, 0.0, 0.0, 0.0, 1.0
])
} else {
// `v` represents a sphere. the normalization condition says that the
// Lorentz product of `v` with itself is 1
DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[
curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2],
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0
])
}
}
// use backtracking line search to find a better configuration
fn seek_better_config(
gram: &PartialMatrix,
@ -238,12 +378,12 @@ fn seek_better_config(
None
}
// seek a matrix `config` for which `config' * Q * config` matches the partial
// matrix `gram`. use gradient descent starting from `guess`
// seek a matrix `config` that matches the partial matrix `problem.frozen` and
// has `config' * Q * config` matching the partial matrix `problem.gram`. start
// at `problem.guess`, set the frozen entries to their desired values, and then
// use a regularized Newton's method to seek the desired Gram matrix
pub fn realize_gram(
gram: &PartialMatrix,
guess: DMatrix<f64>,
frozen: &[(usize, usize)],
problem: &ConstraintProblem,
scaled_tol: f64,
min_efficiency: f64,
backoff: f64,
@ -251,6 +391,11 @@ pub fn realize_gram(
max_descent_steps: i32,
max_backoff_steps: i32
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
// destructure the problem data
let ConstraintProblem {
gram, guess, frozen
} = problem;
// start the descent history
let mut history = DescentHistory::new();
@ -265,11 +410,11 @@ pub fn realize_gram(
// convert the frozen indices to stacked format
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|index| index.1*element_dim + index.0
|MatrixEntry { index: (row, col), .. }| col*element_dim + row
).collect();
// use Newton's method with backtracking and gradient descent backup
let mut state = SearchState::from_config(gram, guess);
// use a regularized Newton's method with backtracking
let mut state = SearchState::from_config(gram, 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
@ -344,7 +489,19 @@ pub fn realize_gram(
}
let success = state.loss < tol;
let tangent = if success {
ConfigSubspace::symmetric_kernel(hess, assembly_dim)
// express the uniform basis in the standard basis
const UNIFORM_DIM: usize = 4;
let total_dim_unif = UNIFORM_DIM * assembly_dim;
let mut unif_to_std = DMatrix::<f64>::zeros(total_dim, total_dim_unif);
for n in 0..assembly_dim {
let block_start = (element_dim * n, UNIFORM_DIM * n);
unif_to_std
.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
ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim)
} else {
ConfigSubspace::zero(assembly_dim)
};
@ -353,50 +510,22 @@ pub fn realize_gram(
// --- tests ---
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
// below includes a nice translation of the problem statement, which was
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
// Present_)
//
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
// https://www.nippon.com/en/japan-topics/c12801/
//
#[cfg(feature = "dev")]
pub mod irisawa {
use std::{array, f64::consts::PI};
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
// Present_)
//
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
// https://www.nippon.com/en/japan-topics/c12801/
//
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
let gram = {
let mut gram_to_be = PartialMatrix::new();
for s in 0..9 {
// each sphere is represented by a spacelike vector
gram_to_be.push_sym(s, s, 1.0);
// the circumscribing sphere is tangent to all of the other
// spheres, with matching orientation
if s > 0 {
gram_to_be.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 {
gram_to_be.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;
gram_to_be.push_sym(s, s_next, -1.0);
}
}
gram_to_be
};
let guess = DMatrix::from_columns(
let mut problem = ConstraintProblem::from_guess(
[
sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.0),
@ -411,20 +540,109 @@ pub mod irisawa {
).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
let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k));
for k in 0..4 {
problem.frozen.push(3, k, problem.guess[(3, k)]);
}
realize_gram(
&gram, guess, &frozen,
scaled_tol, 0.5, 0.9, 1.1, 200, 110
)
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
}
// set up a kaleidocycle, made of points with fixed distances between them,
// and find its tangent space
pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
const N_HINGES: usize = 6;
let mut problem = ConstraintProblem::from_guess(
(0..N_HINGES).step_by(2).flat_map(
|n| {
let ang_hor = (n as f64) * PI/3.0;
let ang_vert = ((n + 1) as f64) * PI/3.0;
let x_vert = ang_vert.cos();
let y_vert = ang_vert.sin();
[
point(0.0, 0.0, 0.0),
point(ang_hor.cos(), ang_hor.sin(), 0.0),
point(x_vert, y_vert, -0.5),
point(x_vert, y_vert, 0.5)
]
}
).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;
for j in 0..2 {
// diagonal and hinge edges
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)
}
}
#[cfg(test)]
mod tests {
use super::{*, irisawa::realize_irisawa_hexlet};
use nalgebra::Vector3;
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter};
use super::{*, examples::*};
#[test]
fn freeze_test() {
let frozen = PartialMatrix(vec![
MatrixEntry { index: (0, 0), value: 14.0 },
MatrixEntry { index: (0, 2), value: 28.0 },
MatrixEntry { index: (1, 1), value: 42.0 },
MatrixEntry { index: (1, 2), value: 49.0 }
]);
let config = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0,
4.0, 5.0, 6.0
]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
14.0, 2.0, 28.0,
4.0, 42.0, 49.0
]);
assert_eq!(frozen.freeze(&config), expected_result);
}
#[test]
fn sub_proj_test() {
@ -447,20 +665,14 @@ mod tests {
#[test]
fn zero_loss_test() {
let gram = PartialMatrix({
let mut entries = Vec::<MatrixEntry>::new();
for j in 0..3 {
for k in 0..3 {
entries.push(MatrixEntry {
index: (j, k),
value: if j == k { 1.0 } else { -1.0 }
});
}
let mut gram = PartialMatrix::new();
for j in 0..3 {
for k in 0..3 {
gram.push(j, k, if j == k { 1.0 } else { -1.0 });
}
entries
});
}
let config = {
let a: f64 = 0.75_f64.sqrt();
let a = 0.75_f64.sqrt();
DMatrix::from_columns(&[
sphere(1.0, 0.0, 0.0, a),
sphere(-0.5, a, 0.0, a),
@ -471,6 +683,36 @@ mod tests {
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
#[test]
fn frozen_entry_test() {
let mut problem = ConstraintProblem::from_guess(&[
point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 0.95)
]);
for j in 0..2 {
for k in j..2 {
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
problem.frozen.push(3, 1, 0.5);
let (config, _, success, history) = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
assert_eq!(success, true);
for base_step in history.base_step.into_iter() {
for &MatrixEntry { index, .. } in &problem.frozen {
assert_eq!(base_step[index], 0.0);
}
}
for MatrixEntry { index, value } in problem.frozen {
assert_eq!(config[index], value);
}
}
#[test]
fn irisawa_hexlet_test() {
// solve Irisawa's problem
@ -486,91 +728,240 @@ mod tests {
}
#[test]
fn tangent_test() {
fn tangent_test_three_spheres() {
const SCALED_TOL: f64 = 1.0e-12;
const ELEMENT_DIM: usize = 5;
const ASSEMBLY_DIM: usize = 3;
let gram = {
let mut gram_to_be = PartialMatrix::new();
for j in 0..3 {
for k in j..3 {
gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
gram_to_be
};
let guess = DMatrix::from_columns(&[
let mut problem = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.0, -2.0),
sphere(0.0, 0.0, 1.0, 1.0),
sphere(0.0, 0.0, -1.0, 1.0)
]);
let frozen: [_; 5] = std::array::from_fn(|k| (k, 0));
for j in 0..3 {
for k in j..3 {
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
for n in 0..ELEMENT_DIM {
problem.frozen.push(n, 0, problem.guess[(n, 0)]);
}
let (config, tangent, success, history) = realize_gram(
&gram, guess.clone(), &frozen,
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
&problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
assert_eq!(config, guess);
assert_eq!(config, problem.guess);
assert_eq!(success, true);
assert_eq!(history.scaled_loss.len(), 1);
// confirm that the tangent space has dimension five or less
let ConfigSubspace(ref tangent_basis) = tangent;
assert_eq!(tangent_basis.len(), 5);
// list some motions that should form a basis for the tangent space of
// the solution variety
const UNIFORM_DIM: usize = 4;
let element_dim = problem.guess.nrows();
let assembly_dim = problem.guess.ncols();
let tangent_motions_unif = vec![
basis_matrix((0, 1), UNIFORM_DIM, assembly_dim),
basis_matrix((1, 1), UNIFORM_DIM, assembly_dim),
basis_matrix((0, 2), UNIFORM_DIM, assembly_dim),
basis_matrix((1, 2), UNIFORM_DIM, assembly_dim),
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, -0.5, -0.5,
0.0, 0.0, -0.5, 0.5
])
];
let tangent_motions_std = vec![
basis_matrix((0, 1), element_dim, assembly_dim),
basis_matrix((1, 1), element_dim, assembly_dim),
basis_matrix((0, 2), element_dim, assembly_dim),
basis_matrix((1, 2), element_dim, assembly_dim),
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
0.0, 0.0, 0.0, 0.00, 0.0,
0.0, 0.0, -1.0, -0.25, -1.0,
0.0, 0.0, -1.0, 0.25, 1.0
])
];
// confirm that the dimension of the tangent space is no greater than
// 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
let tangent_motions = vec![
basis_matrix((0, 1), ELEMENT_DIM, ASSEMBLY_DIM),
basis_matrix((1, 1), ELEMENT_DIM, ASSEMBLY_DIM),
basis_matrix((0, 2), ELEMENT_DIM, ASSEMBLY_DIM),
basis_matrix((1, 2), ELEMENT_DIM, ASSEMBLY_DIM),
DMatrix::<f64>::from_column_slice(ELEMENT_DIM, 3, &[
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, -1.0, -0.25, -1.0,
0.0, 0.0, -1.0, 0.25, 1.0
])
];
let tol_sq = ((ELEMENT_DIM * ASSEMBLY_DIM) as f64) * SCALED_TOL * SCALED_TOL;
for motion in tangent_motions {
let motion_proj: DMatrix<_> = motion.column_iter().enumerate().map(
let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL;
for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) {
let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map(
|(k, v)| tangent.proj(&v, k)
).sum();
assert!((motion - motion_proj).norm_squared() < tol_sq);
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
}
}
// at the frozen indices, the optimization steps should have exact zeros,
// and the realized configuration should match the initial guess
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| {
let vel = ang_vel.cross(&pt.fixed_rows::<3>(0));
let mut elt_motion = DVector::zeros(4);
elt_motion.fixed_rows_mut::<3>(0).copy_from(&vel);
elt_motion
}
).collect()
}
#[test]
fn frozen_entry_test() {
let gram = {
let mut gram_to_be = PartialMatrix::new();
for j in 0..2 {
for k in j..2 {
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
gram_to_be
};
let guess = DMatrix::from_columns(&[
point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 1.0)
]);
let frozen = [(3, 0), (3, 1)];
println!();
let (config, _, success, history) = realize_gram(
&gram, guess.clone(), &frozen,
1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
fn tangent_test_kaleidocycle() {
// set up a kaleidocycle and find its tangent space
const SCALED_TOL: f64 = 1.0e-12;
let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
assert_eq!(success, true);
for base_step in history.base_step.into_iter() {
for index in frozen {
assert_eq!(base_step[index], 0.0);
}
}
for index in frozen {
assert_eq!(config[index], guess[index]);
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;
let element_dim = config.nrows();
let assembly_dim = config.ncols();
let tangent_motions_unif = vec![
// the translations along the coordinate axes
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:
// - twisting is the kaleidocycle's only internal degree of
// freedom
// - every first-order motion of the kaleidocycle comes from an
// actual motion
(0..N_HINGES).step_by(2).flat_map(
|n| {
let ang_vert = ((n + 1) as f64) * PI/3.0;
let vel_vert_x = 4.0 * ang_vert.cos();
let vel_vert_y = 4.0 * ang_vert.sin();
[
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0])
]
}
).collect::<Vec<_>>()
];
let tangent_motions_std = tangent_motions_unif.iter().map(
|motion| DMatrix::from_columns(
&config.column_iter().zip(motion).map(
|(v, elt_motion)| local_unif_to_std(v) * elt_motion
).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
let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL;
for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) {
let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map(
|(k, v)| tangent.proj(&v.as_view(), k)
).sum();
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, &[
1.0, 0.0, 0.0, 0.0, dis[0],
0.0, 1.0, 0.0, 0.0, dis[1],
0.0, 0.0, 1.0, 0.0, dis[2],
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
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]
fn proj_equivar_test() {
// find a pair of spheres that meet at 120°
const SCALED_TOL: f64 = 1.0e-12;
let mut problem_orig = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.5, 1.0),
sphere(0.0, 0.0, -0.5, 1.0)
]);
problem_orig.gram.push_sym(0, 0, 1.0);
problem_orig.gram.push_sym(1, 1, 1.0);
problem_orig.gram.push_sym(0, 1, 0.5);
let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram(
&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
assert_eq!(config_orig, problem_orig.guess);
assert_eq!(success_orig, true);
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 = {
let a = 0.5 * FRAC_1_SQRT_2;
DMatrix::from_columns(&[
sphere(a, 0.0, 7.0 + a, 1.0),
sphere(-a, 0.0, 7.0 - a, 1.0)
])
};
let problem_tfm = ConstraintProblem {
gram: problem_orig.gram,
guess: guess_tfm,
frozen: problem_orig.frozen
};
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
assert_eq!(config_tfm, problem_tfm.guess);
assert_eq!(success_tfm, true);
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
const ELEMENT_DIM: usize = 5;
let rot = DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 0.0,
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0, 1.0
]);
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
const SCALED_TOL_TFM: f64 = 1.0e-9;
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

@ -3,6 +3,10 @@ mod assembly;
mod display;
mod engine;
mod outline;
mod specified;
#[cfg(test)]
mod tests;
use rustc_hash::FxHashSet;
use sycamore::prelude::*;

View file

@ -1,61 +1,144 @@
use itertools::Itertools;
use std::rc::Rc;
use sycamore::prelude::*;
use web_sys::{
Event,
HtmlInputElement,
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
};
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}};
use crate::{
AppState,
assembly,
assembly::{
ElementKey,
HalfCurvatureRegulator,
InversiveDistanceRegulator,
Regulator,
RegulatorKey
},
specified::SpecifiedValue
};
// an editable view of the Lorentz product representing a constraint
// an editable view of a regulator
#[component(inline_props)]
fn LorentzProductInput(constraint: Constraint) -> View {
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 || {
batch(|| {
valid.set(true);
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",
bind:value=constraint.lorentz_prod_text,
on:change=move |event: Event| {
let target: HtmlInputElement = event.target().unwrap().unchecked_into();
match target.value().parse::<f64>() {
Ok(lorentz_prod) => batch(|| {
constraint.lorentz_prod.set(lorentz_prod);
constraint.lorentz_prod_valid.set(true);
}),
Err(_) => constraint.lorentz_prod_valid.set(false)
};
class=move || {
if valid.get() {
set_point.with(|set_pt| {
if set_pt.is_present() {
"regulator-input constraint"
} else {
"regulator-input"
}
})
} else {
"regulator-input invalid"
}
},
placeholder=measurement.with(|result| result.to_string()),
bind:value=value,
on:change=move |_| {
valid.set(
match SpecifiedValue::try_from(value.get_clone_untracked()) {
Ok(set_pt) => {
set_point.set(set_pt);
true
}
Err(_) => false
}
)
},
on:keydown={
move |event: KeyboardEvent| {
match event.key().as_str() {
"Escape" => reset_value(),
_ => ()
}
}
}
)
}
}
// a list item that shows a constraint in an outline view of an element
#[component(inline_props)]
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let assembly = &state.assembly;
let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone());
let other_subject = if constraint.subjects.0 == element_key {
constraint.subjects.1
} else {
constraint.subjects.0
};
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
let class = constraint.lorentz_prod_valid.map(
|&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" }
);
view! {
li(class=class.get()) {
input(r#type="checkbox", bind:checked=constraint.active)
div(class="constraint-label") { (other_subject_label) }
LorentzProductInput(constraint=constraint)
div(class="status")
pub trait OutlineItem {
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View;
}
impl OutlineItem for InversiveDistanceRegulator {
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let other_subject = if self.subjects[0] == element_key {
self.subjects[1]
} else {
self.subjects[0]
};
let other_subject_label = state.assembly.elements.with(
|elts| elts[other_subject].label.clone()
);
view! {
li(class="regulator") {
div(class="regulator-label") { (other_subject_label) }
div(class="regulator-type") { "Inversive distance" }
RegulatorInput(regulator=self)
div(class="status")
}
}
}
}
impl OutlineItem for HalfCurvatureRegulator {
fn outline_item(self: Rc<Self>, _element_key: ElementKey) -> View {
view! {
li(class="regulator") {
div(class="regulator-label") // for spacing
div(class="regulator-type") { "Half-curvature" }
RegulatorInput(regulator=self)
div(class="status")
}
}
}
}
// a list item that shows a regulator in an outline view of an element
#[component(inline_props)]
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let regulator = state.assembly.regulators.with(
|regs| regs[regulator_key].clone()
);
regulator.outline_item(element_key)
}
// a list item that shows an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
@ -64,14 +147,27 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
move |sel| if sel.contains(&key) { "selected" } else { "" }
);
let label = element.label.clone();
let rep_components = element.representation.map(
|rep| rep.iter().map(
|u| format!("{:.3}", u).replace("-", "\u{2212}")
).collect()
);
let constrained = element.constraints.map(|csts| csts.len() > 0);
let constraint_list = element.constraints.map(
|csts| csts.clone().into_iter().collect()
let rep_components = move || {
element.representation.with(
|rep| rep.iter().map(
|u| {
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
view! { div { (u_str) } }
}
).collect::<Vec<_>>()
)
};
let regulated = element.regulators.map(|regs| regs.len() > 0);
let regulator_list = element.regulators.map(
move |elt_reg_keys| elt_reg_keys
.clone()
.into_iter()
.sorted_by_key(
|&reg_key| state.assembly.regulators.with(
|regs| regs[reg_key].subjects().len()
)
)
.collect()
);
let details_node = create_node_ref();
view! {
@ -86,7 +182,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
state.select(key, event.shift_key());
event.prevent_default();
},
"ArrowRight" if constrained.get() => {
"ArrowRight" if regulated.get() => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
@ -129,27 +225,20 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
}
) {
div(class="element-label") { (label) }
div(class="element-representation") {
Indexed(
list=rep_components,
view=|coord_str| view! {
div { (coord_str) }
}
)
}
div(class="element-representation") { (rep_components) }
div(class="status")
}
}
ul(class="constraints") {
ul(class="regulators") {
Keyed(
list=constraint_list,
view=move |cst_key| view! {
ConstraintOutlineItem(
constraint_key=cst_key,
list=regulator_list,
view=move |reg_key| view! {
RegulatorOutlineItem(
regulator_key=reg_key,
element_key=key
)
},
key=|cst_key| cst_key.clone()
key=|reg_key| reg_key.clone()
)
}
}
@ -157,9 +246,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
}
}
// a component that lists the elements of the current assembly, showing the
// constraints on each element as a collapsible sub-list. its implementation
// is based on Kate Morley's HTML + CSS tree views:
// a component that lists the elements of the current assembly, showing each
// element's regulators in a collapsible sub-list. its implementation is based
// on Kate Morley's HTML + CSS tree views:
//
// https://iamkate.com/code/tree-views/
//

View file

@ -0,0 +1,44 @@
use std::num::ParseFloatError;
// a real number described by a specification string. since the structure is
// read-only, we can guarantee that `spec` always specifies `value` in the
// following format
// ┌──────────────────────────────────────────────────────┬───────────┐
// │ `spec` │ `value` │
// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥
// │ a string that parses to the floating-point value `x` │ `Some(x)` │
// ├──────────────────────────────────────────────────────┼───────────┤
// │ the empty string │ `None` │
// └──────────────────────────────────────────────────────┴───────────┘
#[readonly::make]
pub struct SpecifiedValue {
pub spec: String,
pub value: Option<f64>
}
impl SpecifiedValue {
pub fn from_empty_spec() -> SpecifiedValue {
SpecifiedValue { spec: String::new(), value: None }
}
pub fn is_present(&self) -> bool {
matches!(self.value, Some(_))
}
}
// 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
impl TryFrom<String> for SpecifiedValue {
type Error = ParseFloatError;
fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() {
Ok(SpecifiedValue::from_empty_spec())
} else {
spec.parse::<f64>().map(
|value| SpecifiedValue { spec: spec, value: Some(value) }
)
}
}
}

14
app-proto/src/tests.rs Normal file
View file

@ -0,0 +1,14 @@
use std::process::Command;
// build and bundle the application, reporting success if there are no errors or
// warnings. to see this test fail while others succeed, try moving `index.html`
// or one of the assets that it links to
#[test]
fn trunk_build_test() {
let build_status = Command::new("trunk")
.arg("build")
.env("RUSTFLAGS", "-D warnings")
.status()
.expect("Call to Trunk failed");
assert!(build_status.success());
}