Constraint-based three-dimensional dynamic geometry
Go to file
Vectornaut 22870342f3 Manipulate the assembly (#29)
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: #29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
app-proto Manipulate the assembly (#29) 2024-12-30 22:53:07 +00:00
coffeetest Set up testing with Ava 2019-12-11 12:07:43 -05:00
doc Switch to good old make to reduce redundancies in build 2019-12-12 00:33:59 -05:00
engine-proto Integrate engine into application prototype (#15) 2024-11-12 00:46:16 +00:00
notes Integrate engine into application prototype (#15) 2024-11-12 00:46:16 +00:00
src Adjust lighting and camera for decent initial rendering of polyhedra 2019-12-31 07:20:33 -08:00
.gitignore Copy only the production dependencies to the site directory 2019-12-11 13:20:31 -05:00
LICENSE Initial commit 2019-09-14 19:00:59 +00:00
Makefile First pass at coordinate axes 2019-12-12 02:44:33 -05:00
package-lock.json Adjust lighting and camera for decent initial rendering of polyhedra 2019-12-31 07:20:33 -08:00
package.json Switch to good old make to reduce redundancies in build 2019-12-12 00:33:59 -05:00
README.md Turn non-automated tests into Cargo examples (#24) 2024-11-26 00:32:50 +00:00

dyna3

Abstract

Constraint-based three-dimensional dynamic geometry

Description

From a thorough web search, there does not seem to be a dynamic geometry software package which (a) began its life handling three dimensions, rather than just two, and (b) allows you to express the desired geometric configuration in terms of constraints on the entities (e.g. l and k are parallel, a, b, and c a collinear, etc.) rather than as a construction (e.g. l is the perpendicular bisector of a and b). The goal of the dyna3 project is to close this gap.

Note that currently this is just the barest beginnings of the project, more of a framework for developing dyna3 rather than anything useful.

Implementation goals

  • Comfortable, intuitive UI

  • Able to run in browser (so implemented in WASM-compatible language)

  • Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well.

Prototype

The latest prototype is in the folder app-proto. It includes both a user interface and a numerical constraint-solving engine.

Install the prerequisites

  1. Install rustup: the officially recommended Rust toolchain manager
    • It's available on Ubuntu as a Snap
  2. Call rustup default stable to "download the latest stable release of Rust and set it as your default toolchain"
    • If you forget, the rustup help system will remind you
  3. Call rustup target add wasm32-unknown-unknown to add the most generic 32-bit WebAssembly target
  4. Call cargo install wasm-pack to install the WebAssembly toolchain
  5. Call cargo install trunk to install the Trunk web-build tool
  6. Add the .cargo/bin folder in your home directory to your executable search path
    • This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
    • On POSIX systems, the search path is stored in the PATH environment variable

Play with the prototype

  1. Go into the app-proto folder
  2. 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
  3. In a web browser, visit one of the URLs listed under the message INFO 📡 server listening at:
    • Touching any file in the app-proto folder will make Trunk rebuild and live-reload the prototype
  4. Press ctrl+C in the shell where Trunk is running to stop serving the prototype

Run the engine on some example problems

  1. Go into the app-proto folder
  2. Call ./run-examples
    • For each example problem, the engine will print the value of the loss function at each optimization step

    • The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into engine-proto/gram-test, launch Julia, and then

      include("irisawa-hexlet.jl")
      for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
        println(rpad(step-1, 4), " | ", scaled_loss)
      end
      

      you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show

Run the automated tests

  1. Go into the app-proto folder
  2. Call cargo test