algovivo Simulation Framework

repository·main·Indexed 43 days ago

https://github.com/juniorrojas/algovivo

An energy-based simulation framework for soft-bodied virtual creatures. It utilizes gradient-based optimization on differentiable energy functions, such as neo-Hookean triangles and controllable muscles, leveraging automatic differentiation via Enzyme. The framework includes a WebAssembly core, a JavaScript wrapper, and tools for neural controllers via MLPPolicy and 2D rendering through the mm2d module.

Tokens
10.3K
Snippets
49
Records
54
Agent score
56%

What's inside algovivo

  1. Quick start with algovivo simulation

    main

    To create a simple simulation in a web environment, import the algovivo.min.js ES6 module and load the algovivo.wasm binary. You initialize a System by passing the WASM instance, configure the simulation geometry (positions, triangles, and muscles) using system.set(), and use SystemViewport to render the results to the DOM.

    To run the simulation loop, call system.step() and viewport.render() at a regular interval (e.g., 30 FPS).

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
    </head>
    <body>
      <script type="module">
        import * as algovivo from "https://cdn.jsdelivr.net/gh/juniorrojas/algovivo@bea663b/build/algovivo.min.js";
    
        async function loadWasm() {
          const response = await fetch("https://cdn.jsdelivr.net/gh/juniorrojas/algovivo@bea663b/build/algovivo.wasm");
          const wasm = await WebAssembly.instantiateStreaming(response);
          return wasm.instance;
        }
    
        async function main() {
          const system = new algovivo.System({
            wasmInstance: await loadWasm()
          });
          system.set({
            pos: [[0, 0], [2, 0], [1, 1]],
            triangles: [[0, 1, 2]],
            muscles: [[0, 2], [1, 2]]
          });
    
          const viewport = new algovivo.SystemViewport({ system });
          document.body.appendChild(viewport.domElement);
          viewport.render();
    
          let t = 0;
          setInterval(() => {
            // Example: periodic muscle control
            system.a.set([
              1,
              0.2 + 0.8 * (Math.cos(t * 0.1) * 0.5 + 0.5)
            ]);
            t++;
    
            system.step();
            viewport.render();
          }, 1000 / 30);
        }
    
        main();
      </script>
    </body>
    </html>
  2. Build algovivo from source

    main

    To build the project from source, you need to build both the JavaScript wrapper and the WebAssembly core.

    Build JavaScript

    Requires npm.

    npm ci
    npm run build

    Build WebAssembly

    Requires python and docker. The WASM build uses a specialized Docker image containing LLVM 18 and Enzyme for automatic differentiation.

    python codegen/codegen_csrc.py && \
    docker run \
      --user $(id -u):$(id -g) \
      -v $(pwd):/workspace \
      -w /workspace \
      ghcr.io/juniorrojas/algovivo/llvm18-enzyme:latest \
      ./build.sh
  3. Register inertial modules in BackwardEuler

    main

    In BackwardEuler, inertial_modules are special modules that own the state (differentiable arguments) that the integrator solves for.

    When adding an inertial module, ensure it implements:

    • add_differentiable_args(args): To register its stateful parameters.
    • get_inertia_src(): To provide the C++ code for inertial energy.
    • get_update_vel_src(): To provide the C++ code for velocity updates.
    • get_optim_init_src(): To provide initialization code for the optimizer.
    • get_grad_projection_src(): To provide gradient projection logic.
    • add_update_pos_args(args) and add_update_vel_args(args): To register update-related arguments.

    Error Conditions:

    • If an inertial module declares no differentiable arguments, a ValueError is raised.
    • If a differentiable argument is not owned by an inertial module, a ValueError is raised.
  4. Use a neural controller (MLPPolicy) for locomotion

    main

    For complex locomotion, you can use a neural controller instead of manual muscle commands. The algovivo.nn.MLPPolicy class maps proprioceptive signals (vertex positions and velocities in a local frame) to muscle control signals.

    To use a pretrained policy:

    1. Load your mesh data and policy JSON.
    2. Initialize algovivo.System with the WASM instance and load the mesh via system.set(meshData).
    3. Instantiate algovivo.nn.MLPPolicy with the system and set active: true.
    4. Load the policy weights using policy.loadData(policyData).
    5. In the simulation loop, call policy.step() followed by system.step().
    const system = new algovivo.System({ wasmInstance: await loadWasm() });
    system.set(meshData);
    
    const policy = new algovivo.nn.MLPPolicy({ system, active: true });
    policy.loadData(policyData);
    
    const viewport = new algovivo.SystemViewport({
      system,
      sortedVertexIds: meshData.sorted_vertex_ids,
      vertexDepths: meshData.depth
    });
    
    setInterval(() => {
      policy.step(); // Computes muscle commands from proprioception
      system.step();  // Advances physics simulation
      viewport.render();
    }, 1000 / 30);
  5. Control muscles using system.a.set()

    main

    Muscle commands are applied to the system using system.a.set([...]). The input array must have a length equal to the number of muscles in the simulation.

    Each value in the array represents a target contraction state:

    • 1: The muscle is relaxed and maintains its original rest length.
    • < 1: The muscle contracts to a fraction of its original rest length.
    • 0: Maximum contraction.

    This control mechanism uses an action-dependent potential energy function: $E(x, a) = \frac{k}{2} \left(\frac{l(x)}{a\ l_0} - 1\right)^2$.

    // Contract the first muscle to 30% of rest length, leave second muscle relaxed
    system.a.set([0.3, 1]);
    
    // Contract both muscles to 30%
    system.a.set([0.3, 0.3]);
  6. Reference: backward_euler_update argument types

    main

    The following table defines the expected argtypes for the backward_euler_update C function call via ctypes:

    ArgumentTypeDescription
    space_dimctypes.c_intDimension of the space
    hctypes.c_floatTime step
    num_verticesctypes.c_intNumber of vertices
    pos0ctypes.POINTER(ctypes.c_float)Initial positions
    vel0ctypes.POINTER(ctypes.c_float)Initial velocities
    vertex_massctypes.c_floatMass per vertex
    num_fixed_verticesctypes.c_intNumber of fixed vertices
    fixed_vertex_idsctypes.POINTER(ctypes.c_int)IDs of fixed vertices
    pos1ctypes.POINTER(ctypes.c_float)Updated positions
    pos_gradctypes.POINTER(ctypes.c_float)Position gradient
    pos_tmpctypes.POINTER(ctypes.c_float)Temporary position buffer
    vel1ctypes.POINTER(ctypes.c_float)Updated velocities
    num_musclesctypes.c_intNumber of muscles
    musclesctypes.POINTER(ctypes.c_int)Muscle indices/mapping
    kctypes.c_floatMuscle stiffness constant
    actypes.POINTER(ctypes.c_float)Muscle parameter array
    l0ctypes.POINTER(ctypes.c_float)Muscle parameter array
    num_trianglesctypes.c_intNumber of triangles
    indicesctypes.POINTER(ctypes.c_int)Triangle indices
    rsictypes.POINTER(ctypes.c_float)RSI parameter array
    muctypes.POINTER(ctypes.c_float)Mu parameter array
    lambdactypes.POINTER(ctypes.c_float)Lambda parameter array
    gctypes.c_floatGravity
    k_frictionctypes.c_floatFriction coefficient
    k_collisionctypes.c_floatCollision coefficient
    max_optim_itersctypes.c_intMaximum optimization iterations
    initial_step_sizectypes.c_floatInitial step size for line search
    backtracking_scalectypes.c_floatBacktracking scale factor
    max_line_search_itersctypes.c_intMaximum line search iterations
    grad_q_tolctypes.c_floatGradient tolerance
  7. Prepare muscle arguments for simulation steps with to_step_args()

    main

    The .to_step_args() method prepares the muscle parameters as a list of arguments suitable for passing to the underlying library's simulation steps. It handles the conversion of tensors to pointers (using as_int_ptr and as_float_ptr).

    Returns a list containing:

    1. num_muscles (int)
    2. indices (int pointer or None)
    3. k (float)
    4. a (float pointer or None)
    5. l0 (float pointer or None)
    args = muscles.to_step_args()
    # args can then be used to call library functions
  8. Load a native library with NativeInstance

    main

    Use NativeInstance.load(filename) to load a C++ shared library via ctypes. This method verifies the file exists before attempting to load it. It returns an instance of NativeInstance which wraps the loaded library object.

    from algovivo.native_instance import NativeInstance
    
    # Load the shared library
    instance = NativeInstance.load("path/to/your/library.so")
    # Access the underlying library via instance.lib
  9. Use the BackwardEuler class for numerical integration codegen

    main

    The BackwardEuler class is a code generation tool used to instantiate a Backward Euler numerical integrator. It orchestrates the collection of modules, potentials, and inertial components to generate C++ source code (headers) for loss functions, optimizers, and update routines.

    To use it, you typically:

    1. Instantiate BackwardEuler.
    2. Register modules (for potentials) and inertial_modules (for stateful/inertial dynamics).
    3. Register potentials to contribute to the energy loss.
    4. Call instantiate_templates(csrc_dirpath) to write the generated .h files to a C++ source directory.

    Constraints:

    • Every differentiable argument must be owned by an inertial_module.
    • At least one differentiable argument must be present for the solver to function.
    from codegen.algovivo_codegen.backward_euler import BackwardEuler
    
    be = BackwardEuler()
    # ... add modules, potentials, and inertial_modules ...
    be.instantiate_templates("path/to/csrc_output")
  10. Initialize the mmgrten engine

    main

    Use the engine function to create a new instance of the Engine class. You can optionally pass a wasmInstance within the args object to provide a pre-initialized WebAssembly instance to the engine.

    Returns an instance of Engine.

    import { engine } from './mmgrten/index.js';
    
    const ten = engine({ wasmInstance: myWasmInstance });