├── utils ├── py │ ├── .gitignore │ ├── algovivo │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── system.py │ │ ├── native_instance.py │ │ ├── muscles.py │ │ ├── vertices.py │ │ └── triangles.py │ ├── test │ │ ├── test_load_lib.py │ │ └── trajectory │ │ │ └── test_trajectory.py │ └── pyproject.toml └── trajectory │ ├── public │ ├── algovivo.wasm │ ├── algovivo.min.js │ └── index.html │ ├── ppw │ ├── index.js │ ├── server.js │ └── Window.js │ ├── pathutils │ └── index.js │ ├── FrameRecorder.js │ └── Trajectory.js ├── demo ├── public │ ├── algovivo.wasm │ ├── .gitignore │ ├── icons │ │ └── doc.svg │ └── index.html ├── test │ ├── ppw │ │ ├── index.js │ │ ├── Window.js │ │ └── server.js │ ├── utils.js │ ├── agentSystem.test.js │ └── basic.test.js ├── .babelrc ├── rollup.config.js ├── package.json ├── src │ ├── AgentSystem.js │ ├── Footer.js │ ├── ui.js │ ├── Section.js │ ├── Header.js │ ├── main.js │ ├── AgentMini.js │ ├── AgentManager.js │ └── BrainButton.js └── scripts │ └── deploy-local ├── media ├── periodic.gif ├── locomotion.gif ├── muscle-contract-both.png ├── muscle-contract-left.png └── muscle-contract-right.png ├── algovivo ├── nn │ └── index.js ├── mmgrten │ ├── nn │ │ ├── Module.js │ │ ├── Sequential.js │ │ ├── ReLU.js │ │ ├── Tanh.js │ │ ├── index.js │ │ └── Linear.js │ ├── mmgr │ │ ├── linked │ │ │ ├── index.js │ │ │ ├── Node.js │ │ │ └── List.js │ │ ├── index.js │ │ ├── ReservedSlot.js │ │ ├── FreeSlot.js │ │ ├── Slot.js │ │ └── MemoryManager.js │ ├── index.js │ ├── TensorIterator.js │ ├── IntTuple.js │ ├── utils.js │ ├── Functional.js │ └── Engine.js ├── render │ ├── mm2d │ │ ├── background │ │ │ ├── index.js │ │ │ └── Background.js │ │ ├── ui │ │ │ ├── index.js │ │ │ ├── cursorUtils.js │ │ │ └── DragBehavior.js │ │ ├── core │ │ │ ├── index.js │ │ │ ├── Scene.js │ │ │ ├── Camera.js │ │ │ └── Mesh.js │ │ ├── shaders │ │ │ ├── index.js │ │ │ ├── PointShader.js │ │ │ ├── LineShader.js │ │ │ └── TriangleShader.js │ │ ├── math │ │ │ ├── index.js │ │ │ ├── Transform2d.js │ │ │ ├── AABB.js │ │ │ ├── Vec2.js │ │ │ └── Matrix2x2.js │ │ ├── sorted │ │ │ ├── Simplex.js │ │ │ ├── Vertex.js │ │ │ ├── Simplices.js │ │ │ ├── MeshTopology.js │ │ │ └── index.js │ │ └── index.js │ ├── index.js │ ├── TriangleRenderer.js │ ├── Floor.js │ ├── Tracker.js │ ├── VertexRenderer.js │ └── LineRenderer.js ├── index.js ├── Muscles.js └── Triangles.js ├── codegen ├── algovivo_codegen │ ├── codegen │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── fun.py │ │ ├── backward_pass_fun.py │ │ └── args.py │ ├── csrc │ │ ├── main.cpp │ │ ├── mmgrten │ │ │ ├── mmgrten.h │ │ │ ├── fill.h │ │ │ ├── add.h │ │ │ ├── relu.h │ │ │ ├── clamp.h │ │ │ ├── matvec.h │ │ │ ├── tensor.h │ │ │ └── mm.h │ │ ├── enzyme.h │ │ ├── modules │ │ │ ├── gravity.h │ │ │ ├── collision.h │ │ │ ├── friction.h │ │ │ ├── muscles.h │ │ │ └── triangles.h │ │ ├── vec2.h │ │ ├── dynamics │ │ │ └── inertia.h │ │ ├── mat2x2.h │ │ ├── arr.h │ │ └── frame_projection.h │ ├── __init__.py │ ├── modules │ │ ├── __init__.py │ │ ├── collision.py │ │ ├── friction.py │ │ ├── muscles.py │ │ ├── triangles.py │ │ └── vertices.py │ ├── potentials │ │ ├── __init__.py │ │ ├── triangles.py │ │ ├── collision.py │ │ ├── muscles.py │ │ ├── friction.py │ │ └── gravity.py │ ├── templates │ │ ├── backward_euler.template.h │ │ └── optim.template.h │ └── neohookean.py ├── test │ └── test_backward_euler.py └── codegen_csrc.py ├── jest.config.js ├── .gitignore ├── test ├── render │ ├── vertexRenderer.test.js │ ├── mm2d │ │ ├── AABB.test.js │ │ ├── grid.test.js │ │ └── sortedElements.test.js │ └── viewport.test.js ├── ten │ ├── fill.test.js │ ├── intTuple.test.js │ ├── nn │ │ ├── sequential.test.js │ │ └── linear.test.js │ ├── clamp.test.js │ └── tensor.test.js ├── systemWithCustomWasmEnv.test.js ├── backwardEuler.test.js ├── system.test.js ├── memory.test.js ├── utils.js ├── nn │ ├── generateTrajectory.js │ └── trajectory.test.js ├── vertices.test.js ├── frameProjection.test.js ├── muscles.test.js └── triangles.test.js ├── CITATION.bib ├── .github └── workflows │ ├── test-codegen.yml │ ├── test.yml │ ├── py.yml │ ├── build-enzyme-docker-image.yml │ └── trajectory.yml ├── enzyme └── Dockerfile-llvm11 ├── package.json ├── LICENSE └── rollup.config.js /utils/py/.gitignore: -------------------------------------------------------------------------------- 1 | _version.py -------------------------------------------------------------------------------- /demo/public/algovivo.wasm: -------------------------------------------------------------------------------- 1 | ../../build/algovivo.wasm -------------------------------------------------------------------------------- /utils/trajectory/public/algovivo.wasm: -------------------------------------------------------------------------------- 1 | ../../../build/algovivo.wasm -------------------------------------------------------------------------------- /utils/trajectory/public/algovivo.min.js: -------------------------------------------------------------------------------- 1 | ../../../build/algovivo.min.js -------------------------------------------------------------------------------- /media/periodic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorrojas/algovivo/HEAD/media/periodic.gif -------------------------------------------------------------------------------- /media/locomotion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorrojas/algovivo/HEAD/media/locomotion.gif -------------------------------------------------------------------------------- /algovivo/nn/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NeuralFramePolicy: require("./NeuralFramePolicy") 3 | } -------------------------------------------------------------------------------- /algovivo/mmgrten/nn/Module.js: -------------------------------------------------------------------------------- 1 | class Module { 2 | constructor() { 3 | 4 | } 5 | } 6 | 7 | module.exports = Module; -------------------------------------------------------------------------------- /media/muscle-contract-both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorrojas/algovivo/HEAD/media/muscle-contract-both.png -------------------------------------------------------------------------------- /media/muscle-contract-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorrojas/algovivo/HEAD/media/muscle-contract-left.png -------------------------------------------------------------------------------- /media/muscle-contract-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorrojas/algovivo/HEAD/media/muscle-contract-right.png -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/linked/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | List: require("./List"), 3 | Node: require("./Node") 4 | }; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/codegen/__init__.py: -------------------------------------------------------------------------------- 1 | from .args import Args 2 | from .fun import Fun 3 | from .utils import indent -------------------------------------------------------------------------------- /codegen/algovivo_codegen/codegen/utils.py: -------------------------------------------------------------------------------- 1 | def indent(s): 2 | return "\n".join(" " + line for line in s.split("\n")) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: [ 3 | "/node_modules/", 4 | "/demo/" 5 | ] 6 | }; -------------------------------------------------------------------------------- /algovivo/render/mm2d/background/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Grid: require("./Grid"), 3 | Background: require("./Background") 4 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/ui/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cursorUtils: require("./cursorUtils"), 3 | DragBehavior: require("./DragBehavior") 4 | }; -------------------------------------------------------------------------------- /demo/public/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .mypy_cache 4 | node_modules 5 | *.swo 6 | *.swp 7 | .DS_Store 8 | .vscode 9 | *.out 10 | *.out.* 11 | *.log 12 | *.log.* -------------------------------------------------------------------------------- /utils/py/algovivo/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .native_instance import NativeInstance 3 | from .system import System 4 | from .vertices import Vertices -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/main.cpp: -------------------------------------------------------------------------------- 1 | #include "mmgrten/mmgrten.h" 2 | 3 | #include "enzyme.h" 4 | 5 | #include "dynamics/backward_euler.h" 6 | 7 | #include "frame_projection.h" -------------------------------------------------------------------------------- /utils/trajectory/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/test/ppw/index.js: -------------------------------------------------------------------------------- 1 | import Window from "./Window.js"; 2 | import { runWebServer } from "./server.js"; 3 | 4 | export default { 5 | Window: Window, 6 | runWebServer: runWebServer 7 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/core/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Camera: require("./Camera"), 3 | Mesh: require("./Mesh"), 4 | Renderer: require("./Renderer"), 5 | Scene: require("./Scene") 6 | }; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/__init__.py: -------------------------------------------------------------------------------- 1 | from .codegen import indent, Args, Fun 2 | from .backward_euler import BackwardEuler 3 | from .neohookean import Neohookean 4 | from . import potentials, modules -------------------------------------------------------------------------------- /algovivo/render/mm2d/shaders/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PointShader: require("./PointShader"), 3 | LineShader: require("./LineShader"), 4 | TriangleShader: require("./TriangleShader") 5 | }; -------------------------------------------------------------------------------- /codegen/test/test_backward_euler.py: -------------------------------------------------------------------------------- 1 | import algovivo_codegen 2 | 3 | def test_backward_euler(): 4 | backward_euler = algovivo_codegen.BackwardEuler() 5 | assert backward_euler.loss is not None -------------------------------------------------------------------------------- /algovivo/render/mm2d/math/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Vec2: require("./Vec2"), 3 | Matrix2x2: require("./Matrix2x2"), 4 | Transform2d: require("./Transform2d"), 5 | AABB: require("./AABB") 6 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/mmgrten.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "tensor.h" 3 | #include "add.h" 4 | #include "mm.h" 5 | #include "matvec.h" 6 | #include "relu.h" 7 | #include "clamp.h" 8 | #include "fill.h" -------------------------------------------------------------------------------- /codegen/algovivo_codegen/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .vertices import Vertices 2 | from .muscles import Muscles 3 | from .triangles import Triangles 4 | from .friction import Friction 5 | from .collision import Collision -------------------------------------------------------------------------------- /codegen/algovivo_codegen/potentials/__init__.py: -------------------------------------------------------------------------------- 1 | from .muscles import Muscles 2 | from .triangles import Triangles 3 | from .collision import Collision 4 | from .friction import Friction 5 | from .gravity import Gravity -------------------------------------------------------------------------------- /codegen/algovivo_codegen/modules/collision.py: -------------------------------------------------------------------------------- 1 | class Collision: 2 | def add_args(self, args): 3 | args.add_arg("float", "k_collision") 4 | 5 | def add_update_args(self, args): 6 | self.add_args(args) -------------------------------------------------------------------------------- /codegen/algovivo_codegen/modules/friction.py: -------------------------------------------------------------------------------- 1 | class Friction: 2 | def add_args(self, args): 3 | args.add_arg("float", "k_friction") 4 | 5 | def add_update_args(self, args): 6 | self.add_args(args) -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | linked: require("./linked"), 3 | MemoryManager: require("./MemoryManager"), 4 | FreeSlot: require("./FreeSlot"), 5 | ReservedSlot: require("./ReservedSlot") 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .mypy_cache 4 | node_modules 5 | *.swo 6 | *.swp 7 | .DS_Store 8 | .vscode 9 | *.out 10 | *.out.* 11 | *.log 12 | *.log.* 13 | /build 14 | *.build.* 15 | /csrc 16 | *.ignore 17 | *.egg-info 18 | -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/enzyme.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | int enzyme_dup; 4 | int enzyme_out; 5 | int enzyme_const; 6 | 7 | template 8 | Output __enzyme_autodiff(Output (*)(ForwardArgs...), BackwardArgs...); -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-transform-runtime" 14 | ] 15 | } -------------------------------------------------------------------------------- /utils/trajectory/ppw/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Window: require("./Window") 3 | } 4 | 5 | function mergeModule(name) { 6 | const m = require(name); 7 | for (let k in m) { 8 | module.exports[k] = m[k]; 9 | } 10 | } 11 | 12 | mergeModule("./server"); -------------------------------------------------------------------------------- /test/render/vertexRenderer.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | 3 | test("vertex renderer", () => { 4 | const system = { numVertices: 4 }; 5 | const vertexRenderer = new algovivo.render.VertexRenderer({ system }); 6 | expect(vertexRenderer.numVertices).toBe(4); 7 | }); -------------------------------------------------------------------------------- /algovivo/render/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SystemViewport: require("./SystemViewport"), 3 | VertexRenderer: require("./VertexRenderer"), 4 | LineRenderer: require("./LineRenderer"), 5 | TriangleRenderer: require("./TriangleRenderer"), 6 | Tracker: require("./Tracker"), 7 | }; -------------------------------------------------------------------------------- /demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | 3 | export default { 4 | input: ["src/main.js"], 5 | output: { 6 | file: "public/main.build.js", 7 | format: "esm", 8 | sourcemap: false 9 | }, 10 | plugins: [ 11 | terser() 12 | ], 13 | }; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/modules/gravity.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace algovivo { 4 | 5 | __attribute__((always_inline)) 6 | void accumulate_gravity_energy( 7 | float &energy, 8 | float py, 9 | float m, 10 | float g 11 | ) { 12 | energy += py * m * g; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/fill.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "tensor.h" 3 | 4 | namespace mmgrten { 5 | 6 | extern "C" 7 | void fill_( 8 | int n, 9 | float* data, 10 | float value 11 | ) { 12 | for (int i = 0; i < n; i++) { 13 | data[i] = value; 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/vec2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define vec2_get(name, arr, i) \ 4 | const auto (name##x) = arr[2 * i ]; \ 5 | const auto (name##y) = arr[2 * i + 1]; 6 | 7 | #define vec2_sub(r, a, b) \ 8 | const auto (r##x) = (a##x) - (b##x); \ 9 | const auto (r##y) = (a##y) - (b##y); -------------------------------------------------------------------------------- /demo/test/utils.js: -------------------------------------------------------------------------------- 1 | import fsp from "fs/promises"; 2 | 3 | export async function loadWasm(args = {}) { 4 | const wasm = await WebAssembly.compile(await fsp.readFile(__dirname + "/../public/algovivo.wasm")); 5 | const wasmInstance = await WebAssembly.instantiate(wasm, args); 6 | return wasmInstance; 7 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/sorted/Simplex.js: -------------------------------------------------------------------------------- 1 | class Simplex { 2 | constructor(id, vertexIds) { 3 | if (id == null) { 4 | throw new Error("id required to create simplex"); 5 | } 6 | this.order = vertexIds.length; 7 | this.id = id; 8 | this.vertexIds = vertexIds; 9 | } 10 | } 11 | 12 | module.exports = Simplex; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/modules/collision.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace algovivo { 4 | 5 | __attribute__((always_inline)) 6 | void accumulate_collision_energy( 7 | float &energy, 8 | float py, 9 | float k_collision 10 | ) { 11 | if (py < 0) { 12 | energy += k_collision * py * py; 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /demo/public/icons/doc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /algovivo/render/mm2d/index.js: -------------------------------------------------------------------------------- 1 | const core = require("./core") 2 | 3 | module.exports = { 4 | math: require("./math"), 5 | ui: require("./ui"), 6 | shaders: require("./shaders"), 7 | background: require("./background"), 8 | sorted: require("./sorted"), 9 | core: core, 10 | Renderer: core.Renderer, 11 | Camera: core.Camera, 12 | Scene: core.Scene 13 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/shaders/PointShader.js: -------------------------------------------------------------------------------- 1 | class PointShader { 2 | constructor() { 3 | } 4 | 5 | renderPoint(args = {}) { 6 | const ctx = args.ctx; 7 | const p = args.p; 8 | 9 | const radius = 3; 10 | ctx.beginPath(); 11 | ctx.arc(p[0], p[1], radius, 0, 2 * Math.PI); 12 | ctx.fill(); 13 | } 14 | } 15 | 16 | module.exports = PointShader; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/add.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace mmgrten { 4 | 5 | extern "C" 6 | void add( 7 | int a_numel, 8 | const float* a_data, 9 | const float* b_data, 10 | float* c_data 11 | ) { 12 | for (int i = 0; i < a_numel; i++) { 13 | auto ai = a_data[i]; 14 | auto bi = b_data[i]; 15 | c_data[i] = ai + bi; 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /algovivo/mmgrten/index.js: -------------------------------------------------------------------------------- 1 | const Engine = require("./Engine"); 2 | 3 | function engine(args = {}) { 4 | const ten = new Engine({ 5 | wasmInstance: args.wasmInstance 6 | }); 7 | return ten; 8 | } 9 | 10 | module.exports = { 11 | engine: engine, 12 | Engine: Engine, 13 | Tensor: require("./Tensor"), 14 | mmgr: require("./mmgr"), 15 | utils: require("./utils") 16 | }; -------------------------------------------------------------------------------- /utils/py/test/test_load_lib.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | this_filepath = Path(os.path.realpath(__file__)) 4 | this_dirpath = this_filepath.parent 5 | import algovivo 6 | 7 | def test_load_lib(): 8 | native_instance = algovivo.NativeInstance.load( 9 | os.environ["ALGOVIVO_LIB_FILENAME"] 10 | ) 11 | assert native_instance.lib.backward_euler_update is not None -------------------------------------------------------------------------------- /codegen/algovivo_codegen/potentials/triangles.py: -------------------------------------------------------------------------------- 1 | class Triangles: 2 | def __init__(self): 3 | pass 4 | 5 | def add_to_loss(self, be): 6 | from ..neohookean import Neohookean 7 | neohookean = Neohookean( 8 | simplex_order=3, 9 | simplex_name_singular="triangle" 10 | ) 11 | be.loss_body += "\n" + neohookean.codegen_accumulate_simplices_energy() -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/relu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "tensor.h" 3 | 4 | namespace mmgrten { 5 | 6 | extern "C" 7 | void relu( 8 | int n, 9 | const float* a_data, 10 | float* b_data 11 | ) { 12 | // TODO strides 13 | for (int i = 0; i < n; i++) { 14 | const auto ai = a_data[i]; 15 | float bi; 16 | if (ai < 0) bi = 0; else bi = ai; 17 | b_data[i] = bi; 18 | } 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/modules/muscles.py: -------------------------------------------------------------------------------- 1 | class Muscles: 2 | def __init__(self): 3 | pass 4 | 5 | def add_args(self, args): 6 | args.add_arg("int", "num_muscles") 7 | args.add_arg("int*", "muscles") 8 | args.add_arg("float", "k") 9 | args.add_arg("float*", "a") 10 | args.add_arg("float*", "l0") 11 | 12 | def add_update_args(self, args): 13 | self.add_args(args) -------------------------------------------------------------------------------- /codegen/algovivo_codegen/potentials/collision.py: -------------------------------------------------------------------------------- 1 | class Collision: 2 | def __init__(self): 3 | pass 4 | 5 | def add_to_loss(self, be): 6 | be.loss_body += """ 7 | for (int i = 0; i < num_vertices; i++) { 8 | const auto offset = space_dim * i; 9 | const auto py = pos[offset + 1]; 10 | 11 | accumulate_collision_energy( 12 | potential_energy, 13 | py, 14 | k_collision 15 | ); 16 | } 17 | """ -------------------------------------------------------------------------------- /codegen/algovivo_codegen/modules/triangles.py: -------------------------------------------------------------------------------- 1 | class Triangles: 2 | def __init__(self): 3 | pass 4 | 5 | def add_args(self, args): 6 | args.add_arg("int", "num_triangles") 7 | args.add_arg("int*", "triangles") 8 | args.add_arg("float*", "rsi") 9 | args.add_arg("float*", "mu") 10 | args.add_arg("float*", "lambda") 11 | 12 | def add_update_args(self, args): 13 | self.add_args(args) -------------------------------------------------------------------------------- /test/ten/fill.test.js: -------------------------------------------------------------------------------- 1 | const { mmgrten } = require("algovivo"); 2 | const utils = require("../utils"); 3 | 4 | test("fill", async () => { 5 | const ten = await utils.loadTen(); 6 | 7 | const a = ten.zeros([2, 3]); 8 | 9 | a.fill_(2); 10 | expect(a.toArray()).toEqual([ 11 | [2, 2, 2], 12 | [2, 2, 2] 13 | ]); 14 | 15 | a.zero_(); 16 | expect(a.toArray()).toEqual([ 17 | [0, 0, 0], 18 | [0, 0, 0] 19 | ]); 20 | }) 21 | -------------------------------------------------------------------------------- /algovivo/render/mm2d/sorted/Vertex.js: -------------------------------------------------------------------------------- 1 | const Simplices = require("./Simplices"); 2 | 3 | class Vertex { 4 | constructor(id) { 5 | this.id = id; 6 | this.edges = new Simplices({ order: 2 }); 7 | this.triangles = new Simplices({ order: 3 }); 8 | } 9 | 10 | addTriangle(triangle, id) { 11 | this.triangles.add(triangle, id); 12 | } 13 | 14 | addEdge(edge, id) { 15 | this.edges.add(edge, id); 16 | } 17 | } 18 | 19 | module.exports = Vertex; -------------------------------------------------------------------------------- /algovivo/index.js: -------------------------------------------------------------------------------- 1 | const System = require("./System"); 2 | const Vertices = require("./Vertices"); 3 | 4 | const mmgrten = require("./mmgrten/index"); 5 | const render = require("./render"); 6 | const mm2d = require("./render/mm2d"); 7 | const nn = require("./nn"); 8 | 9 | module.exports = { 10 | System: System, 11 | Vertices: Vertices, 12 | mmgrten: mmgrten, 13 | SystemViewport: render.SystemViewport, 14 | mm2d: mm2d, 15 | render: render, 16 | nn: nn 17 | }; -------------------------------------------------------------------------------- /algovivo/render/mm2d/shaders/LineShader.js: -------------------------------------------------------------------------------- 1 | class LineShader { 2 | constructor() { 3 | } 4 | 5 | renderLine(args = {}) { 6 | const ctx = args.ctx; 7 | const a = args.a; 8 | const b = args.b; 9 | 10 | ctx.beginPath(); 11 | ctx.strokeStyle = "red"; 12 | ctx.lineWidth = 5; 13 | ctx.moveTo(a[0], a[1]); 14 | ctx.lineTo(b[0], b[1]); 15 | ctx.closePath(); 16 | ctx.stroke(); 17 | } 18 | } 19 | 20 | module.exports = LineShader; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/potentials/muscles.py: -------------------------------------------------------------------------------- 1 | class Muscles: 2 | def __init__(self): 3 | pass 4 | 5 | def add_to_loss(self, be): 6 | be.loss_body += """ 7 | for (int i = 0; i < num_muscles; i++) { 8 | const auto offset = i * 2; 9 | const auto i1 = muscles[offset ]; 10 | const auto i2 = muscles[offset + 1]; 11 | 12 | accumulate_muscle_energy( 13 | potential_energy, 14 | pos, 15 | i1, i2, 16 | a[i], l0[i], k 17 | ); 18 | }""" -------------------------------------------------------------------------------- /utils/py/algovivo/utils.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import torch 3 | 4 | float_ptr_t = ctypes.POINTER(ctypes.c_float) 5 | int_ptr_t = ctypes.POINTER(ctypes.c_int) 6 | 7 | def as_float_ptr(x: torch.Tensor): 8 | if x is None: 9 | return None 10 | return ctypes.cast(x.data_ptr(), ctypes.POINTER(ctypes.c_float)) 11 | 12 | def as_int_ptr(x: torch.Tensor): 13 | if x is None: 14 | return None 15 | return ctypes.cast(x.data_ptr(), ctypes.POINTER(ctypes.c_int32)) -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/modules/friction.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace algovivo { 4 | 5 | __attribute__((always_inline)) 6 | void accumulate_friction_energy( 7 | float &energy, 8 | float px, 9 | float p0x, float p0y, 10 | float h, 11 | float k_friction 12 | ) { 13 | const float eps = 1e-2; 14 | const auto height = p0y - eps; 15 | if (height < 0) { 16 | const auto vx = (px - p0x) / h; 17 | energy += k_friction * vx * vx * -height; 18 | } 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /test/render/mm2d/AABB.test.js: -------------------------------------------------------------------------------- 1 | const mm2d = require("algovivo").mm2d; 2 | 3 | test("aabb", () => { 4 | const aabb = new mm2d.math.AABB({ 5 | x0: 20, 6 | y0: 15, 7 | width: 10, 8 | height: 40, 9 | }); 10 | expect(aabb.x0).toEqual(20); 11 | expect(aabb.x1).toEqual(30); 12 | expect(aabb.width).toEqual(10); 13 | expect(aabb.height).toEqual(40); 14 | expect(aabb.y0).toEqual(15); 15 | expect(aabb.y1).toEqual(55); 16 | expect(aabb.center).toEqual([25, 35]); 17 | }); -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/clamp.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "tensor.h" 3 | 4 | namespace mmgrten { 5 | 6 | extern "C" 7 | void clamp( 8 | int n, 9 | const float* a_data, 10 | float* b_data, 11 | float min, float max, 12 | bool use_min, bool use_max 13 | ) { 14 | for (int i = 0; i < n; i++) { 15 | auto ai = a_data[i]; 16 | float bi; 17 | if (use_min && ai < min) bi = min; 18 | else if (use_max && ai > max) bi = max; 19 | else bi = ai; 20 | b_data[i] = bi; 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @proceedings{10.1162/isal_a_00748, 2 | author = {Rojas, Junior}, 3 | title = "{Energy-Based Models for Virtual Creatures}", 4 | volume = {ALIFE 2024: Proceedings of the 2024 Artificial Life Conference}, 5 | series = {Artificial Life Conference Proceedings}, 6 | pages = {30}, 7 | year = {2024}, 8 | month = {07}, 9 | doi = {10.1162/isal_a_00748}, 10 | url = {https://doi.org/10.1162/isal\_a\_00748}, 11 | eprint = {https://direct.mit.edu/isal/proceedings-pdf/isal2024/36/30/2461221/isal\_a\_00748.pdf}, 12 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/core/Scene.js: -------------------------------------------------------------------------------- 1 | const Mesh = require("./Mesh"); 2 | 3 | class Scene { 4 | constructor() { 5 | this.meshes = new Map(); 6 | } 7 | 8 | clean() { 9 | this.meshes = new Map(); 10 | } 11 | 12 | numMeshes() { 13 | return this.meshes.size; 14 | } 15 | 16 | addMesh() { 17 | const id = this.meshes.size; 18 | const mesh = new Mesh({ 19 | scene: this, 20 | id: id 21 | }); 22 | this.meshes.set(id, mesh); 23 | return mesh; 24 | } 25 | } 26 | 27 | module.exports = Scene; -------------------------------------------------------------------------------- /algovivo/mmgrten/nn/Sequential.js: -------------------------------------------------------------------------------- 1 | const Module = require("./Module"); 2 | 3 | class Sequential extends Module { 4 | constructor(nn, layers) { 5 | super(); 6 | this.nn = nn; 7 | this.layers = layers; 8 | } 9 | 10 | forward(x) { 11 | let x1 = x; 12 | this.layers.forEach(layer => { 13 | x1 = layer.forward(x1); 14 | }); 15 | return x1; 16 | } 17 | 18 | dispose() { 19 | this.layers.forEach(layer => { 20 | layer.dispose(); 21 | }); 22 | } 23 | } 24 | 25 | module.exports = Sequential; -------------------------------------------------------------------------------- /demo/test/agentSystem.test.js: -------------------------------------------------------------------------------- 1 | import AgentSystem from "../src/AgentSystem.js"; 2 | import algovivo from "../../algovivo/index.js"; 3 | import { loadWasm } from "./utils.js"; 4 | 5 | test("agent system", async () => { 6 | const wasmInstance = await loadWasm(); 7 | const system = new algovivo.System({ wasmInstance }); 8 | const agentSystem = new AgentSystem({ algovivo, system }); 9 | expect(agentSystem.numVertices).toBe(0); 10 | agentSystem.set({ 11 | mesh: { pos: [[0, 0]] } 12 | }); 13 | expect(agentSystem.numVertices).toBe(1); 14 | }); -------------------------------------------------------------------------------- /algovivo/render/TriangleRenderer.js: -------------------------------------------------------------------------------- 1 | class TriangleRenderer { 2 | constructor(args = {}) { 3 | this.fillColor = args.fillColor ?? "white"; 4 | } 5 | 6 | renderTriangle(args = {}) { 7 | const ctx = args.ctx; 8 | const a = args.a; 9 | const b = args.b; 10 | const c = args.c; 11 | 12 | ctx.beginPath(); 13 | ctx.fillStyle = this.fillColor; 14 | ctx.moveTo(...a); 15 | ctx.lineTo(...b); 16 | ctx.lineTo(...c); 17 | ctx.closePath(); 18 | ctx.fill(); 19 | } 20 | } 21 | 22 | module.exports = TriangleRenderer; -------------------------------------------------------------------------------- /.github/workflows/test-codegen.yml: -------------------------------------------------------------------------------- 1 | name: test-codegen 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test-codegen: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Clone repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Install dependencies 20 | run: pip install pytest 21 | 22 | - name: Test 23 | run: | 24 | export PYTHONPATH=./codegen 25 | pytest -s ./codegen/test 26 | -------------------------------------------------------------------------------- /algovivo/render/mm2d/shaders/TriangleShader.js: -------------------------------------------------------------------------------- 1 | class TriangleShader { 2 | constructor() { 3 | } 4 | 5 | renderTriangle(args = {}) { 6 | const ctx = args.ctx; 7 | const a = args.a; 8 | const b = args.b; 9 | const c = args.c; 10 | 11 | ctx.save(); 12 | ctx.beginPath(); 13 | ctx.strokeStyle = "black"; 14 | ctx.moveTo(a[0], a[1]); 15 | ctx.lineTo(b[0], b[1]); 16 | ctx.lineTo(c[0], c[1]); 17 | ctx.closePath(); 18 | ctx.stroke(); 19 | ctx.restore(); 20 | } 21 | } 22 | 23 | module.exports = TriangleShader; -------------------------------------------------------------------------------- /utils/py/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "algovivo" 7 | dynamic = ["version"] 8 | dependencies = [ 9 | "torch>=2.7.0", 10 | "numpy>=1.24.1", 11 | ] 12 | 13 | [project.optional-dependencies] 14 | dev = [ 15 | "pytest>=8.3.3", 16 | ] 17 | 18 | [tool.setuptools] 19 | packages = ["algovivo"] 20 | 21 | [tool.setuptools_scm] 22 | root = "../.." 23 | write_to = "utils/py/algovivo/_version.py" 24 | local_scheme = "node-and-timestamp" -------------------------------------------------------------------------------- /codegen/algovivo_codegen/potentials/friction.py: -------------------------------------------------------------------------------- 1 | class Friction: 2 | def __init__(self): 3 | pass 4 | 5 | def add_to_loss(self, be): 6 | be.loss_body += """ 7 | for (int i = 0; i < num_vertices; i++) { 8 | const auto offset = space_dim * i; 9 | 10 | const auto px = pos[offset ]; 11 | const auto py = pos[offset + 1]; 12 | 13 | const auto p0x = pos0[offset ]; 14 | const auto p0y = pos0[offset + 1]; 15 | 16 | accumulate_friction_energy( 17 | potential_energy, 18 | px, 19 | p0x, p0y, 20 | h, 21 | k_friction 22 | ); 23 | } 24 | """ -------------------------------------------------------------------------------- /algovivo/mmgrten/nn/ReLU.js: -------------------------------------------------------------------------------- 1 | const Module = require("./Module"); 2 | 3 | class ReLU extends Module { 4 | constructor(nn) { 5 | super(); 6 | this.nn = nn; 7 | this.output = null; 8 | } 9 | 10 | forward(x) { 11 | const ten = this.nn.engine; 12 | // TODO check shape consistency if input has different shape 13 | if (this.output == null) { 14 | this.output = ten.zerosLike(x); 15 | } 16 | ten.functional.relu(x, this.output); 17 | return this.output; 18 | } 19 | 20 | dispose() { 21 | if (this.output != null) this.output.dispose(); 22 | } 23 | } 24 | 25 | module.exports = ReLU; -------------------------------------------------------------------------------- /algovivo/mmgrten/nn/Tanh.js: -------------------------------------------------------------------------------- 1 | const Module = require("./Module"); 2 | 3 | class Tanh extends Module { 4 | constructor(nn) { 5 | super(); 6 | this.nn = nn; 7 | this.output = null; 8 | } 9 | 10 | forward(x) { 11 | const ten = this.nn.engine; 12 | // TODO check shape consistency if input has different shape 13 | if (this.output == null) { 14 | this.output = ten.zerosLike(x); 15 | } 16 | ten.functional.tanh(x, this.output); 17 | return this.output; 18 | } 19 | 20 | dispose() { 21 | if (this.output != null) this.output.dispose(); 22 | } 23 | } 24 | 25 | module.exports = Tanh; -------------------------------------------------------------------------------- /utils/trajectory/pathutils/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const fsp = fs.promises; 3 | 4 | function fileExists(filename) { 5 | return new Promise((resolve, reject) => { 6 | fs.access(filename, fs.constants.F_OK, (err) => { 7 | if (err) resolve(false); 8 | else resolve(true); 9 | }); 10 | }); 11 | } 12 | 13 | async function cleandir(dirname) { 14 | if (!await fileExists(dirname)) { 15 | await fsp.mkdir(dirname); 16 | } else { 17 | fs.rmSync(dirname, { recursive: true }); 18 | await fsp.mkdir(dirname); 19 | } 20 | } 21 | 22 | module.exports = { 23 | fileExists, 24 | cleandir 25 | }; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/codegen/fun.py: -------------------------------------------------------------------------------- 1 | from .args import Args 2 | from .backward_pass_fun import BackwardPassFun 3 | 4 | class Fun: 5 | def __init__(self, name): 6 | self.args = Args() 7 | self.name = name 8 | self.src_body = "" 9 | 10 | @property 11 | def num_args(self): 12 | return len(self.args) 13 | 14 | def codegen(self): 15 | src_fun_signature = self.args.codegen_fun_signature() 16 | return f"""extern \"C\" 17 | float {self.name}({src_fun_signature}) {{ 18 | {self.src_body} 19 | }}""" 20 | 21 | def make_backward_pass(self): 22 | return BackwardPassFun(self) -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "build": "rollup -c rollup.config.js", 5 | "watch": "rollup -c rollup.config.js --watch", 6 | "test": "export NODE_PATH=. && jest" 7 | }, 8 | "devDependencies": { 9 | "@babel/core": "^7.23.2", 10 | "@babel/plugin-transform-runtime": "^7.22.10", 11 | "@babel/preset-env": "^7.22.10", 12 | "@rollup/plugin-terser": "^0.4.1", 13 | "express": "^4.21.1", 14 | "jest": "^29.5.0", 15 | "puppeteer": "^22.15.0", 16 | "rollup": "^3.29.5" 17 | }, 18 | "jest": { 19 | "transform": { 20 | "^.+\\.(js|mjs)$": "babel-jest" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /algovivo/mmgrten/nn/index.js: -------------------------------------------------------------------------------- 1 | const Sequential = require("./Sequential"); 2 | const Linear = require("./Linear"); 3 | const ReLU = require("./ReLU"); 4 | const Tanh = require("./Tanh"); 5 | 6 | class nn { 7 | constructor(args = {}) { 8 | this.engine = args.engine; 9 | } 10 | 11 | Linear(inputSize, outputSize) { 12 | return new Linear(this, inputSize, outputSize); 13 | } 14 | 15 | ReLU() { 16 | return new ReLU(this); 17 | } 18 | 19 | Tanh() { 20 | return new Tanh(this); 21 | } 22 | 23 | Sequential() { 24 | const layers = Array.from(arguments); 25 | return new Sequential(this, layers); 26 | } 27 | } 28 | 29 | module.exports = nn; -------------------------------------------------------------------------------- /test/render/mm2d/grid.test.js: -------------------------------------------------------------------------------- 1 | const mm2d = require("algovivo").mm2d; 2 | 3 | test("grid", () => { 4 | const scene = new mm2d.Scene(); 5 | const grid = new mm2d.background.Grid({ 6 | scene: scene, 7 | rows: 1, 8 | cols: 1, 9 | innerCells: 1 10 | }); 11 | expect(grid.numLines).toBe(4); 12 | expect(grid.numVertices).toBe(8); 13 | 14 | grid.set({ 15 | rows: 1, 16 | cols: 1, 17 | innerCells: 1 18 | }); 19 | expect(grid.numLines).toBe(4); 20 | expect(grid.numVertices).toBe(8); 21 | 22 | grid.set({ 23 | rows: 2, 24 | cols: 3, 25 | innerCells: 2 26 | }); 27 | expect(grid.numLines).toBe(12); 28 | expect(grid.numVertices).toBe(24); 29 | }); -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/ReservedSlot.js: -------------------------------------------------------------------------------- 1 | const Slot = require("./Slot"); 2 | 3 | class ReservedSlot extends Slot { 4 | constructor(args = {}) { 5 | super(args); 6 | } 7 | 8 | isFree() { 9 | return false; 10 | } 11 | 12 | free() { 13 | let freeSlot = this.appendFree(this.ptr, this.size); 14 | this.remove(); 15 | 16 | const prev = freeSlot.prev(); 17 | const next = freeSlot.next(); 18 | 19 | if (prev != null && prev.isFree()) { 20 | freeSlot = prev.merge(freeSlot); 21 | } 22 | if (next != null && next.isFree()) { 23 | freeSlot = freeSlot.merge(next); 24 | } 25 | 26 | return freeSlot; 27 | } 28 | } 29 | 30 | module.exports = ReservedSlot; -------------------------------------------------------------------------------- /enzyme/Dockerfile-llvm11: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y \ 7 | llvm-11 clang-11 lld-11 \ 8 | ninja-build build-essential cmake \ 9 | git \ 10 | zlib1g-dev && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | RUN git clone https://github.com/EnzymeAD/Enzyme.git /Enzyme && \ 14 | cd /Enzyme && \ 15 | git checkout 86197cb2d776d72e2063695be21b729f6cffeb9b 16 | 17 | RUN cd /Enzyme/enzyme && \ 18 | mkdir build && \ 19 | cd build && \ 20 | cmake -G Ninja .. -DLLVM_DIR=/usr/lib/llvm-11/ && \ 21 | ninja 22 | 23 | ENV ENZYME=/Enzyme/enzyme/build/Enzyme/LLVMEnzyme-11.so 24 | ENV LLVM_BIN_DIR=/usr/lib/llvm-11/bin -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/dynamics/inertia.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace algovivo { 4 | 5 | __attribute__((always_inline)) 6 | void accumulate_inertial_energy( 7 | float &energy, 8 | int i, 9 | const float* pos, 10 | const float* vel0, 11 | const float* pos0, 12 | const float h, 13 | float m, 14 | int space_dim 15 | ) { 16 | float dy2 = 0; 17 | for (int j = 0; j < space_dim; j++) { 18 | const auto offset = space_dim * i; 19 | const auto p0j = pos0[offset + j]; 20 | const auto v0j = vel0[offset + j]; 21 | const auto yj = p0j + h * v0j; 22 | const auto pj = pos[offset + j]; 23 | const auto dj = pj - yj; 24 | dy2 += dj * dj; 25 | } 26 | energy += m * dy2; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /test/ten/intTuple.test.js: -------------------------------------------------------------------------------- 1 | const utils = require("../utils"); 2 | 3 | test("get/set", async () => { 4 | const ten = await utils.loadTen(); 5 | const a = ten.intTuple([1, 4, 8]); 6 | expect(a.get(0)).toBe(1); 7 | expect(a.get(1)).toBe(4); 8 | expect(a.get(2)).toBe(8); 9 | expect(a.toString()).toBe("1,4,8"); 10 | a.set(1, 6); 11 | expect(a.get(1)).toBe(6); 12 | expect(a.toString()).toBe("1,6,8"); 13 | }); 14 | 15 | test("flatten idx", async () => { 16 | const ten = await utils.loadTen(); 17 | const order = 1; 18 | const idx = ten.intTuple([1]); 19 | const stride = ten.intTuple([1]); 20 | const flatIdx = ten.wasmInstance.exports.flatten_idx(order, idx.slot.ptr, stride.slot.ptr); 21 | expect(flatIdx).toBe(1); 22 | }); -------------------------------------------------------------------------------- /test/ten/nn/sequential.test.js: -------------------------------------------------------------------------------- 1 | const utils = require("../../utils"); 2 | 3 | test("sequential", async () => { 4 | const ten = await utils.loadTen(); 5 | const nn = ten.nn; 6 | const mgr = ten.mgr; 7 | 8 | const bytes0 = ten.mgr.numReservedBytes(); 9 | 10 | const inputSize = 3; 11 | const outputSize = 2; 12 | 13 | const model = nn.Sequential( 14 | nn.Linear(inputSize, 32), 15 | nn.ReLU(), 16 | nn.Linear(32, outputSize), 17 | nn.Tanh() 18 | ); 19 | 20 | const input = ten.tensor([ 21 | 2, 22 | 4, 23 | 3 24 | ]); 25 | 26 | const output = model.forward(input); 27 | expect(output.toArray().length).toBe(2); 28 | 29 | expect(mgr.numReservedBytes()).not.toBe(bytes0); 30 | model.dispose(); 31 | input.dispose(); 32 | expect(mgr.numReservedBytes()).toBe(bytes0); 33 | }); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algovivo", 3 | "author": { 4 | "name": "Junior Rojas" 5 | }, 6 | "main": "./algovivo/index.js", 7 | "module": "./build/algovivo.mjs", 8 | "exports": { 9 | "import": "./build/algovivo.mjs", 10 | "require": "./algovivo/index.js" 11 | }, 12 | "scripts": { 13 | "build": "rollup -c rollup.config.js", 14 | "watch": "rollup -c rollup.config.js --watch", 15 | "test": "export NODE_PATH=. && jest" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.23.2", 19 | "@rollup/plugin-commonjs": "^24.1.0", 20 | "@rollup/plugin-node-resolve": "^15.0.2", 21 | "@rollup/plugin-terser": "^0.4.1", 22 | "argparse": "^2.0.1", 23 | "express": "^4.21.1", 24 | "jest": "^29.5.0", 25 | "puppeteer": "^22.15.0", 26 | "rollup": "^2.79.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mat2x2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define mat2x2_det(m) ((m##00) * (m##11) - (m##01) * (m##10)) 4 | 5 | #define mat2x2_pow2_sum(m) ((m##00) * (m##00) + (m##01) * (m##01) + (m##10) * (m##10) + (m##11) * (m##11)) 6 | 7 | #define mat2x2_inv(inv, m) \ 8 | const auto (m##_det) = mat2x2_det(m); \ 9 | const auto (inv##00) = (m##11) / (m##_det); \ 10 | const auto (inv##01) = (-m##01) / (m##_det); \ 11 | const auto (inv##10) = (-m##10) / (m##_det); \ 12 | const auto (inv##11) = (m##00) / (m##_det); 13 | 14 | #define mat2x2_mm(c00, c01, c10, c11, a00, a01, a10, a11, b00, b01, b10, b11) \ 15 | const auto (c00) = (a00) * (b00) + (a01) * (b10); \ 16 | const auto (c01) = (a00) * (b01) + (a01) * (b11); \ 17 | const auto (c10) = (a10) * (b00) + (a11) * (b10); \ 18 | const auto (c11) = (a10) * (b01) + (a11) * (b11); -------------------------------------------------------------------------------- /algovivo/mmgrten/nn/Linear.js: -------------------------------------------------------------------------------- 1 | const Module = require("./Module"); 2 | 3 | class Linear extends Module { 4 | constructor(nn, inputSize, outputSize) { 5 | super(); 6 | this.nn = nn; 7 | this.inputSize = inputSize; 8 | this.outputSize = outputSize; 9 | 10 | const ten = this.nn.engine; 11 | 12 | this.weight = ten.zeros([outputSize, inputSize]); 13 | this.bias = ten.zeros([outputSize]); 14 | 15 | this.output = ten.zeros([outputSize]); 16 | } 17 | 18 | forward(x) { 19 | const F = this.nn.engine.functional; 20 | F.matvec(this.weight, x, this.output); 21 | F.add(this.output, this.bias, this.output); 22 | return this.output; 23 | } 24 | 25 | dispose() { 26 | this.weight.dispose(); 27 | this.bias.dispose(); 28 | this.output.dispose(); 29 | } 30 | } 31 | 32 | module.exports = Linear; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/potentials/gravity.py: -------------------------------------------------------------------------------- 1 | from .. import Fun 2 | 3 | class Gravity: 4 | def __init__(self): 5 | pass 6 | 7 | def get_src(self): 8 | return """ 9 | for (int i = 0; i < num_vertices; i++) { 10 | const auto offset = space_dim * i; 11 | 12 | const auto px = pos[offset ]; 13 | const auto py = pos[offset + 1]; 14 | 15 | accumulate_gravity_energy( 16 | potential_energy, 17 | py, 18 | vertex_mass, 19 | g 20 | ); 21 | } 22 | """ 23 | 24 | def make_loss_fn(self, name): 25 | f = Fun(name) 26 | f.args.add_arg("int", "space_dim") 27 | f.args.add_arg("float", "g") 28 | f.args.add_arg("int", "num_vertices") 29 | f.src_body = self.get_src() 30 | return f 31 | 32 | def add_to_loss(self, be): 33 | be.loss_body += self.get_src() -------------------------------------------------------------------------------- /test/ten/nn/linear.test.js: -------------------------------------------------------------------------------- 1 | const utils = require("../../utils"); 2 | 3 | test("linear", async () => { 4 | const ten = await utils.loadTen(); 5 | const mgr = ten.mgr; 6 | 7 | const bytes0 = ten.mgr.numReservedBytes(); 8 | 9 | const inputSize = 3; 10 | const outputSize = 2; 11 | const linear = ten.nn.Linear(inputSize, outputSize); 12 | linear.weight.set([ 13 | [1, 2, 5], 14 | [6, 7, 9] 15 | ]); 16 | linear.bias.set([ 17 | 3, 18 | 5 19 | ]); 20 | const input = ten.tensor([ 21 | 2, 22 | 4, 23 | 3 24 | ]); 25 | 26 | const output = linear.forward(input); 27 | expect(output.toArray()).toEqual([ 28 | 28, 29 | 72 30 | ]); 31 | 32 | expect(mgr.numReservedBytes()).not.toBe(bytes0); 33 | linear.dispose(); 34 | input.dispose(); 35 | expect(mgr.numReservedBytes()).toBe(bytes0); 36 | }); -------------------------------------------------------------------------------- /utils/trajectory/FrameRecorder.js: -------------------------------------------------------------------------------- 1 | const pathutils = require("./pathutils"); 2 | const path = require("path"); 3 | 4 | class FrameRecorder { 5 | constructor(args = {}) { 6 | if (args.framesDirname == null) throw new Error("framesDirname required"); 7 | this.framesDirname = args.framesDirname; 8 | this.initialized = false; 9 | this.nextFrameId = 0; 10 | } 11 | 12 | async saveFrame(window) { 13 | if (window == null) throw new Error("window required"); 14 | 15 | if (!this.initialized) { 16 | await pathutils.cleandir(this.framesDirname); 17 | this.initialized = true; 18 | } 19 | 20 | const frameFilename = path.join(this.framesDirname, `${this.nextFrameId}.png`); 21 | await window.screenshot({ path: frameFilename }); 22 | this.nextFrameId++; 23 | } 24 | } 25 | 26 | module.exports = FrameRecorder; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/arr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace algovivo { 4 | 5 | typedef unsigned int size_t; 6 | 7 | extern "C" 8 | void* memcpy(void* dst, const void* src, size_t n) { 9 | char* d = static_cast(dst); 10 | const char* s = static_cast(src); 11 | for (size_t i = 0; i < n; ++i) { 12 | d[i] = s[i]; 13 | } 14 | return dst; 15 | } 16 | 17 | void zero_(int n, float* data) { 18 | for (int i = 0; i < n; i++) data[i] = 0.0; 19 | } 20 | 21 | void copy_(int n, const float* src, float* dst) { 22 | for (int i = 0; i < n; i++) dst[i] = src[i]; 23 | } 24 | 25 | // d = a + b * c 26 | void add_scaled(int n, const float* a, const float* b, const float c, float* d) { 27 | for (int i = 0; i < n; i++) d[i] = a[i] + b[i] * c; 28 | } 29 | 30 | void scale_(int n, float* a, float c) { 31 | for (int i = 0; i < n; i++) a[i] *= c; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/matvec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace mmgrten { 4 | 5 | extern "C" 6 | void matvec( 7 | int a_rows, 8 | int a_cols, 9 | 10 | const int* a_stride, const float* a_data, 11 | const int* b_stride, const float* b_data, 12 | const int* c_stride, float* c_data 13 | ) { 14 | for (int i = 0; i < a_rows; i++) { 15 | float s = 0.0; 16 | for (int k = 0; k < a_cols; k++) { 17 | int a_idx[2] = {i, k}; 18 | auto aik = get_tensor_elem( 19 | 2, a_stride, a_data, 20 | a_idx 21 | ); 22 | 23 | int b_idx[1] = {k}; 24 | auto bkj = get_tensor_elem( 25 | 1, b_stride, b_data, 26 | b_idx 27 | ); 28 | 29 | s += aik * bkj; 30 | } 31 | 32 | int c_idx[1] = {i}; 33 | set_tensor_elem( 34 | 1, c_stride, c_data, 35 | c_idx, s 36 | ); 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /utils/trajectory/Trajectory.js: -------------------------------------------------------------------------------- 1 | const fsp = require("fs").promises; 2 | const path = require("path"); 3 | 4 | async function getNumFilesWithExtension(dirname, ext = ".json") { 5 | try { 6 | const files = await fsp.readdir(dirname); 7 | const filenames = files.filter(file => path.extname(file).toLowerCase() === ext); 8 | return filenames.length; 9 | } catch (err) { 10 | throw new Error(`Error reading directory ${err}`); 11 | } 12 | } 13 | 14 | class Trajectory { 15 | constructor(dirname) { 16 | this.dirname = dirname; 17 | } 18 | 19 | async numSteps() { 20 | return await getNumFilesWithExtension(this.dirname, ".json"); 21 | } 22 | 23 | async loadStep(step) { 24 | const filename = path.join(this.dirname, `${step}.json`); 25 | const data = JSON.parse(await fsp.readFile(filename)); 26 | return data; 27 | } 28 | } 29 | 30 | module.exports = Trajectory; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/tensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace mmgrten { 4 | 5 | extern "C" 6 | int flatten_idx( 7 | int order, 8 | const int* idx, 9 | const int* stride 10 | ) { 11 | int flat_idx = 0; 12 | for (int i = 0; i < order; i++) { 13 | flat_idx += stride[i] * idx[i]; 14 | } 15 | return flat_idx; 16 | } 17 | 18 | extern "C" 19 | float get_tensor_elem( 20 | int order, 21 | const int* stride, 22 | const float* data, 23 | const int* idx 24 | ) { 25 | int flat_idx = flatten_idx(order, idx, stride); 26 | return data[flat_idx]; 27 | } 28 | 29 | extern "C" 30 | void set_tensor_elem( 31 | int order, 32 | const int* stride, 33 | float* data, 34 | const int* idx, 35 | float value 36 | ) { 37 | int flat_idx = flatten_idx(order, idx, stride); 38 | data[flat_idx] = value; 39 | } 40 | 41 | extern "C" 42 | void zero_(int n, float* data) { 43 | for (int i = 0; i < n; i++) { 44 | data[i] = 0.0; 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/codegen/backward_pass_fun.py: -------------------------------------------------------------------------------- 1 | from .utils import indent 2 | 3 | class BackwardPassFun: 4 | def __init__(self, primal): 5 | self.primal = primal 6 | self.args = self.primal.args.with_tangent_args() 7 | 8 | @property 9 | def name(self): 10 | return f"{self.primal.name}_grad" 11 | 12 | def codegen(self): 13 | grad_args = self.args 14 | src_grad_fun_signature = grad_args.codegen_fun_signature() 15 | 16 | src_grad_fun_body = self.codegen_body() 17 | 18 | return f"""extern \"C\" 19 | void {self.name}({src_grad_fun_signature}) {{ 20 | {src_grad_fun_body} 21 | }}""" 22 | 23 | def codegen_body(self): 24 | primal_args = self.primal.args 25 | 26 | enzyme_args_call = primal_args.codegen_enzyme_call() 27 | src_grad_fun_body = f"""__enzyme_autodiff( 28 | {indent(self.primal.name)}, 29 | {indent(enzyme_args_call)} 30 | );""" 31 | 32 | return src_grad_fun_body -------------------------------------------------------------------------------- /algovivo/render/mm2d/background/Background.js: -------------------------------------------------------------------------------- 1 | class Background { 2 | constructor(args = {}) { 3 | if (args.scene == null) { 4 | throw new Error("scene required"); 5 | } 6 | const mesh = this.mesh = args.scene.addMesh(); 7 | mesh.pos = [[0, 0]]; 8 | 9 | const color1 = (args.color1 == null) ? "#fcfcfc" : args.color1; 10 | const color2 = (args.color2 == null) ? "#d7d8d8" : args.color2; 11 | mesh.pointShader.renderPoint = (args = {}) => { 12 | const width = args.renderer.width; 13 | const height = args.renderer.height; 14 | const ctx = args.ctx; 15 | 16 | const grd = ctx.createRadialGradient( 17 | width * 0.5, height * 0.5, width * 0.05, 18 | width * 0.5, height * 0.5, width * 0.5 19 | ); 20 | grd.addColorStop(0, color1); 21 | grd.addColorStop(1, color2); 22 | ctx.fillStyle = grd; 23 | ctx.fillRect(0, 0, width, height); 24 | } 25 | } 26 | } 27 | 28 | module.exports = Background; -------------------------------------------------------------------------------- /algovivo/render/mm2d/math/Transform2d.js: -------------------------------------------------------------------------------- 1 | const Matrix2x2 = require("./Matrix2x2"); 2 | const Vec2 = require("./Vec2"); 3 | 4 | class Transform2d { 5 | constructor() { 6 | this.translation = [0, 0]; 7 | this.linear = new Matrix2x2( 8 | 1, 0, 9 | 0, 1 10 | ); 11 | } 12 | 13 | inferScale() { 14 | const sx = this.linear.m00; 15 | return sx; 16 | } 17 | 18 | apply(v) { 19 | return Vec2.add(this.linear.apply(v), this.translation); 20 | } 21 | 22 | inv() { 23 | const inv = new Transform2d(); 24 | inv.linear = this.linear.inv(); 25 | inv.translation = inv.linear.negate().apply(this.translation); 26 | return inv; 27 | } 28 | 29 | toColumnMajorArray() { 30 | return [ 31 | this.linear.get(0, 0), 32 | this.linear.get(1, 0), 33 | this.linear.get(0, 1), 34 | this.linear.get(1, 1), 35 | this.translation[0], 36 | this.translation[1], 37 | ]; 38 | } 39 | } 40 | 41 | module.exports = Transform2d; -------------------------------------------------------------------------------- /demo/src/AgentSystem.js: -------------------------------------------------------------------------------- 1 | export default class AgentSystem { 2 | constructor(args = {}) { 3 | if (args.algovivo == null) throw new Error("algovivo required"); 4 | this.algovivo = args.algovivo; 5 | 6 | if (args.system == null) throw new Error("system required"); 7 | this.system = args.system; 8 | 9 | this.policy = null; 10 | } 11 | 12 | get numVertices() { 13 | return this.system.numVertices; 14 | } 15 | 16 | set(args = {}) { 17 | const mesh = args.mesh; 18 | const policy = args.policy; 19 | 20 | if (mesh == null) throw new Error("mesh required"); 21 | 22 | this.dispose(); 23 | 24 | this.system.set(mesh); 25 | if (policy != null) { 26 | this.policy = new this.algovivo.nn.NeuralFramePolicy({ system: this.system }); 27 | this.policy.loadData(policy); 28 | } 29 | } 30 | 31 | dispose() { 32 | if (this.policy != null) { 33 | this.policy.dispose(); 34 | this.policy = null; 35 | } 36 | this.system.dispose(); 37 | } 38 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/mmgrten/mm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "tensor.h" 3 | 4 | namespace mmgrten { 5 | 6 | extern "C" 7 | void mm( 8 | int a_rows, 9 | int a_cols, 10 | int b_cols, 11 | 12 | const int* a_stride, const float* a_data, 13 | const int* b_stride, const float* b_data, 14 | const int* c_stride, float* c_data 15 | ) { 16 | for (int i = 0; i < a_rows; i++) { 17 | for (int j = 0; j < b_cols; j++) { 18 | float s = 0.0; 19 | for (int k = 0; k < a_cols; k++) { 20 | int a_idx[2] = {i, k}; 21 | auto aik = get_tensor_elem( 22 | 2, a_stride, a_data, 23 | a_idx 24 | ); 25 | 26 | int b_idx[2] = {k, j}; 27 | auto bkj = get_tensor_elem( 28 | 2, b_stride, b_data, 29 | b_idx 30 | ); 31 | 32 | s += aik * bkj; 33 | } 34 | 35 | int c_idx[2] = {i, j}; 36 | set_tensor_elem( 37 | 2, c_stride, c_data, 38 | c_idx, s 39 | ); 40 | } 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /test/systemWithCustomWasmEnv.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | async function loadTen() { 7 | const ten = new algovivo.mmgrten.Engine(); 8 | const wasmInstance = await utils.loadWasm({ 9 | env: { 10 | malloc: (x) => { 11 | return ten.mgr.malloc(Number(x)); 12 | }, 13 | free: (x) => { 14 | return ten.mgr.free(Number(x)); 15 | } 16 | } 17 | }); 18 | ten.init({ wasmInstance }); 19 | return ten; 20 | } 21 | 22 | test("system", async () => { 23 | const ten = await loadTen(); 24 | const system = new algovivo.System({ ten }); 25 | expect(system.numVertices).toBe(0); 26 | expect(system.numMuscles).toBe(0); 27 | expect(system.numTriangles).toBe(0); 28 | 29 | system.set({ 30 | pos: [ 31 | [1, 2] 32 | ] 33 | }); 34 | 35 | expect(system.numVertices).toBe(1); 36 | expect(system.pos.toArray()).toBeCloseToArray([[1, 2]]); 37 | expect(system.vel.toArray()).toBeCloseToArray([[0, 0]]); 38 | }); -------------------------------------------------------------------------------- /codegen/codegen_csrc.py: -------------------------------------------------------------------------------- 1 | import algovivo_codegen as codegen 2 | import argparse 3 | 4 | if __name__ == "__main__": 5 | arg_parser = argparse.ArgumentParser() 6 | arg_parser.add_argument("-o", "--output-csrc-dirname", type=str, default="csrc") 7 | args = arg_parser.parse_args() 8 | 9 | backward_euler = codegen.BackwardEuler() 10 | 11 | backward_euler.modules = [ 12 | codegen.modules.Vertices(), 13 | codegen.modules.Muscles(), 14 | codegen.modules.Triangles(), 15 | codegen.modules.Friction(), 16 | codegen.modules.Collision() 17 | ] 18 | 19 | backward_euler.inertial_modules = [ 20 | codegen.modules.Vertices() 21 | ] 22 | 23 | backward_euler.potentials = [ 24 | codegen.potentials.Muscles(), 25 | codegen.potentials.Triangles(), 26 | codegen.potentials.Gravity(), 27 | codegen.potentials.Collision(), 28 | codegen.potentials.Friction() 29 | ] 30 | 31 | backward_euler.init_csrc(args.output_csrc_dirname) 32 | backward_euler.instantiate_templates(args.output_csrc_dirname) -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/FreeSlot.js: -------------------------------------------------------------------------------- 1 | const Slot = require("./Slot"); 2 | 3 | class FreeSlot extends Slot { 4 | constructor(args = {}) { 5 | super(args); 6 | } 7 | 8 | isFree() { 9 | return true; 10 | } 11 | 12 | reserve(bytes) { 13 | const availableBytes = this.numBytes(); 14 | if (bytes > availableBytes) { 15 | throw new Error(`cannot reserve ${bytes} bytes, only ${availableBytes} bytes are available`); 16 | } 17 | const reserved = this.appendReserved(this.ptr, bytes); 18 | reserved.appendFree(this.ptr + bytes, availableBytes - bytes); 19 | this.remove(); 20 | return reserved; 21 | } 22 | 23 | merge(slot) { 24 | if (!this.isFree() || !slot.isFree()) { 25 | throw new Error("only free slots can be merged"); 26 | } 27 | if (this.next() != slot) { 28 | throw new Error("only adjacent slots can be merged"); 29 | } 30 | const merged = slot.appendFree( 31 | this.ptr, 32 | this.size + slot.size 33 | ); 34 | this.remove(); 35 | slot.remove(); 36 | return merged; 37 | } 38 | } 39 | 40 | module.exports = FreeSlot; -------------------------------------------------------------------------------- /algovivo/mmgrten/TensorIterator.js: -------------------------------------------------------------------------------- 1 | const IntTuple = require("./IntTuple"); 2 | 3 | class TensorIterator { 4 | constructor(shape) { 5 | if (shape == null) throw new Error("shape required"); 6 | if (!(shape instanceof IntTuple)) { 7 | throw new Error(`IntTuple shape expected, found ${typeof shape}: shape`); 8 | } 9 | this.shape = shape; 10 | this.done = false; 11 | this.idx = []; 12 | shape.forEach((si) => { 13 | this.idx.push(0); 14 | }); 15 | } 16 | 17 | next() { 18 | const shape = this.shape; 19 | for (let _i = 0; _i < shape.length; _i++) { 20 | const i = shape.length - 1 - _i; 21 | if (this.idx[i] < shape.get(i) - 1) { 22 | this.idx[i]++; 23 | return; 24 | } else 25 | if (i == 0) { 26 | this.done = true; 27 | return; 28 | } else { 29 | this.idx[i] = 0; 30 | } 31 | } 32 | } 33 | 34 | static shapeForEach(shape, f) { 35 | const it = new TensorIterator(shape); 36 | while (!it.done) { 37 | f(it.idx); 38 | it.next(); 39 | } 40 | } 41 | } 42 | 43 | module.exports = TensorIterator; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Junior Rojas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/ten/clamp.test.js: -------------------------------------------------------------------------------- 1 | const { mmgrten } = require("algovivo"); 2 | const utils = require("../utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("clamp min max", async () => { 7 | const ten = await utils.loadTen(); 8 | 9 | const a = ten.tensor([ 10 | [1, -3, 0.9], 11 | [0.5, 0.2, 0.3] 12 | ]); 13 | 14 | a.clamp_({ min: 0.1, max: 0.4 }); 15 | expect(a.toArray()).toBeCloseToArray([ 16 | [0.4, 0.1, 0.4], 17 | [0.4, 0.2, 0.3] 18 | ]); 19 | }); 20 | 21 | test("clamp min", async () => { 22 | const ten = await utils.loadTen(); 23 | 24 | const a = ten.tensor([ 25 | [1, -3, 0.9], 26 | [0.5, 0.2, 0.3] 27 | ]); 28 | a.clamp_({ min: 0.1 }); 29 | expect(a.toArray()).toBeCloseToArray([ 30 | [1, 0.1, 0.9], 31 | [0.5, 0.2, 0.3] 32 | ]); 33 | }); 34 | 35 | test("clamp max", async () => { 36 | const ten = await utils.loadTen(); 37 | 38 | const a = ten.tensor([ 39 | [1, -3, 0.9], 40 | [0.5, 0.2, 0.3] 41 | ]); 42 | a.clamp_({ max: 0.4 }); 43 | expect(a.toArray()).toBeCloseToArray([ 44 | [0.4, -3, 0.4], 45 | [0.4, 0.2, 0.3] 46 | ]); 47 | }) -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/modules/muscles.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../vec2.h" 4 | 5 | namespace algovivo { 6 | 7 | __attribute__((always_inline)) 8 | void accumulate_muscle_energy( 9 | float &energy, 10 | const float* pos, 11 | int i1, int i2, 12 | float a, float l0, 13 | float k 14 | ) { 15 | vec2_get(p1, pos, i1); 16 | vec2_get(p2, pos, i2); 17 | 18 | vec2_sub(d, p1, p2); 19 | const auto q = dx * dx + dy * dy; 20 | const float l = __builtin_sqrt(q + 1e-6); 21 | const auto al0 = a * l0; 22 | const auto dl = (l - al0) / al0; 23 | energy += 0.5 * k * dl * dl; 24 | } 25 | 26 | extern "C" 27 | void l0_of_pos( 28 | int num_vertices, 29 | const float* pos, 30 | int num_edges, 31 | const int* indices, 32 | float* l0 33 | ) { 34 | for (int i = 0; i < num_edges; i++) { 35 | const auto offset = 2 * i; 36 | const auto i1 = indices[offset ]; 37 | const auto i2 = indices[offset + 1]; 38 | 39 | vec2_get(p1, pos, i1); 40 | vec2_get(p2, pos, i2); 41 | 42 | vec2_sub(d, p1, p2); 43 | const auto q = dx * dx + dy * dy; 44 | l0[i] = __builtin_sqrt(q); 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/math/AABB.js: -------------------------------------------------------------------------------- 1 | class AABB { 2 | constructor(args = {}) { 3 | if (args.x0 == null) throw new Error("x0 required"); 4 | if (args.y0 == null) throw new Error("y0 required"); 5 | this._x0 = args.x0; 6 | this._y0 = args.y0; 7 | 8 | let x1 = null; 9 | if (args.width != null) { 10 | x1 = this._x0 + args.width 11 | } else { 12 | if (args.x1 == null) throw new Error("x1 required"); 13 | x1 = args.x1; 14 | } 15 | this._x1 = x1; 16 | 17 | let y1 = null; 18 | if (args.height != null) { 19 | y1 = this._y0 + args.height 20 | } else { 21 | if (args.y1 == null) throw new Error("y1 required"); 22 | y1 = args.y1; 23 | } 24 | this._y1 = y1; 25 | } 26 | 27 | get x0() { return this._x0; } 28 | get x1() { return this._x1; } 29 | get y0() { return this._y0; } 30 | get y1() { return this._y1; } 31 | get width() { return this._x1 - this._x0; } 32 | get height() { return this._y1 - this._y0; } 33 | 34 | get center() { 35 | return [ 36 | (this.x0 + this.x1) * 0.5, 37 | (this.y0 + this.y1) * 0.5 38 | ]; 39 | } 40 | } 41 | 42 | module.exports = AABB; -------------------------------------------------------------------------------- /utils/trajectory/ppw/server.js: -------------------------------------------------------------------------------- 1 | const net = require("net"); 2 | const express = require("express"); 3 | 4 | function getFreePort() { 5 | return new Promise((resolve) => { 6 | const server = net.createServer(); 7 | server.listen(0, () => { 8 | const port = server.address().port; 9 | server.close(); 10 | resolve(port); 11 | }); 12 | }); 13 | } 14 | 15 | function runWebServer(args = {}) { 16 | const staticDirname = args.staticDirname; 17 | if (staticDirname == null) { 18 | throw new Error("staticDirname required"); 19 | } 20 | const onReady = args.onReady; 21 | if (onReady == null) { 22 | throw new Error("onReady required"); 23 | } 24 | return new Promise((resolve, reject) => { 25 | (async () => { 26 | const port = await getFreePort(); 27 | const app = express(); 28 | app.use(express.static(staticDirname)); 29 | const server = app.listen(port, async () => { 30 | try { 31 | await onReady(port); 32 | } catch(e) { 33 | reject(e); 34 | } finally { 35 | server.close(() => { 36 | resolve(); 37 | }); 38 | } 39 | }); 40 | })(); 41 | }); 42 | } 43 | 44 | module.exports = { 45 | getFreePort: getFreePort, 46 | runWebServer: runWebServer 47 | } -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/linked/Node.js: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(list, data) { 3 | this.list = list; 4 | this.data = data; 5 | this.next = null; 6 | this.prev = null; 7 | } 8 | 9 | append(data) { 10 | const node = new Node(this.list, data); 11 | 12 | if (this.list.last == this) this.list.last = node; 13 | 14 | node.next = this.next; 15 | node.prev = this; 16 | 17 | if (this.next != null) this.next.prev = node; 18 | 19 | this.next = node; 20 | this.list.size++; 21 | 22 | return node; 23 | } 24 | 25 | prepend(data) { 26 | const node = new Node(this.list, data); 27 | 28 | if (this.list.first == this) this.list.first = node; 29 | 30 | node.next = this; 31 | node.prev = this.prev; 32 | 33 | if (this.prev != null) this.prev.next = node; 34 | 35 | this.prev = node; 36 | this.list.size++; 37 | 38 | return node; 39 | } 40 | 41 | remove() { 42 | if (this.prev != null) this.prev.next = this.next; 43 | if (this.next != null) this.next.prev = this.prev; 44 | if (this.list.first == this) this.list.first = this.next; 45 | if (this.list.last == this) this.list.last = this.prev; 46 | this.list.size--; 47 | 48 | this.next = null; 49 | this.prev = null; 50 | } 51 | } 52 | 53 | module.exports = Node; -------------------------------------------------------------------------------- /algovivo/render/mm2d/math/Vec2.js: -------------------------------------------------------------------------------- 1 | function clone(a) { 2 | return [a[0], a[1]]; 3 | } 4 | 5 | function add(a, b) { 6 | return [ 7 | a[0] + b[0], 8 | a[1] + b[1] 9 | ]; 10 | } 11 | 12 | function add_(a, b) { 13 | a[0] += b[0]; 14 | a[1] += b[1]; 15 | } 16 | 17 | function mulScalar_(a, c) { 18 | a[0] *= c; 19 | a[1] *= c; 20 | } 21 | 22 | function mulScalar(a, c) { 23 | const ca = clone(a); 24 | mulScalar_(ca, c); 25 | return ca; 26 | } 27 | 28 | function sub(a, b) { 29 | return [ 30 | a[0] - b[0], 31 | a[1] - b[1] 32 | ]; 33 | } 34 | 35 | function quadrance(a) { 36 | return a[0] * a[0] + a[1] * a[1]; 37 | } 38 | 39 | function norm(a) { 40 | return Math.sqrt(quadrance(a)); 41 | } 42 | 43 | function normalize_(a) { 44 | const n = norm(a); 45 | if (n !== 0) { 46 | mulScalar_(a, 1 / n); 47 | } 48 | } 49 | 50 | function normalize(a) { 51 | const result = [...a]; 52 | normalize_(result); 53 | return result; 54 | } 55 | 56 | function dot(a, b) { 57 | return a[0] * b[0] + a[1] * b[1]; 58 | } 59 | 60 | module.exports = { 61 | clone: clone, 62 | add: add, 63 | add_: add_, 64 | mulScalar_: mulScalar_, 65 | mulScalar: mulScalar, 66 | sub: sub, 67 | quadrance: quadrance, 68 | norm: norm, 69 | normalize: normalize, 70 | normalize_: normalize_, 71 | dot: dot 72 | }; -------------------------------------------------------------------------------- /algovivo/render/mm2d/sorted/Simplices.js: -------------------------------------------------------------------------------- 1 | const Simplex = require("./Simplex"); 2 | 3 | function hashSimplex(vids) { 4 | vids.sort(); 5 | return vids.join("_"); 6 | } 7 | 8 | class Simplices { 9 | constructor(args = {}) { 10 | if (args.order == null) throw new Error("order required"); 11 | this.order = args.order; 12 | this.simplicesByHash = new Map(); 13 | } 14 | 15 | forEach(f) { 16 | this.simplicesByHash.forEach(f); 17 | } 18 | 19 | size() { 20 | return this.simplicesByHash.size; 21 | } 22 | 23 | has(simplex) { 24 | return this.simplicesByHash.has(hashSimplex(simplex.vertexIds)); 25 | } 26 | 27 | add(simplex, id) { 28 | let vertexIds = null; 29 | if (Array.isArray(simplex)) { 30 | if (id == null) throw new Error("id required"); 31 | vertexIds = simplex; 32 | simplex = new Simplex(id, vertexIds); 33 | } else { 34 | vertexIds = simplex.vertexIds; 35 | if (vertexIds == null) { 36 | throw new Error(`vertexIds required ${simplex}`); 37 | } 38 | id = simplex.id; 39 | } 40 | if (vertexIds.length != this.order) { 41 | throw new Error(`expected ${this.order} vertices, found ${vertexIds.length}`); 42 | } 43 | const h = hashSimplex(vertexIds); 44 | this.simplicesByHash.set(h, simplex); 45 | return simplex; 46 | } 47 | } 48 | 49 | module.exports = Simplices; -------------------------------------------------------------------------------- /demo/test/ppw/Window.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | export default class Window { 4 | constructor(args = {}) { 5 | this.width = args.width ?? 400; 6 | this.height = args.height ?? 400; 7 | this.headless = args.headless ?? false; 8 | this.deviceScaleFactor = args.deviceScaleFactor ?? 1; 9 | this.indexUrl = args.indexUrl; 10 | } 11 | 12 | async launch() { 13 | const width = this.width; 14 | const height = this.height; 15 | const deviceScaleFactor = this.deviceScaleFactor; 16 | const browser = this.browser = await puppeteer.launch({ 17 | headless: this.headless, 18 | args: [ 19 | `--window-size=${this.width},${this.height}`, 20 | "--no-sandbox" 21 | ] 22 | }); 23 | const page = await browser.newPage(); 24 | await page.setViewport({ width: width, height: height, deviceScaleFactor: deviceScaleFactor }); 25 | await page.goto(this.indexUrl); 26 | this.page = page; 27 | } 28 | 29 | async evaluate() { 30 | return await this.page.evaluate.apply(this.page, arguments); 31 | } 32 | 33 | async screenshot() { 34 | return await this.page.screenshot.apply(this.page, arguments); 35 | } 36 | 37 | async waitForNetworkIdle() { 38 | return await this.page.waitForNetworkIdle.apply(this.page, arguments); 39 | } 40 | 41 | async close() { 42 | await this.browser.close(); 43 | } 44 | } -------------------------------------------------------------------------------- /demo/src/Footer.js: -------------------------------------------------------------------------------- 1 | import { makeGitHubLink } from "./ui.js"; 2 | 3 | export default class Footer { 4 | constructor() { 5 | const div = document.createElement("div"); 6 | div.style.paddingTop = "15px"; 7 | div.style.paddingBottom = "15px"; 8 | 9 | this.domElement = div; 10 | 11 | div.innerHTML = ``; 12 | } 13 | } -------------------------------------------------------------------------------- /utils/py/test/trajectory/test_trajectory.py: -------------------------------------------------------------------------------- 1 | import algovivo 2 | import shutil 3 | import json 4 | from pathlib import Path 5 | import os 6 | this_filepath = Path(os.path.realpath(__file__)) 7 | this_dirpath = this_filepath.parent 8 | 9 | if __name__ == "__main__": 10 | native_instance = algovivo.NativeInstance.load( 11 | str(this_dirpath.parent.parent.parent.parent.joinpath("build", "algovivo.so")) 12 | ) 13 | 14 | system = algovivo.System(native_instance) 15 | print(system.num_vertices) 16 | 17 | with open(this_dirpath.joinpath("data", "mesh.json"), "r") as f: 18 | data = json.load(f) 19 | 20 | system.vertices.set(pos=data["pos"]) 21 | system.muscles.set( 22 | indices=data["muscles"], 23 | pos=data["pos"], 24 | l0=data["l0"] 25 | ) 26 | system.triangles.set( 27 | indices=data["triangles"], 28 | pos=data["pos"], 29 | rsi=data["rsi"] 30 | ) 31 | 32 | seq_dirname = "steps.out" 33 | shutil.rmtree(seq_dirname, ignore_errors=True) 34 | os.makedirs(seq_dirname, exist_ok=True) 35 | 36 | for i in range(100): 37 | print(i) 38 | system.step() 39 | 40 | filename = os.path.join(seq_dirname, f"{i}.json") 41 | with open(filename, "w") as f: 42 | json.dump({ 43 | "pos0": system.vertices.pos.tolist(), 44 | "a0": system.muscles.a.tolist() 45 | }, f) -------------------------------------------------------------------------------- /demo/src/ui.js: -------------------------------------------------------------------------------- 1 | export function initStyle() { 2 | document.body.style.background = "rgb(248, 248, 248)"; 3 | document.body.style.display = "flex"; 4 | document.body.style.flexDirection = "column"; 5 | } 6 | 7 | export function makeGitHubLink() { 8 | const a = document.createElement("a"); 9 | a.href = "https://github.com/juniorrojas/algovivo"; 10 | a.innerHTML = ``; 11 | return a; 12 | } -------------------------------------------------------------------------------- /test/render/viewport.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("../utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("hit test", async () => { 7 | const wasmInstance = await utils.loadWasm(); 8 | const system = new algovivo.System({ wasmInstance }); 9 | 10 | system.set({ 11 | pos: [ 12 | [0, 0], 13 | [1, 2] 14 | ] 15 | }); 16 | 17 | const viewport = new algovivo.SystemViewport({ system, headless: true }); 18 | expect(viewport.hitTestVertex([0, 0])).toBe(0); 19 | expect(viewport.hitTestVertex([1, 2])).toBe(1); 20 | expect(viewport.hitTestVertex([10, 20])).toBeNull(); 21 | expect(viewport.hitTestVertex([1, 2], 100)).toBe(1); 22 | }); 23 | 24 | test("fix vertex", async () => { 25 | const wasmInstance = await utils.loadWasm(); 26 | const system = new algovivo.System({ wasmInstance }); 27 | 28 | system.set({ 29 | pos: [ 30 | [5, 6], 31 | [11, 12] 32 | ] 33 | }); 34 | 35 | system.vel.set([ 36 | [1, 2], 37 | [3, 4] 38 | ]); 39 | 40 | const viewport = new algovivo.SystemViewport({ system, headless: true }); 41 | expect(system.pos.toArray()).toBeCloseToArray([[5, 6], [11, 12]]); 42 | expect(system.vel.toArray()).toBeCloseToArray([[1, 2], [3, 4]]); 43 | viewport.fixVertex(1); 44 | expect(system.pos.toArray()).toBeCloseToArray([[5, 6], [11, 12]]); 45 | expect(system.vel.toArray()).toBeCloseToArray([[1, 2], [0, 0]]); 46 | }); -------------------------------------------------------------------------------- /demo/test/basic.test.js: -------------------------------------------------------------------------------- 1 | import ppw from "./ppw/index.js"; 2 | 3 | test("main", async () => { 4 | const main = async (port) => { 5 | const window = new ppw.Window({ 6 | headless: true, 7 | indexUrl: `http://localhost:${port}`, 8 | width: 800, 9 | height: 1200 10 | }); 11 | try { 12 | await window.launch(); 13 | const numVertices = await window.evaluate(async () => { 14 | function waitInit() { 15 | return new Promise((resolve, reject) => { 16 | const timeout = setTimeout(() => { 17 | clearInterval(interval); 18 | reject(new Error("Timeout waiting for system initialization")); 19 | }, 5000); 20 | 21 | const interval = setInterval(() => { 22 | if (window.system != null) { 23 | clearInterval(interval); 24 | clearTimeout(timeout); 25 | resolve(); 26 | } 27 | }, 50); 28 | }); 29 | } 30 | await waitInit(); 31 | return system.numVertices; 32 | }); 33 | expect(numVertices).toBe(28); 34 | 35 | await window.waitForNetworkIdle(); 36 | 37 | await window.screenshot({ path: `${__dirname}/screenshot.out.png` }); 38 | } finally { 39 | await window.close(); 40 | } 41 | } 42 | 43 | await ppw.runWebServer({ 44 | staticDirname: `${__dirname}/../public`, 45 | onReady: main 46 | }); 47 | }, 10000); -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import terser from "@rollup/plugin-terser"; 4 | import { execSync } from "child_process"; 5 | 6 | function header() { 7 | return { 8 | renderChunk(code) { 9 | const commitSha = execSync("git rev-parse HEAD").toString().trim(); 10 | const buildInfo = `Built from commit ${commitSha}`; 11 | return `/** 12 | * algovivo 13 | * (c) 2023 Junior Rojas 14 | * License: MIT 15 | * 16 | * ${buildInfo} 17 | */ 18 | ${code}`; 19 | } 20 | }; 21 | } 22 | 23 | const configs = []; 24 | 25 | for (let minified of [true, false]) { 26 | configs.push({ 27 | input: "algovivo/index.js", 28 | output: { 29 | file: `build/algovivo${minified ? ".min": ""}.js`, 30 | format: "umd", 31 | name: "algovivo", 32 | sourcemap: false 33 | }, 34 | plugins: [ 35 | resolve(), 36 | commonjs(), 37 | ...(minified ? [terser()] : []), 38 | header(), 39 | ] 40 | }); 41 | } 42 | 43 | for (let minified of [true, false]) { 44 | configs.push({ 45 | input: "algovivo/index.js", 46 | output: { 47 | file: `build/algovivo${minified ? ".min": ""}.mjs`, 48 | format: "esm", 49 | sourcemap: false 50 | }, 51 | plugins: [ 52 | resolve(), 53 | commonjs(), 54 | ...(minified ? [terser()] : []), 55 | header() 56 | ] 57 | }); 58 | } 59 | 60 | export default configs; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/templates/backward_euler.template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../arr.h" 4 | 5 | #include "../vec2.h" 6 | 7 | #include "../dynamics/inertia.h" 8 | 9 | #include "../modules/muscles.h" 10 | #include "../modules/triangles.h" 11 | #include "../modules/friction.h" 12 | #include "../modules/collision.h" 13 | #include "../modules/gravity.h" 14 | 15 | #include "optim.h" 16 | 17 | // {{includes}} 18 | 19 | namespace algovivo { 20 | 21 | float backward_euler_loss( 22 | // {{backward_euler_loss_args}} 23 | ) { 24 | // {{backward_euler_loss_body}} 25 | } 26 | 27 | static void backward_euler_loss_grad( 28 | // {{backward_euler_loss_grad_args}} 29 | ) { 30 | // {{backward_euler_loss_grad_body}} 31 | } 32 | 33 | void backward_euler_update_pos( 34 | /* {{backward_euler_update_pos_args}} */ 35 | ) { 36 | /* {{backward_euler_update_pos_body}} */ 37 | } 38 | 39 | extern "C" 40 | void backward_euler_update_vel( 41 | /* {{backward_euler_update_vel_args}} */ 42 | ) { 43 | // vel1 = (pos1 - pos0) / h 44 | add_scaled( 45 | num_vertices * space_dim, 46 | pos1, pos0, 47 | -1.0, 48 | vel1 49 | ); 50 | scale_( 51 | num_vertices * space_dim, 52 | vel1, 1 / h 53 | ); 54 | } 55 | 56 | extern "C" 57 | void backward_euler_update( 58 | /* {{backward_euler_update_args}} */ 59 | ) { 60 | backward_euler_update_pos(/* {{backward_euler_update_pos_args_call}} */); 61 | backward_euler_update_vel(/* {{backward_euler_update_vel_args_call}} */); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /utils/trajectory/ppw/Window.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | 3 | class Window { 4 | constructor(args = {}) { 5 | this.width = args.width ?? 400; 6 | this.height = args.height ?? 400; 7 | this.headless = args.headless ?? false; 8 | this.deviceScaleFactor = args.deviceScaleFactor ?? 1; 9 | if (args.indexUrl == null) throw new Error("indexUrl required"); 10 | this.indexUrl = args.indexUrl; 11 | } 12 | 13 | async launch() { 14 | const width = this.width; 15 | const height = this.height; 16 | const deviceScaleFactor = this.deviceScaleFactor; 17 | const browser = this.browser = await puppeteer.launch({ 18 | headless: this.headless, 19 | args: [ 20 | `--window-size=${this.width},${this.height}`, 21 | "--no-sandbox" 22 | ] 23 | }); 24 | const page = await browser.newPage(); 25 | await page.setViewport({ width: width, height: height, deviceScaleFactor: deviceScaleFactor }); 26 | await page.goto(this.indexUrl); 27 | this.page = page; 28 | } 29 | 30 | async evaluate() { 31 | return await this.page.evaluate.apply(this.page, arguments); 32 | } 33 | 34 | async screenshot() { 35 | return await this.page.screenshot.apply(this.page, arguments); 36 | } 37 | 38 | async waitForNetworkIdle() { 39 | return await this.page.waitForNetworkIdle.apply(this.page, arguments); 40 | } 41 | 42 | async close() { 43 | await this.browser.close(); 44 | } 45 | } 46 | 47 | module.exports = Window; -------------------------------------------------------------------------------- /algovivo/render/Floor.js: -------------------------------------------------------------------------------- 1 | class Floor { 2 | constructor(args = {}) { 3 | if (args.scene == null) { 4 | throw new Error("scene required"); 5 | } 6 | const scene = this.scene = args.scene; 7 | const mesh = this.mesh = scene.addMesh(); 8 | mesh.pos = [ 9 | [-10, 0], 10 | [10, 0] 11 | ]; 12 | mesh.lines = [ 13 | [0, 1] 14 | ]; 15 | 16 | mesh.lineShader.renderLine = Floor.makeFloorLineShaderFunction({ 17 | width: args.width, 18 | color: args.color 19 | }); 20 | 21 | mesh.pointShader.renderPoint = () => {}; 22 | 23 | mesh.setCustomAttribute("translation", [0, 0]); 24 | } 25 | 26 | static makeFloorLineShaderFunction(args = {}) { 27 | const width = args.width ?? 0.055; 28 | const color = args.color ?? "black"; 29 | return (args) => { 30 | const ctx = args.ctx; 31 | const a = args.a; 32 | const b = args.b; 33 | const camera = args.camera; 34 | const mesh = args.mesh; 35 | const scale = camera.inferScale(); 36 | 37 | const _translation = mesh.getCustomAttribute("translation"); 38 | const translation = [scale * _translation[0], scale * _translation[1]]; 39 | 40 | ctx.strokeStyle = color; 41 | ctx.lineWidth = scale * width; 42 | ctx.beginPath(); 43 | ctx.moveTo(a[0] + translation[0], a[1] + translation[1]); 44 | ctx.lineTo(b[0] + translation[0], b[1] + translation[1]); 45 | ctx.stroke(); 46 | } 47 | } 48 | } 49 | 50 | module.exports = Floor; -------------------------------------------------------------------------------- /algovivo/render/mm2d/ui/cursorUtils.js: -------------------------------------------------------------------------------- 1 | function computeDomCursor(event, domElement) { 2 | let clientX, clientY; 3 | if (event.touches == null) { 4 | clientX = event.clientX; 5 | clientY = event.clientY; 6 | } else { 7 | if (event.touches.length == 0) return null; 8 | const touch = event.touches[0]; 9 | clientX = touch.clientX; 10 | clientY = touch.clientY; 11 | } 12 | 13 | // get cumulative transformation matrix 14 | let matrix = new DOMMatrix(); 15 | let element = domElement; 16 | while (element != null) { 17 | const style = window.getComputedStyle(element); 18 | const elementMatrix = new DOMMatrix(style.transform); 19 | matrix = elementMatrix.multiply(matrix); 20 | element = element.parentElement; 21 | } 22 | const matrixInverse = matrix.inverse(); 23 | 24 | // compute cursor position in the domElement's coordinate system 25 | const clientPos = new DOMPointReadOnly(clientX, clientY); 26 | const transformedClientPos = clientPos.matrixTransform(matrixInverse); 27 | 28 | // transform domElement's bounding rectangle 29 | const rect = domElement.getBoundingClientRect(); 30 | const topLeft = new DOMPointReadOnly(rect.left, rect.top); 31 | const transformedTopLeft = topLeft.matrixTransform(matrixInverse); 32 | 33 | const x = transformedClientPos.x - transformedTopLeft.x; 34 | const y = transformedClientPos.y - transformedTopLeft.y; 35 | const cursor = [x, y]; 36 | return cursor; 37 | } 38 | 39 | module.exports = { 40 | computeDomCursor: computeDomCursor 41 | } -------------------------------------------------------------------------------- /test/backwardEuler.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("update vel", async () => { 7 | const ten = await utils.loadTen(); 8 | 9 | const numVertices = 3; 10 | const spaceDim = 2; 11 | 12 | const pos0 = ten.tensor([ 13 | [1, 2], 14 | [3, 4], 15 | [5, 6] 16 | ]); 17 | const pos1 = ten.tensor([ 18 | [2, 10], 19 | [5, 3], 20 | [1, 16] 21 | ]); 22 | const vel1 = ten.zeros([numVertices, spaceDim]); 23 | const dt = 2; 24 | 25 | ten.wasmInstance.exports.backward_euler_update_vel( 26 | numVertices, spaceDim, 27 | pos0.ptr, 0, 28 | pos1.ptr, vel1.ptr, 29 | dt 30 | ); 31 | expect(vel1.toArray()).toBeCloseToArray([ 32 | [0.5, 4], 33 | [1, -0.5], 34 | [-2, 5] 35 | ]); 36 | }); 37 | 38 | test("optim init", async () => { 39 | const ten = await utils.loadTen(); 40 | 41 | const numVertices = 3; 42 | const spaceDim = 2; 43 | 44 | const pos0 = ten.tensor([ 45 | [1, 2], 46 | [3, 4], 47 | [5, 6] 48 | ]); 49 | const vel0 = ten.tensor([ 50 | [0.5, 4], 51 | [1, -0.5], 52 | [-2, 5] 53 | ]); 54 | const pos = ten.zeros([numVertices, spaceDim]); 55 | const dt = 2; 56 | 57 | ten.wasmInstance.exports.optim_init( 58 | numVertices, spaceDim, 59 | dt, 60 | pos0.ptr, vel0.ptr, 61 | pos.ptr 62 | ); 63 | expect(pos.toArray()).toBeCloseToArray([ 64 | [2, 10], 65 | [5, 3], 66 | [1, 16] 67 | ]); 68 | }); -------------------------------------------------------------------------------- /demo/test/ppw/server.js: -------------------------------------------------------------------------------- 1 | import net from "net"; 2 | import express from "express"; 3 | 4 | export function getFreePort() { 5 | return new Promise((resolve) => { 6 | const server = net.createServer(); 7 | server.listen(0, () => { 8 | const port = server.address().port; 9 | server.close(); 10 | resolve(port); 11 | }); 12 | }); 13 | } 14 | 15 | export function runWebServer(args = {}) { 16 | const staticDirname = args.staticDirname; 17 | if (staticDirname == null) { 18 | throw new Error("staticDirname required"); 19 | } 20 | const onReady = args.onReady; 21 | if (onReady == null) { 22 | throw new Error("onReady required"); 23 | } 24 | return new Promise((resolve, reject) => { 25 | (async () => { 26 | const port = args.port ?? await getFreePort(); 27 | 28 | const app = express(); 29 | 30 | if (args.onPreListen != null) { 31 | args.onPreListen(app); 32 | } 33 | 34 | app.use(express.static(staticDirname)); 35 | 36 | const server = app.listen( 37 | port, 38 | async () => { 39 | try { 40 | await onReady(port); 41 | } catch(e) { 42 | reject(e); 43 | } finally { 44 | const daemon = args.daemon ?? false; 45 | if (!daemon) { 46 | server.close(() => { 47 | resolve(); 48 | }); 49 | } else { 50 | resolve(); 51 | } 52 | } 53 | }); 54 | })(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /algovivo/render/mm2d/sorted/MeshTopology.js: -------------------------------------------------------------------------------- 1 | const Simplices = require("./Simplices"); 2 | const Vertex = require("./Vertex"); 3 | 4 | class MeshTopology { 5 | constructor(args = {}) { 6 | this.vertices = new Map(); 7 | this.edges = new Simplices({ order: 2 }); 8 | this.triangles = new Simplices({ order: 3 }); 9 | 10 | const edges = args.edges ?? []; 11 | edges.forEach((e, i) => { 12 | this.addEdge(i, e); 13 | }); 14 | 15 | const triangles = args.triangles ?? []; 16 | triangles.forEach((t, i) => { 17 | this.addTriangle(i, t); 18 | }); 19 | } 20 | 21 | numVertices() { 22 | return this.vertices.size; 23 | } 24 | 25 | numEdges() { 26 | return this.edges.size(); 27 | } 28 | 29 | numTriangles() { 30 | return this.triangles.size(); 31 | } 32 | 33 | getVertexById(id, create = false) { 34 | let vertex = this.vertices.get(id); 35 | if (vertex == null && create) { 36 | vertex = new Vertex(id); 37 | this.vertices.set(id, vertex); 38 | } 39 | return vertex; 40 | } 41 | 42 | addEdge(id, vertexIds) { 43 | const edge = this.edges.add(vertexIds, id); 44 | vertexIds.forEach(vid => { 45 | this.getVertexById(vid, true).addEdge(edge); 46 | }); 47 | return edge; 48 | } 49 | 50 | addTriangle(id, vertexIds) { 51 | const triangle = this.triangles.add(vertexIds, id); 52 | vertexIds.forEach(vid => { 53 | this.getVertexById(vid, true).addTriangle(triangle); 54 | }); 55 | return triangle; 56 | } 57 | } 58 | 59 | module.exports = MeshTopology; -------------------------------------------------------------------------------- /test/system.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("set pos", async () => { 7 | const ten = await utils.loadTen(); 8 | const system = new algovivo.System({ ten }); 9 | expect(system.numVertices).toBe(0); 10 | expect(system.numMuscles).toBe(0); 11 | expect(system.numTriangles).toBe(0); 12 | 13 | system.set({ 14 | pos: [ 15 | [1, 2] 16 | ] 17 | }); 18 | 19 | expect(system.numVertices).toBe(1); 20 | expect(system.pos.toArray()).toBeCloseToArray([[1, 2]]); 21 | expect(system.vel.toArray()).toBeCloseToArray([[0, 0]]); 22 | }); 23 | 24 | test("step with no vertices", async () => { 25 | const ten = await utils.loadTen(); 26 | const system = new algovivo.System({ ten }); 27 | expect(system.numVertices).toBe(0); 28 | expect(system.numMuscles).toBe(0); 29 | expect(system.numTriangles).toBe(0); 30 | system.step(); 31 | expect(system.numVertices).toBe(0); 32 | expect(system.numMuscles).toBe(0); 33 | expect(system.numTriangles).toBe(0); 34 | }); 35 | 36 | test("step with fixed vertex", async () => { 37 | const ten = await utils.loadTen(); 38 | const system = new algovivo.System({ ten }); 39 | system.set({ 40 | pos: [ 41 | [1, 3], 42 | [5, 6] 43 | ] 44 | }); 45 | system.vertices.fixVertex(0); 46 | system.step(); 47 | const p = system.pos.toArray(); 48 | expect(p[0][0]).toEqual(1); 49 | expect(p[0][1]).toEqual(3); 50 | expect(p[1][0]).toEqual(5); 51 | expect(p[1][1]).toBeLessThan(6); 52 | }); -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | ENZYME_IMAGE: ghcr.io/juniorrojas/algovivo/llvm11-enzyme:latest 14 | 15 | jobs: 16 | build-wasm: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Build WASM 23 | run: | 24 | python codegen/codegen_csrc.py 25 | docker run \ 26 | -v $(pwd):/workspace \ 27 | -w /workspace \ 28 | ${{ env.ENZYME_IMAGE }} \ 29 | ./build.sh 30 | 31 | - name: Upload WASM 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: algovivo.wasm 35 | path: build/algovivo.wasm 36 | 37 | test-js: 38 | runs-on: ubuntu-latest 39 | needs: build-wasm 40 | steps: 41 | - name: Clone repo 42 | uses: actions/checkout@v4 43 | 44 | - name: Download WASM build 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: algovivo.wasm 48 | path: build/ 49 | 50 | - name: Set up node 22 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: 22 54 | 55 | - name: Install dependencies 56 | run: npm ci 57 | 58 | - name: Test 59 | run: npm test 60 | 61 | - name: Upload lib build 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: build 65 | path: ./build -------------------------------------------------------------------------------- /demo/src/Section.js: -------------------------------------------------------------------------------- 1 | export default class Section { 2 | constructor(title, content) { 3 | this.domElement = document.createElement("div"); 4 | this.domElement.style.textAlign = "left"; 5 | this.domElement.style.color = "#666"; 6 | this.domElement.style.fontSize = "14px"; 7 | this.domElement.style.padding = "22px"; 8 | this.domElement.style.paddingRight = "26px"; 9 | this.domElement.style.paddingLeft = "26px"; 10 | this.domElement.style.paddingBottom = "45px"; 11 | this.domElement.style.width = "100%"; 12 | this.domElement.style.display = "flex"; 13 | this.domElement.style.justifyContent = "center"; 14 | 15 | const divContainer = document.createElement("div"); 16 | divContainer.style.maxWidth = "600px"; 17 | this.domElement.appendChild(divContainer); 18 | 19 | const h2 = document.createElement("h2"); 20 | this.header = h2; 21 | h2.style.color = "black"; 22 | h2.style.fontSize = "25px"; 23 | h2.style.padding = "10px"; 24 | h2.style.borderBottom = "2px solid black"; 25 | h2.textContent = title; 26 | 27 | const contentElement = document.createElement("div"); 28 | contentElement.innerHTML = content; 29 | 30 | divContainer.appendChild(h2); 31 | divContainer.appendChild(contentElement); 32 | } 33 | 34 | setStyle1() { 35 | this.domElement.style.backgroundColor = "black"; 36 | this.domElement.style.color = "rgb(199, 199, 199)"; 37 | this.domElement.style.boxShadow = "rgba(0, 0, 0, 0.3) 0px 0px 10px"; 38 | this.header.style.color = "white"; 39 | this.header.style.borderBottom = "2px solid white"; 40 | } 41 | } -------------------------------------------------------------------------------- /demo/src/Header.js: -------------------------------------------------------------------------------- 1 | import { makeGitHubLink } from "./ui.js"; 2 | 3 | export default class Header { 4 | constructor() { 5 | const divTitle = document.createElement("div"); 6 | this.domElement = divTitle; 7 | (style => { 8 | style.display = "flex"; 9 | style.flexDirection = "column"; 10 | style.alignItems = "center"; 11 | style.color = "white"; 12 | style.width = "100%"; 13 | style.backgroundColor = "#000000"; 14 | style.paddingBottom = "20px"; 15 | style.marginBottom = "30px"; 16 | style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.3)"; 17 | })(divTitle.style); 18 | 19 | const divContent = document.createElement("div"); 20 | divTitle.appendChild(divContent); 21 | (style => { 22 | style.maxWidth = "1200px"; 23 | style.width = "100%"; 24 | style.paddingTop = "20px"; 25 | style.position = "relative"; 26 | style.textAlign = "center"; 27 | style.paddingRight = "50px"; 28 | style.paddingLeft = "50px"; 29 | })(divContent.style); 30 | 31 | const h1 = document.createElement("h1"); 32 | h1.textContent = "algovivo"; 33 | divContent.appendChild(h1); 34 | ((style) => { 35 | style.fontSize = "33px"; 36 | style.color = "white"; 37 | })(h1.style); 38 | 39 | const a = makeGitHubLink(); 40 | divContent.appendChild(a); 41 | 42 | const h2 = document.createElement("h2"); 43 | h2.textContent = "an energy-based formulation for soft-bodied virtual creatures"; 44 | divContent.appendChild(h2); 45 | ((style) => { 46 | style.textAlign = "center"; 47 | style.fontSize = "18px"; 48 | style.color = "#c7c7c7"; 49 | })(h2.style); 50 | } 51 | } -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | algovivo 8 | 57 | 58 | 59 | 69 | 70 | -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/linked/List.js: -------------------------------------------------------------------------------- 1 | const Node = require("./Node"); 2 | 3 | class ListIter { 4 | constructor(list) { 5 | this.list = list; 6 | this.nextNode = this.list.first; 7 | } 8 | 9 | next() { 10 | if (this.nextNode == null) { 11 | return { 12 | done: true 13 | }; 14 | } else { 15 | const r = { 16 | done: false, 17 | value: this.nextNode.data 18 | }; 19 | this.nextNode = this.nextNode.next; 20 | return r; 21 | } 22 | } 23 | } 24 | 25 | class List { 26 | constructor() { 27 | this.first = null; 28 | this.last = null; 29 | this.size = 0; 30 | } 31 | 32 | isEmpty() { 33 | if ( 34 | (this.first == null && this.last != null) || 35 | (this.first != null && this.last == null) 36 | ) { 37 | throw Error("inconsistent first last state"); 38 | } 39 | return this.first == null; 40 | } 41 | 42 | append(data) { 43 | if (this.isEmpty()) { 44 | return this.setSingleton(data); 45 | } else { 46 | return this.last.append(data); 47 | } 48 | } 49 | 50 | prepend(data) { 51 | if (this.isEmpty()) { 52 | return this.setSingleton(data); 53 | } else { 54 | return this.first.prepend(data); 55 | } 56 | } 57 | 58 | setSingleton(data) { 59 | const node = new Node(this, data); 60 | this.first = node; 61 | this.last = node; 62 | this.size = 1; 63 | return node; 64 | } 65 | 66 | iter() { 67 | return new ListIter(this); 68 | } 69 | 70 | *[Symbol.iterator]() { 71 | const it = this.iter(); 72 | let r = it.next(); 73 | while (!r.done) { 74 | yield r.value; 75 | r = it.next(); 76 | } 77 | } 78 | } 79 | 80 | module.exports = List; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/modules/vertices.py: -------------------------------------------------------------------------------- 1 | class Vertices: 2 | def __init__(self): 3 | pass 4 | 5 | def add_args(self, args): 6 | args.add_arg("int", "num_vertices") 7 | args.add_arg("float*", "pos0") 8 | args.add_arg("float*", "vel0") 9 | args.add_arg("float", "vertex_mass") 10 | 11 | def add_update_args(self, args): 12 | args.add_arg("int", "num_vertices") 13 | args.add_arg("float*", "pos0") 14 | args.add_arg("float*", "vel0") 15 | args.add_arg("float", "vertex_mass") 16 | 17 | args.add_arg("int*", "fixed_vertex_id") 18 | 19 | inertial_arg_name = "pos" 20 | inertial_arg_t = "float*" 21 | args.add_arg(inertial_arg_t, f"{inertial_arg_name}1", mut=True) 22 | args.add_arg(inertial_arg_t, f"{inertial_arg_name}_grad", mut=True) 23 | args.add_arg(inertial_arg_t, f"{inertial_arg_name}_tmp", mut=True) 24 | args.add_arg(inertial_arg_t, f"vel1", mut=True) 25 | 26 | def add_update_pos_args(self, update_pos_args): 27 | update_pos_args.add_arg("float*", "pos", mut=True) 28 | update_pos_args.add_arg("float*", "pos_grad", mut=True) 29 | update_pos_args.add_arg("float*", "pos_tmp", mut=True) 30 | update_pos_args.add_arg("int*", "fixed_vertex_id") 31 | 32 | def add_update_vel_args(self, update_vel_args): 33 | update_vel_args.add_arg("int", "num_vertices") 34 | update_vel_args.add_arg("int", "space_dim") 35 | update_vel_args.add_arg("float*", "pos0") 36 | update_vel_args.add_arg("float*", "vel0") 37 | update_vel_args.add_arg("float*", "pos1", mut=True) 38 | update_vel_args.add_arg("float*", "vel1", mut=True) 39 | update_vel_args.add_arg("float", "h") -------------------------------------------------------------------------------- /algovivo/mmgrten/IntTuple.js: -------------------------------------------------------------------------------- 1 | // const mmgr = require("./mmgr"); 2 | 3 | class IntTuple { 4 | constructor(args = {}) { 5 | const engine = args.engine; 6 | if (engine == null) { 7 | throw new Error("engine required to create IntTuple"); 8 | } 9 | this.engine = engine; 10 | 11 | const slot = args.slot; 12 | if (slot == null) { 13 | throw new Error("slot required to create IntTuple"); 14 | } 15 | // TODO check slot instanceof mmgr.Slot 16 | this.slot = slot; 17 | this.ptr = slot.ptr; 18 | 19 | this.length = args.length; 20 | } 21 | 22 | forEach(f) { 23 | for (let i = 0; i < this.length; i++) { 24 | f(this.get(i), i); 25 | } 26 | } 27 | 28 | equal(b) { 29 | if (b instanceof IntTuple) { 30 | for (let i = 0; i < this.length; i++) { 31 | const ai = this.get(i); 32 | const bi = b.get(i); 33 | if (ai != bi) return false; 34 | } 35 | return true; 36 | } else 37 | if (Array.isArray(b)) { 38 | for (let i = 0; i < this.length; i++) { 39 | const ai = this.get(i); 40 | const bi = b[i]; 41 | if (ai != bi) return false; 42 | } 43 | return true; 44 | } else { 45 | return false; 46 | } 47 | } 48 | 49 | toString() { 50 | return this.slot.u32().toString(); 51 | } 52 | 53 | toArray() { 54 | const s = []; 55 | this.forEach((si) => { 56 | s.push(si); 57 | }); 58 | return s; 59 | } 60 | 61 | typedArray() { 62 | return this.slot.u32(); 63 | } 64 | 65 | set(i, v) { 66 | this.typedArray()[i] = v; 67 | } 68 | 69 | get(i) { 70 | return this.typedArray()[i]; 71 | } 72 | 73 | dispose() { 74 | this.slot.free(); 75 | } 76 | } 77 | 78 | module.exports = IntTuple; -------------------------------------------------------------------------------- /test/memory.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | test("memory", async () => { 5 | const system = new algovivo.System({ 6 | wasmInstance: await utils.loadWasm() 7 | }); 8 | const memoryManager = system.ten.mgr; 9 | 10 | expect(system.numVertices).toBe(0); 11 | expect(system.numMuscles).toBe(0); 12 | expect(system.numTriangles).toBe(0); 13 | 14 | const meshData = { 15 | pos: [ 16 | [0, 0], 17 | [1, 0], 18 | [1, 1] 19 | ], 20 | triangles: [ 21 | [0, 1, 2] 22 | ], 23 | muscles: [ 24 | [0, 1], 25 | [2, 1] 26 | ] 27 | }; 28 | 29 | expect(memoryManager.numReservedBytes()).toBe(0); 30 | system.set(meshData); 31 | expect(memoryManager.numReservedBytes()).not.toBe(0); 32 | const reservedBytes = memoryManager.numReservedBytes(); 33 | expect(system.numVertices).toBe(3); 34 | expect(system.numTriangles).toBe(1); 35 | expect(system.numMuscles).toBe(2); 36 | 37 | // updating the mesh again with the same data 38 | // should not allocate any new memory 39 | system.set(meshData); 40 | expect(memoryManager.numReservedBytes()).toBe(reservedBytes); 41 | expect(system.numVertices).toBe(3); 42 | expect(system.numTriangles).toBe(1); 43 | expect(system.numMuscles).toBe(2); 44 | 45 | // free memory 46 | system.dispose(); 47 | expect(memoryManager.numReservedBytes()).toBe(0); 48 | expect(system.numVertices).toBe(0); 49 | expect(system.numTriangles).toBe(0); 50 | expect(system.numMuscles).toBe(0); 51 | 52 | // reset data 53 | system.set(meshData); 54 | expect(memoryManager.numReservedBytes()).toBe(reservedBytes); 55 | expect(system.numVertices).toBe(3); 56 | expect(system.numTriangles).toBe(1); 57 | expect(system.numMuscles).toBe(2); 58 | }); -------------------------------------------------------------------------------- /utils/py/algovivo/system.py: -------------------------------------------------------------------------------- 1 | from .vertices import Vertices 2 | from .muscles import Muscles 3 | from .triangles import Triangles 4 | 5 | class System: 6 | def __init__(self, native_instance): 7 | self.native_instance = native_instance 8 | 9 | self.h = 0.033 10 | 11 | self.vertices = Vertices() 12 | self.muscles = Muscles(native_instance.lib) 13 | self.triangles = Triangles(native_instance.lib) 14 | 15 | self.k_friction = float(300) 16 | self.k_collision = float(14000) 17 | 18 | @property 19 | def space_dim(self): 20 | return 2 21 | 22 | @property 23 | def num_vertices(self): 24 | return self.vertices.num_vertices 25 | 26 | @property 27 | def num_muscles(self): 28 | return self.muscles.num_muscles 29 | 30 | @property 31 | def num_triangles(self): 32 | return self.triangles.num_triangles 33 | 34 | def set(self, pos=None, muscles=None, muscles_l0=None, triangles=None, triangles_rsi=None): 35 | self.vertices.set(pos) 36 | self.muscles.set(indices=muscles, pos=pos, l0=muscles_l0) 37 | self.triangles.set(indices=triangles, pos=pos, rsi=triangles_rsi) 38 | 39 | def step(self): 40 | g = 9.8 41 | h = self.h 42 | 43 | self.native_instance.lib.backward_euler_update( 44 | 2, # 2D 45 | g, 46 | h, 47 | 48 | *self.vertices.to_step_args(), 49 | 50 | *self.muscles.to_step_args(), 51 | 52 | *self.triangles.to_step_args(), 53 | 54 | self.k_friction, 55 | 56 | self.k_collision 57 | ) 58 | 59 | if self.num_vertices != 0: 60 | self.vertices.pos0.data.copy_(self.vertices.pos1) 61 | self.vertices.vel0.data.copy_(self.vertices.vel1) -------------------------------------------------------------------------------- /algovivo/mmgrten/utils.js: -------------------------------------------------------------------------------- 1 | function inferShape(arr) { 2 | const shapeArr = []; 3 | let _arr = arr; 4 | while (true) { 5 | if (Array.isArray(_arr)) { 6 | shapeArr.push(_arr.length); 7 | _arr = _arr[0]; 8 | } else { 9 | break; 10 | } 11 | } 12 | return shapeArr; 13 | } 14 | 15 | function _makeNdArray(arr, dim, shape, value) { 16 | if (dim == shape.length - 1) { 17 | for (let i = 0; i < shape[dim]; i++) { 18 | arr.push(value); 19 | } 20 | } else { 21 | for (let i = 0; i < shape[dim]; i++) { 22 | const li = []; 23 | arr.push(li); 24 | _makeNdArray(li, dim + 1, shape, value); 25 | } 26 | } 27 | } 28 | 29 | function makeNdArray(_shape, value) { 30 | const IntTuple = require("./IntTuple"); 31 | let shape = _shape; 32 | if (_shape instanceof IntTuple) { 33 | shape = _shape.toArray(); 34 | } 35 | const arr = []; 36 | _makeNdArray(arr, 0, shape, value); 37 | return arr; 38 | } 39 | 40 | function numelOfShape(shape) { 41 | let numel = 1; 42 | shape.forEach(si => { 43 | numel *= si; 44 | }); 45 | return numel; 46 | } 47 | 48 | function getArrElem(arr, idx) { 49 | if (!Array.isArray(idx)) { 50 | throw new Error(`expected array, found ${typeof idx}: ${idx}`); 51 | } 52 | if (idx.length == 0) { 53 | return arr; 54 | } else { 55 | return getArrElem(arr[idx[0]], idx.slice(1)); 56 | } 57 | } 58 | 59 | function setArrElem(arr, idx, v) { 60 | if (!Array.isArray(idx)) { 61 | throw new Error(`expected array, found ${typeof idx}: ${idx}`); 62 | } 63 | if (idx.length == 1) { 64 | arr[idx] = v; 65 | } else { 66 | return setArrElem(arr[idx[0]], idx.slice(1), v); 67 | } 68 | } 69 | 70 | module.exports = { 71 | inferShape, 72 | makeNdArray, 73 | numelOfShape, 74 | getArrElem, 75 | setArrElem 76 | } -------------------------------------------------------------------------------- /algovivo/render/mm2d/math/Matrix2x2.js: -------------------------------------------------------------------------------- 1 | class Matrix2x2 { 2 | constructor(m00, m01, m10, m11) { 3 | this.m00 = m00; 4 | this.m01 = m01; 5 | this.m10 = m10; 6 | this.m11 = m11; 7 | } 8 | 9 | get(i, j) { 10 | return this[`m${i}${j}`]; 11 | } 12 | 13 | set(m00, m01, 14 | m10, m11) { 15 | this.m00 = m00; 16 | this.m01 = m01; 17 | this.m10 = m10; 18 | this.m11 = m11; 19 | } 20 | 21 | toArray() { 22 | return [ 23 | [this.m00, this.m01], 24 | [this.m10, this.m11] 25 | ]; 26 | } 27 | 28 | negate() { 29 | return new Matrix2x2( 30 | -this.m00, -this.m01, 31 | -this.m10, -this.m11 32 | ); 33 | } 34 | 35 | apply(v) { 36 | return [ 37 | this.m00 * v[0] + this.m01 * v[1], 38 | this.m10 * v[0] + this.m11 * v[1] 39 | ]; 40 | } 41 | 42 | det() { 43 | return this.m00 * this.m11 - this.m10 * this.m01; 44 | } 45 | 46 | inv() { 47 | const det = this.det(); 48 | return new Matrix2x2( 49 | this.m11 / det, -this.m01 / det, 50 | -this.m10 / det, this.m00 / det 51 | ); 52 | } 53 | 54 | mm(b) { 55 | const a00 = this.m00; 56 | const a01 = this.m01; 57 | const a10 = this.m10; 58 | const a11 = this.m11; 59 | 60 | const b00 = b.m00; 61 | const b01 = b.m01; 62 | const b10 = b.m10; 63 | const b11 = b.m11; 64 | 65 | return new Matrix2x2( 66 | a00 * b00 + a01 * b10, a00 * b01 + a01 * b11, 67 | a10 * b00 + a11 * b10, a10 * b01 + a11 * b11 68 | ); 69 | } 70 | 71 | t() { 72 | return new Matrix2x2( 73 | this.m00, this.m10, 74 | this.m01, this.m11 75 | ); 76 | } 77 | 78 | static fromArray(a) { 79 | return new Matrix2x2( 80 | a[0][0], a[0][1], 81 | a[1][0], a[1][1] 82 | ); 83 | } 84 | } 85 | 86 | module.exports = Matrix2x2; -------------------------------------------------------------------------------- /test/render/mm2d/sortedElements.test.js: -------------------------------------------------------------------------------- 1 | const mm2d = require("algovivo").mm2d; 2 | 3 | test("sorted triangles", () => { 4 | const triangles = [ 5 | [0, 1, 2] 6 | ]; 7 | 8 | const sortedVertexIds = [ 9 | 0, 1, 2 10 | ]; 11 | 12 | const sortedElements = mm2d.sorted.makeSortedElements({ 13 | triangles, 14 | sortedVertexIds, 15 | edges: [] 16 | }); 17 | 18 | expect(sortedElements.length).toBe(4); 19 | 20 | expect(sortedElements[0].order).toEqual(3); 21 | expect(sortedElements[0].vertexIds).toEqual([0, 1, 2]); 22 | 23 | expect(sortedElements[1].order).toEqual(1); 24 | expect(sortedElements[1].id).toEqual(0); 25 | 26 | expect(sortedElements[2].order).toEqual(1); 27 | expect(sortedElements[2].id).toEqual(1); 28 | 29 | expect(sortedElements[3].order).toEqual(1); 30 | expect(sortedElements[3].id).toEqual(2); 31 | }); 32 | 33 | test("sorted triangles and edges", () => { 34 | const triangles = [ 35 | [0, 1, 2] 36 | ]; 37 | 38 | const edges = [ 39 | [0, 2] 40 | ]; 41 | 42 | const sortedVertexIds = [ 43 | 0, 1, 2 44 | ]; 45 | 46 | const sortedElements = mm2d.sorted.makeSortedElements({ 47 | sortedVertexIds: sortedVertexIds, 48 | triangles: triangles, 49 | edges: edges 50 | }); 51 | 52 | expect(sortedElements.length).toBe(5); 53 | 54 | expect(sortedElements[0].order).toEqual(3); 55 | expect(sortedElements[0].vertexIds).toEqual([0, 1, 2]); 56 | 57 | expect(sortedElements[1].order).toEqual(2); 58 | expect(sortedElements[1].vertexIds).toEqual([0, 2]); 59 | 60 | expect(sortedElements[2].order).toEqual(1); 61 | expect(sortedElements[2].id).toEqual(0); 62 | 63 | expect(sortedElements[3].order).toEqual(1); 64 | expect(sortedElements[3].id).toEqual(1); 65 | 66 | expect(sortedElements[4].order).toEqual(1); 67 | expect(sortedElements[4].id).toEqual(2); 68 | }); -------------------------------------------------------------------------------- /utils/py/algovivo/native_instance.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ctypes 3 | 4 | class NativeInstance: 5 | def __init__(self, lib): 6 | self.lib = lib 7 | 8 | lib.backward_euler_update.restype = None 9 | lib.backward_euler_update.argtypes = [ 10 | ctypes.c_int, # space_dim 11 | ctypes.c_float, # g 12 | ctypes.c_float, # h 13 | 14 | ctypes.c_int, # num_vertices 15 | ctypes.POINTER(ctypes.c_float), # pos0 16 | ctypes.POINTER(ctypes.c_float), # vel0 17 | ctypes.c_float, # vertex_mass 18 | 19 | ctypes.POINTER(ctypes.c_int), # fixed_vertex_id 20 | ctypes.POINTER(ctypes.c_float), # pos1 21 | ctypes.POINTER(ctypes.c_float), # pos_grad 22 | ctypes.POINTER(ctypes.c_float), # pos_tmp 23 | ctypes.POINTER(ctypes.c_float), # vel1 24 | 25 | ctypes.c_int, # num_muscles 26 | ctypes.POINTER(ctypes.c_int), # muscles 27 | ctypes.c_float, # k 28 | ctypes.POINTER(ctypes.c_float), # a 29 | ctypes.POINTER(ctypes.c_float), # l0 30 | 31 | ctypes.c_int, # num_triangles 32 | ctypes.POINTER(ctypes.c_int), # indices 33 | ctypes.POINTER(ctypes.c_float), # rsi 34 | ctypes.POINTER(ctypes.c_float), # mu 35 | ctypes.POINTER(ctypes.c_float), # lambda 36 | 37 | ctypes.c_float, # k_friction 38 | ctypes.c_float # k_collision 39 | ] 40 | 41 | @staticmethod 42 | def load(filename): 43 | if not os.path.exists(filename): 44 | raise FileNotFoundError(filename) 45 | lib = ctypes.cdll.LoadLibrary(filename) 46 | return NativeInstance(lib) -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/modules/triangles.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../vec2.h" 3 | #include "../mat2x2.h" 4 | 5 | namespace algovivo { 6 | 7 | __attribute__((always_inline)) 8 | void accumulate_triangle_energy( 9 | float &energy, 10 | const float* pos, 11 | int ia, int ib, int ic, 12 | float rsi00, float rsi01, 13 | float rsi10, float rsi11, 14 | float w, float mu, float lambda 15 | ) { 16 | vec2_get(a, pos, ia); 17 | vec2_get(b, pos, ib); 18 | vec2_get(c, pos, ic); 19 | 20 | vec2_sub(ab, b, a); 21 | vec2_sub(ac, c, a); 22 | 23 | mat2x2_mm( 24 | F00, F01, 25 | F10, F11, 26 | 27 | abx, acx, 28 | aby, acy, 29 | 30 | rsi00, rsi01, 31 | rsi10, rsi11 32 | ); 33 | 34 | const auto I1 = mat2x2_pow2_sum(F); 35 | const auto J = mat2x2_det(F); 36 | const float qlogJ = -1.5 + 2 * J - 0.5 * J * J; 37 | const float psi_mu = 0.5 * mu * (I1 - 2) - mu * qlogJ; 38 | const float psi_lambda = 0.5 * lambda * qlogJ * qlogJ; 39 | 40 | energy += w * (psi_mu + psi_lambda); 41 | } 42 | 43 | extern "C" 44 | void rsi_of_pos( 45 | int num_vertices, 46 | const float* pos, 47 | int num_triangles, 48 | const int* indices, 49 | float* rsi 50 | ) { 51 | for (int i = 0; i < num_triangles; i++) { 52 | const auto offset = 3 * i; 53 | const auto ia = indices[offset ]; 54 | const auto ib = indices[offset + 1]; 55 | const auto ic = indices[offset + 2]; 56 | 57 | vec2_get(a, pos, ia); 58 | vec2_get(b, pos, ib); 59 | vec2_get(c, pos, ic); 60 | 61 | vec2_sub(ab, b, a); 62 | vec2_sub(ac, c, a); 63 | 64 | const auto rs00 = abx; 65 | const auto rs01 = acx; 66 | const auto rs10 = aby; 67 | const auto rs11 = acy; 68 | 69 | mat2x2_inv(rsi, rs); 70 | 71 | const auto offset_rsi = i * 4; 72 | rsi[offset_rsi ] = rsi00; 73 | rsi[offset_rsi + 1] = rsi01; 74 | rsi[offset_rsi + 2] = rsi10; 75 | rsi[offset_rsi + 3] = rsi11; 76 | } 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /codegen/algovivo_codegen/neohookean.py: -------------------------------------------------------------------------------- 1 | def indent(s): 2 | return "\n".join(" " + line for line in s.split("\n")) 3 | 4 | class Neohookean: 5 | def __init__(self, simplex_order, simplex_name_singular, simplex_name_plural=None): 6 | self.simplex_order = simplex_order 7 | self.simplex_name_singular = simplex_name_singular 8 | if simplex_name_plural is None: 9 | simplex_name_plural = f"{simplex_name_singular}s" 10 | self.simplex_name_plural = simplex_name_plural 11 | 12 | def codegen_accumulate_simplices_energy(self): 13 | simplex_order = self.simplex_order 14 | simplex_name_singular = self.simplex_name_singular 15 | simplex_name_plural = self.simplex_name_plural 16 | 17 | rsi_numel = (simplex_order - 1) ** 2 18 | 19 | get_indices = f"const auto offset = i * {simplex_order};\n" 20 | for i in range(simplex_order): 21 | get_indices += f"const auto i{i + 1} = {simplex_name_plural}[offset + {i}];\n" 22 | 23 | args_call_indices = ", ".join(f"i{i + 1}" for i in range(simplex_order)) 24 | 25 | get_rsi = f"const auto rsi_offset = {rsi_numel} * i;\n" 26 | for i in range(simplex_order - 1): 27 | for j in range(simplex_order - 1): 28 | get_rsi += f"const auto rsi{i}{j} = rsi[rsi_offset + {i * (simplex_order - 1) + j}];\n" 29 | 30 | args_call_rsi = "" 31 | for i in range(simplex_order - 1): 32 | for j in range(simplex_order - 1): 33 | args_call_rsi += f"rsi{i}{j}, " 34 | args_call_rsi += "\n" 35 | 36 | src = f"for (int i = 0; i < num_{simplex_name_plural}; i++) {{" 37 | src += f""" 38 | {indent(get_indices)} 39 | {indent(get_rsi)} 40 | 41 | accumulate_{simplex_name_singular}_energy( 42 | potential_energy, 43 | pos, 44 | {args_call_indices}, 45 | 46 | {indent(indent(args_call_rsi))} 47 | 1, 48 | mu[i], lambda[i] 49 | ); 50 | }}""" 51 | return src -------------------------------------------------------------------------------- /test/ten/tensor.test.js: -------------------------------------------------------------------------------- 1 | const { mmgrten } = require("algovivo"); 2 | const utils = require("../utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("tensor", async () => { 7 | const ten = await utils.loadTen(); 8 | const a = ten.tensor([1, 2, 3]); 9 | expect(a.shape.toArray()).toEqual([3]); 10 | expect(a.stride.toArray()).toEqual([1]); 11 | expect(a.order).toEqual(1); 12 | expect(a.numel).toEqual(3); 13 | 14 | expect(Array.from(a.typedArray())).toEqual([1, 2, 3]); 15 | 16 | expect(a.get([0])).toBe(1); 17 | a.set([0], 10); 18 | expect(a.get([0])).toBe(10); 19 | 20 | expect(a.flattenIdx([1])).toBe(1); 21 | expect(a.get([1])).toBe(2); 22 | expect(a.flattenIdx([2])).toBe(2); 23 | expect(a.get([2])).toBe(3); 24 | }); 25 | 26 | test("empty + zero", async () => { 27 | const ten = await utils.loadTen(); 28 | 29 | const a = ten.empty([3, 1, 2]); 30 | expect(a.shape.toArray()).toEqual([3, 1, 2]); 31 | expect(a.order).toEqual(3); 32 | a.zero_(); 33 | expect(a.toArray()).toBeCloseToArray([ 34 | [[0, 0]], 35 | [[0, 0]], 36 | [[0, 0]] 37 | ]); 38 | }); 39 | 40 | test("constructors", async () => { 41 | const ten = await utils.loadTen(); 42 | 43 | const a = ten.zeros([3, 1, 2]); 44 | expect(a.toArray()).toBeCloseToArray([ 45 | [[0, 0]], 46 | [[0, 0]], 47 | [[0, 0]] 48 | ]); 49 | 50 | const b = ten.ones([3, 1, 2]); 51 | expect(b.toArray()).toBeCloseToArray([ 52 | [[1, 1]], 53 | [[1, 1]], 54 | [[1, 1]] 55 | ]); 56 | }); 57 | 58 | test("matrix set", async () => { 59 | const ten = await utils.loadTen(); 60 | 61 | const a = ten.zeros([2, 3]); 62 | a.set([1, 2], 10); 63 | a.set([0, 1], 30); 64 | expect(a.toArray()).toEqual([ 65 | [0, 30, 0], 66 | [0, 0, 10] 67 | ]); 68 | }); 69 | 70 | test("set tensor with leading zero dim", async () => { 71 | const ten = await utils.loadTen(); 72 | 73 | const a = ten.zeros([0, 2, 3]); 74 | expect(a.shape.toArray()).toEqual([0, 2, 3]); 75 | expect(a.toArray()).toEqual([]); 76 | }); -------------------------------------------------------------------------------- /algovivo/render/mm2d/core/Camera.js: -------------------------------------------------------------------------------- 1 | const math = require("../math"); 2 | 3 | class Camera { 4 | constructor() { 5 | this.transform = new math.Transform2d(); 6 | } 7 | 8 | domToWorldSpace(pos) { 9 | if (!Array.isArray(pos)) throw new Error(`array expected, found ${typeof pos}`); 10 | if (pos.length != 2) throw new Error(`array with 2 elements expected, found ${pos.length}`); 11 | const worldPos = this.transform.inv().apply(pos); 12 | return worldPos; 13 | } 14 | 15 | inferScale() { 16 | return this.transform.inferScale(); 17 | } 18 | 19 | center(args = {}) { 20 | let viewportWidth = args.viewportWidth; 21 | let viewportHeight = args.viewportHeight; 22 | 23 | const renderer = args.renderer; 24 | if ((viewportWidth == null || viewportHeight == null) && renderer == null) { 25 | throw new Error("renderer required"); 26 | } 27 | if (renderer != null) { 28 | // if present, args.renderer overwrites 29 | // viewportWidth and viewportHeight 30 | viewportWidth = renderer.width; 31 | viewportHeight = renderer.height; 32 | } 33 | 34 | if (viewportWidth == null) { 35 | throw new Error("viewportWidth required"); 36 | } 37 | if (viewportHeight == null) { 38 | throw new Error("viewportHeight required"); 39 | } 40 | 41 | let scale = args.zoom ?? 1; 42 | if (args.worldWidth != null) { 43 | scale = viewportWidth / args.worldWidth; 44 | } 45 | 46 | this.transform.linear = new math.Matrix2x2( 47 | scale, 0, 48 | 0, -scale 49 | ); 50 | 51 | let translation; 52 | if (args.worldCenter != null) { 53 | const worldCenter = args.worldCenter; 54 | translation = [ 55 | viewportWidth * 0.5 - worldCenter[0] * scale, 56 | viewportHeight * 0.5 + worldCenter[1] * scale 57 | ] 58 | } else { 59 | translation = [ 60 | viewportWidth * 0.5, 61 | viewportHeight * 0.5 62 | ] 63 | } 64 | this.transform.translation = translation; 65 | } 66 | } 67 | 68 | module.exports = Camera; -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const fsp = require("fs/promises"); 2 | const fs = require("fs"); 3 | const algovivo = require("algovivo"); 4 | 5 | function toBeCloseToArray(a, b, tolerance = 1e-3) { 6 | if ((typeof a == "number") && (typeof b == "number")) { 7 | if (Math.abs(a - b) <= tolerance) { 8 | return { 9 | pass: true, 10 | }; 11 | } 12 | return { 13 | pass: false, 14 | message: () => `not close enough, ${a} != ${b}` 15 | }; 16 | } 17 | 18 | if (!Array.isArray(a) || !Array.isArray(b)) { 19 | return { 20 | pass: false, 21 | message: () => `expected two arrays, found ${typeof a} and ${typeof b}` 22 | }; 23 | } 24 | 25 | if (a.length != b.length) { 26 | return { 27 | pass: false, 28 | message: () => `length mismatch, ${a.length} != ${b.length}` 29 | }; 30 | } 31 | 32 | for (let i = 0; i < a.length; i++) { 33 | const r = toBeCloseToArray(a[i], b[i], tolerance); 34 | if (!r.pass) return r; 35 | } 36 | 37 | return { 38 | pass: true 39 | }; 40 | } 41 | 42 | async function loadWasm(args = {}) { 43 | const wasm = await WebAssembly.compile(await fsp.readFile(__dirname + "/../build/algovivo.wasm")); 44 | const wasmInstance = await WebAssembly.instantiate(wasm, args); 45 | return wasmInstance; 46 | } 47 | 48 | async function loadTen() { 49 | const ten = new algovivo.mmgrten.Engine(); 50 | const wasmInstance = await loadWasm(); 51 | ten.init({ wasmInstance }); 52 | return ten; 53 | } 54 | 55 | function fileExists(filename) { 56 | return new Promise((resolve, reject) => { 57 | fs.access(filename, fs.constants.F_OK, (err) => { 58 | if (err) resolve(false); 59 | else resolve(true); 60 | }); 61 | }); 62 | } 63 | 64 | async function cleandir(dirname) { 65 | if (!await fileExists(dirname)) { 66 | await fsp.mkdir(dirname); 67 | } else { 68 | fs.rmSync(dirname, { recursive: true }); 69 | await fsp.mkdir(dirname); 70 | } 71 | } 72 | 73 | module.exports = { 74 | loadWasm, 75 | loadTen, 76 | toBeCloseToArray, 77 | cleandir 78 | }; -------------------------------------------------------------------------------- /utils/py/algovivo/muscles.py: -------------------------------------------------------------------------------- 1 | from .utils import as_float_ptr, as_int_ptr 2 | import torch 3 | 4 | class Muscles: 5 | def __init__(self, lib): 6 | self.lib = lib 7 | self.indices = None 8 | self.k = float(90) 9 | self.a = None 10 | self.l0 = None 11 | 12 | def l_of_pos(self, num_vertices, pos, num_edges, edges, l0): 13 | self.lib.l0_of_pos( 14 | num_vertices, 15 | as_float_ptr(pos), 16 | num_edges, 17 | as_int_ptr(edges), 18 | as_float_ptr(l0) 19 | ) 20 | 21 | @property 22 | def num_muscles(self): 23 | if self.indices is None: 24 | return 0 25 | return self.indices.shape[0] 26 | 27 | def to_step_args(self): 28 | num_muscles = self.num_muscles 29 | return [ 30 | num_muscles, 31 | as_int_ptr(self.indices) if num_muscles > 0 else None, 32 | self.k, 33 | as_float_ptr(self.a) if num_muscles > 0 else None, 34 | as_float_ptr(self.l0) if num_muscles > 0 else None, 35 | ] 36 | 37 | def set(self, pos=None, indices=None, k=None, l0=None): 38 | if indices is None: 39 | raise ValueError("indices required") 40 | num_muscles = len(indices) 41 | 42 | if k is not None: 43 | self.k = k 44 | 45 | if pos is not None and isinstance(pos, list): 46 | pos = torch.tensor(pos, dtype=torch.float32) 47 | if pos is not None: 48 | assert isinstance(pos, torch.Tensor) 49 | 50 | if pos is None and l0 is None: 51 | raise ValueError("either pos or l0 required") 52 | 53 | indices = self.indices = torch.tensor(indices, dtype=torch.int32) 54 | 55 | if l0 is None: 56 | self.l0 = torch.empty(num_muscles, dtype=torch.float32) 57 | self.l_of_pos(num_muscles, pos, num_muscles, indices, self.l0) 58 | else: 59 | self.l0 = torch.tensor(l0, dtype=torch.float32) 60 | 61 | self.a = torch.ones(num_muscles, dtype=torch.float32) 62 | -------------------------------------------------------------------------------- /algovivo/mmgrten/Functional.js: -------------------------------------------------------------------------------- 1 | class Functional { 2 | constructor(args = {}) { 3 | const engine = this.engine = args.engine; 4 | this.wasmInstance = engine.wasmInstance; 5 | } 6 | 7 | matvec(a, b, c) { 8 | const m = a.shape.get(0); 9 | const n = a.shape.get(1); 10 | const inputSize = b.shape.get(0); 11 | const outputSize = c.shape.get(0); 12 | if (m != outputSize) { 13 | throw new Error(`inconsistent output size ${m} != ${outputSize}`); 14 | } 15 | if (n != inputSize) { 16 | throw new Error(`inconsistent input size ${n} != ${inputSize}`); 17 | } 18 | this.wasmInstance.exports.matvec( 19 | m, n, 20 | a.stride.ptr, a.ptr, 21 | b.stride.ptr, b.ptr, 22 | c.stride.ptr, c.ptr 23 | ); 24 | } 25 | 26 | mm(a, b, c) { 27 | const m = a.shape.get(0); 28 | const n = a.shape.get(1); 29 | const p = b.shape.get(1); 30 | this.wasmInstance.exports.mm( 31 | m, n, p, 32 | a.stride.ptr, a.ptr, 33 | b.stride.ptr, b.ptr, 34 | c.stride.ptr, c.ptr 35 | ); 36 | } 37 | 38 | relu(a, b) { 39 | this.wasmInstance.exports.relu( 40 | a.numel, 41 | a.ptr, 42 | b.ptr 43 | ); 44 | } 45 | 46 | tanh(a, b) { 47 | // TODO c++ version 48 | // this.wasmInstance.exports.tanh( 49 | // a.numel, 50 | // a.ptr, 51 | // b.ptr 52 | // ); 53 | const n = a.numel; 54 | const _a = a.typedArray(); 55 | const _b = b.typedArray(); 56 | for (let i = 0; i < n; i++) { 57 | _b[i] = Math.tanh(_a[i]); 58 | } 59 | } 60 | 61 | add(a, b, c) { 62 | this.wasmInstance.exports.add( 63 | a.numel, 64 | a.ptr, 65 | b.ptr, 66 | c.ptr 67 | ); 68 | } 69 | 70 | sum(a, s) { 71 | this.wasmInstance.exports.sum( 72 | a.numel, 73 | a.ptr, 74 | s.ptr 75 | ); 76 | } 77 | 78 | sumBackward(a, aGrad, s, sGrad) { 79 | this.wasmInstance.exports.sum_backward( 80 | a.numel, 81 | a.ptr, 82 | aGrad.ptr, 83 | s.ptr, 84 | sGrad.ptr 85 | ); 86 | } 87 | } 88 | 89 | module.exports = Functional; -------------------------------------------------------------------------------- /algovivo/render/mm2d/core/Mesh.js: -------------------------------------------------------------------------------- 1 | const math = require("../math"); 2 | const shaders = require("../shaders"); 3 | 4 | class Mesh { 5 | constructor(args = {}) { 6 | // if (args.scene == null) throw new Error("scene required"); 7 | // if (args.id == null) throw new Error("id required"); 8 | this.scene = args.scene; 9 | this.id = args.id; 10 | 11 | this.x = []; 12 | this.triangles = []; 13 | this.lines = []; 14 | 15 | this.pointShader = new shaders.PointShader({}); 16 | this.lineShader = new shaders.LineShader({}); 17 | this.triangleShader = new shaders.TriangleShader({}); 18 | 19 | this.customAttributes = {}; 20 | } 21 | 22 | get pos() { return this.x; } 23 | set pos(x) { this.x = x; } 24 | 25 | numVertices() { 26 | return this.pos.length; 27 | } 28 | 29 | numTriangles() { 30 | return this.triangles.length; 31 | } 32 | 33 | numLines() { 34 | return this.lines.length; 35 | } 36 | 37 | setCustomAttribute(key, value) { 38 | this.customAttributes[key] = value; 39 | } 40 | 41 | getCustomAttribute(key) { 42 | return this.customAttributes[key]; 43 | } 44 | 45 | computeAABB() { 46 | let minX = null; 47 | let maxX = null; 48 | let minY = null; 49 | let maxY = null; 50 | this.pos.forEach((pi) => { 51 | const x = pi[0]; 52 | const y = pi[1]; 53 | if (minX == null || x < minX) minX = x; 54 | if (maxX == null || x > maxX) maxX = x; 55 | if (minY == null || y < minY) minY = y; 56 | if (maxY == null || y > maxY) maxY = y; 57 | }); 58 | return new math.AABB({ 59 | x0: minX, 60 | y0: minY, 61 | x1: maxX, 62 | y1: maxY 63 | }); 64 | } 65 | 66 | computeCenter() { 67 | const numVertices = this.pos.length; 68 | if (numVertices == 0) { 69 | throw new Error("no vertices to compute center"); 70 | } 71 | 72 | let center = [0, 0]; 73 | for (let i = 0; i < numVertices; i++) { 74 | const xi = this.pos[i]; 75 | math.Vec2.add_(center, xi); 76 | } 77 | math.Vec2.mulScalar_(center, 1 / numVertices); 78 | return center; 79 | } 80 | } 81 | 82 | module.exports = Mesh; -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/Slot.js: -------------------------------------------------------------------------------- 1 | class Slot { 2 | constructor(args = {}) { 3 | this.manager = args.manager; 4 | this.ptr = args.ptr; 5 | this.size = args.size; 6 | this.node = args.node; 7 | } 8 | 9 | numBytes() { 10 | return this.size; 11 | } 12 | 13 | prev() { 14 | const prevNode = this.node.prev; 15 | if (prevNode != null) return prevNode.data; 16 | else return null; 17 | } 18 | 19 | next() { 20 | const nextNode = this.node.next; 21 | if (nextNode != null) return nextNode.data; 22 | else return null; 23 | } 24 | 25 | appendReserved(ptr, size) { 26 | const ReservedSlot = require("./ReservedSlot"); 27 | const node = this.node.append(null); 28 | const slot = new ReservedSlot({ 29 | manager: this.manager, 30 | ptr: ptr, 31 | size: size, 32 | node: node 33 | }); 34 | node.data = slot; 35 | this.manager._addReservedSlot(slot); 36 | return slot; 37 | } 38 | 39 | appendFree(ptr, size) { 40 | const FreeSlot = require("./FreeSlot"); 41 | const node = this.node.append(null); 42 | const slot = new FreeSlot({ 43 | manager: this.manager, 44 | ptr: ptr, 45 | size: size, 46 | node: node 47 | }); 48 | node.data = slot; 49 | this.manager._addFreeSlot(slot); 50 | return slot; 51 | } 52 | 53 | remove() { 54 | this.node.remove(); 55 | this.node.data = null; 56 | this.node = null; 57 | if (this.isFree()) this.manager._removeFreeSlot(this); 58 | else this.manager._removeReservedSlot(this); 59 | } 60 | 61 | toTypedArray(ArrayClass) { 62 | const bytes = this.size; 63 | const bytesPerElement = ArrayClass.BYTES_PER_ELEMENT; 64 | if (bytes % bytesPerElement != 0) { 65 | throw new Error(`size in bytes must be a multiple of ${bytesPerElement}, found ${bytes}`); 66 | } 67 | const start = this.ptr; 68 | return new ArrayClass( 69 | this.manager.array, 70 | start, 71 | bytes / bytesPerElement 72 | ); 73 | } 74 | 75 | f32() { 76 | return this.toTypedArray(Float32Array); 77 | } 78 | 79 | u32() { 80 | return this.toTypedArray(Uint32Array); 81 | } 82 | } 83 | 84 | module.exports = Slot; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/templates/optim.template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define _optim_init() { \ 4 | for (int i = 0; i < num_vertices; i++) { \ 5 | const auto offset = i * space_dim; \ 6 | for (int j = 0; j < space_dim; j++) { \ 7 | pos[offset + j] = pos0[offset + j] + h * vel0[offset + j]; \ 8 | } \ 9 | } \ 10 | } 11 | 12 | extern "C" 13 | void optim_init( 14 | int num_vertices, int space_dim, 15 | float h, 16 | const float* pos0, const float* vel0, 17 | float* pos 18 | ) { 19 | _optim_init(); 20 | } 21 | 22 | #define loss_backward() { \ 23 | zero_(num_vertices * space_dim, pos_grad); \ 24 | backward_euler_loss_grad(/* {{forward_non_differentiable_args}} */, pos, pos_grad); \ 25 | if (fixed_vertex_id != 0) { \ 26 | const auto offset = fixed_vertex_id[0] * space_dim; \ 27 | for (int j = 0; j < space_dim; j++) { \ 28 | pos_grad[offset + j] = 0.0; \ 29 | } \ 30 | } \ 31 | } 32 | 33 | #define break_if_optim_converged() { \ 34 | if (optim_converged(space_dim, num_vertices, pos_grad)) break; \ 35 | } 36 | 37 | bool optim_converged(int space_dim, int num_vertices, const float* pos_grad) { 38 | float grad_max_q = 0.0; 39 | float grad_q_tol = 0.5 * 1e-5; 40 | for (int k = 0; k < num_vertices; k++) { 41 | int offset = k * space_dim; 42 | float q = 0.0; 43 | for (int j = 0; j < space_dim; j++) { 44 | q += pos_grad[offset + j] * pos_grad[offset + j]; 45 | } 46 | if (q > grad_max_q) grad_max_q = q; 47 | } 48 | return grad_max_q < grad_q_tol; 49 | } 50 | 51 | #define optim_step() { \ 52 | float step_size = 1.0; \ 53 | const auto max_line_search_iters = 20; \ 54 | float backtracking_scale = 0.3; \ 55 | const auto loss0 = backward_euler_loss(/* {{forward_non_differentiable_args}} */, pos); \ 56 | for (int i = 0; i < max_line_search_iters; i++) { \ 57 | add_scaled(num_vertices * space_dim, pos, pos_grad, -step_size, pos_tmp); \ 58 | const auto loss1 = backward_euler_loss(/* {{forward_non_differentiable_args}} */, pos_tmp); \ 59 | if (loss1 < loss0) { \ 60 | break; \ 61 | } else { \ 62 | step_size *= backtracking_scale; \ 63 | } \ 64 | } \ 65 | add_scaled(num_vertices * space_dim, pos, pos_grad, -step_size, pos); \ 66 | } -------------------------------------------------------------------------------- /test/nn/generateTrajectory.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const fsp = require("fs/promises"); 3 | const path = require("path"); 4 | const utils = require("../utils"); 5 | 6 | const dataDirname = path.join(__dirname, "data"); 7 | 8 | async function loadMeshData() { 9 | const meshFilename = process.env.MESH_FILENAME || path.join(dataDirname, "mesh.json"); 10 | return JSON.parse(await fsp.readFile(meshFilename)); 11 | } 12 | 13 | async function loadPolicyData() { 14 | const policyFilename = process.env.POLICY_FILENAME || path.join(dataDirname, "policy.json"); 15 | return JSON.parse(await fsp.readFile(policyFilename)); 16 | } 17 | 18 | async function main() { 19 | const [wasmInstance, meshData, policyData] = await Promise.all( 20 | [utils.loadWasm, loadMeshData, loadPolicyData].map(f => f()) 21 | ); 22 | 23 | const system = new algovivo.System({ wasmInstance }); 24 | 25 | system.set({ 26 | pos: meshData.pos, 27 | muscles: meshData.muscles, 28 | musclesL0: meshData.l0, 29 | triangles: meshData.triangles, 30 | trianglesRsi: meshData.rsi 31 | }); 32 | const policy = new algovivo.nn.NeuralFramePolicy({ 33 | system: system, 34 | active: true 35 | }); 36 | policy.loadData(policyData); 37 | 38 | const outputDirname = process.env.OUTPUT_DIRNAME || path.join(__dirname, "data", "trajectory"); 39 | 40 | await utils.cleandir(outputDirname); 41 | 42 | const n = process.env.STEPS ? parseInt(process.env.STEPS, 10) : 100; 43 | for (let i = 0; i < n; i++) { 44 | console.log(`${i + 1} / ${n}`); 45 | const itemData = { 46 | pos0: system.pos0.toArray(), 47 | vel0: system.vel0.toArray(), 48 | a0: system.a.toArray() 49 | }; 50 | 51 | const policyTrace = {}; 52 | policy.step({ trace: policyTrace }); 53 | system.step(); 54 | 55 | itemData.pos1 = system.pos0.toArray(); 56 | itemData.vel1 = system.vel0.toArray(); 57 | itemData.a1 = system.a.toArray(); 58 | itemData.policy_input = policyTrace.policyInput; 59 | itemData.policy_output = policyTrace.policyOutput; 60 | 61 | const filename = `${outputDirname}/${i}.json`; 62 | await fsp.writeFile(filename, JSON.stringify(itemData, null, 2)); 63 | } 64 | 65 | console.log(`trajectory saved to ${outputDirname}`); 66 | } 67 | 68 | main(); -------------------------------------------------------------------------------- /utils/py/algovivo/vertices.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from .utils import as_float_ptr, as_int_ptr 3 | 4 | class Vertices: 5 | def __init__(self, space_dim=2): 6 | self.vertex_mass = 6.0714287757873535 7 | self.pos0 = None 8 | self.vel0 = None 9 | self.pos1 = None 10 | self.vel1 = None 11 | self.pos_grad = None 12 | self.pos_tmp = None 13 | self.space_dim = space_dim 14 | 15 | @property 16 | def pos(self): 17 | return self.pos0 18 | 19 | @property 20 | def vel(self): 21 | return self.vel0 22 | 23 | def set(self, pos): 24 | if pos is None: 25 | raise ValueError("pos required") 26 | 27 | num_vertices = len(pos) 28 | pos_tensor = torch.tensor(pos, dtype=torch.float32) 29 | 30 | if self.pos0 is not None: 31 | self.pos0 = None 32 | self.pos0 = pos_tensor 33 | 34 | if self.pos1 is not None: 35 | self.pos1 = None 36 | self.pos1 = torch.zeros((num_vertices, self.space_dim), dtype=torch.float32) 37 | 38 | if self.vel0 is not None: 39 | self.vel0 = None 40 | self.vel0 = torch.zeros((num_vertices, self.space_dim), dtype=torch.float32) 41 | 42 | if self.vel1 is not None: 43 | self.vel1 = None 44 | self.vel1 = torch.zeros((num_vertices, self.space_dim), dtype=torch.float32) 45 | 46 | self.update_tmp_buffers() 47 | 48 | def update_tmp_buffers(self): 49 | if self.pos0 is None: 50 | raise ValueError("pos0 required") 51 | 52 | num_vertices = self.pos0.shape[0] 53 | space_dim = self.space_dim 54 | 55 | self.pos_grad = torch.zeros((num_vertices, space_dim), dtype=torch.float32) 56 | self.pos_tmp = torch.zeros((num_vertices, space_dim), dtype=torch.float32) 57 | 58 | @property 59 | def num_vertices(self): 60 | if self.pos0 is None: 61 | return 0 62 | return self.pos0.shape[0] 63 | 64 | def to_step_args(self): 65 | return [ 66 | self.num_vertices, 67 | as_float_ptr(self.pos0), 68 | as_float_ptr(self.vel0), 69 | self.vertex_mass, 70 | 71 | None, # fixed vertex 72 | 73 | as_float_ptr(self.pos1), 74 | as_float_ptr(self.pos_grad), 75 | as_float_ptr(self.pos_tmp), 76 | as_float_ptr(self.vel1) 77 | ] -------------------------------------------------------------------------------- /algovivo/render/Tracker.js: -------------------------------------------------------------------------------- 1 | class Tracker { 2 | constructor(args = {}) { 3 | this.targetCenterX = null; 4 | this.currentCenterX = null; 5 | this.active = true; 6 | this.visibleWorldWidth = args.visibleWorldWidth ?? 3.8; 7 | this.targetCenterY = args.targetCenterY ?? 1; 8 | this.offsetX = args.offsetX ?? 0; 9 | this.fullGrid = false; 10 | this.centeringSpeedFactor = 0.5 ?? args.centeringSpeedFactor; 11 | } 12 | 13 | step(args = {}) { 14 | if (!this.active) return; 15 | 16 | const renderer = args.renderer; 17 | const camera = args.camera; 18 | const mesh = args.mesh; 19 | const floor = args.floor; 20 | const grid = args.grid; 21 | 22 | let meshCenter = [0, 0]; 23 | if (mesh.pos.length > 0) meshCenter = mesh.computeCenter(); 24 | const meshCenterX = meshCenter[0] + this.offsetX; 25 | 26 | if (!isNaN(meshCenterX)) this.targetCenterX = meshCenterX; 27 | 28 | if (this.currentCenterX == null) { 29 | this.currentCenterX = this.targetCenterX; 30 | } else { 31 | this.currentCenterX += (this.targetCenterX - this.currentCenterX) * this.centeringSpeedFactor; 32 | } 33 | 34 | const center = [this.currentCenterX, this.targetCenterY]; 35 | camera.center({ 36 | worldCenter: center, 37 | worldWidth: this.visibleWorldWidth, 38 | viewportWidth: renderer.width, 39 | viewportHeight: renderer.height, 40 | }); 41 | 42 | const topRight = camera.domToWorldSpace([renderer.width, 0]); 43 | const bottomLeft = camera.domToWorldSpace([0, renderer.height]); 44 | 45 | const marginCells = 1; 46 | 47 | const [_x0, _y0] = bottomLeft; 48 | const x0 = Math.floor(_x0) - marginCells; 49 | let y0 = Math.floor(_y0); 50 | if (!this.fullGrid) { 51 | if (y0 < 0) { 52 | y0 = 0; 53 | } 54 | } 55 | const [_x1, _y1] = topRight; 56 | const x1 = _x1; 57 | const y1 = _y1; 58 | 59 | const width = x1 - x0; 60 | const height = y1 - y0; 61 | const rows = Math.ceil(height) + marginCells; 62 | const cols = Math.ceil(width) + marginCells; 63 | 64 | grid.set({ 65 | x0: x0, 66 | y0: y0, 67 | rows: rows, 68 | cols: cols, 69 | 70 | innerCells: grid.innerCells, 71 | primaryLineWidth: grid.primaryLineWidth, 72 | secondaryLineWidth: grid.secondaryLineWidth 73 | }); 74 | 75 | floor.mesh.pos = [ 76 | [x0, 0], 77 | [x1, 0] 78 | ]; 79 | } 80 | } 81 | 82 | module.exports = Tracker; -------------------------------------------------------------------------------- /algovivo/render/mm2d/ui/DragBehavior.js: -------------------------------------------------------------------------------- 1 | const cursorUtils = require("./cursorUtils"); 2 | 3 | class DragBehavior { 4 | constructor(args = {}) { 5 | this._dragging = false; 6 | 7 | this.onDomCursorDown = args.onDomCursorDown; 8 | this.onDragProgress = args.onDragProgress; 9 | this.onDomCursorUp = args.onDomCursorUp; 10 | 11 | this.domElement = null; 12 | } 13 | 14 | beginDrag() { 15 | this._dragging = true; 16 | } 17 | 18 | endDrag() { 19 | this._dragging = false; 20 | } 21 | 22 | dragging() { 23 | return this._dragging; 24 | } 25 | 26 | domCursorDown(domCursor, event) { 27 | if (this.onDomCursorDown != null) this.onDomCursorDown(domCursor, event); 28 | } 29 | 30 | domCursorMove(domCursor, event) { 31 | if (!this.dragging()) return; 32 | event.preventDefault(); 33 | if (this.onDragProgress != null) this.onDragProgress(domCursor, event); 34 | } 35 | 36 | domCursorUp(domCursor, event) { 37 | this.endDrag(); 38 | if (this.onDomCursorUp != null) this.onDomCursorUp(domCursor, event); 39 | } 40 | 41 | linkToDom(domElement, domElementForMoveEvents = null) { 42 | if (this.domElement != null) { 43 | throw new Error("already linked to DOM"); 44 | } 45 | this.domElement = domElement; 46 | const onDomCursorDown = (event) => { 47 | const domCursor = cursorUtils.computeDomCursor(event, domElement); 48 | this.domCursorDown(domCursor, event); 49 | } 50 | domElement.addEventListener("mousedown", onDomCursorDown, { passive: false }); 51 | domElement.addEventListener("touchstart", onDomCursorDown, { passive: false }); 52 | 53 | const onDomCursorMove = (event) => { 54 | const domCursor = cursorUtils.computeDomCursor(event, domElement); 55 | this.domCursorMove(domCursor, event); 56 | } 57 | if (domElementForMoveEvents == null) domElementForMoveEvents = domElement; 58 | domElementForMoveEvents.addEventListener("mousemove", onDomCursorMove, { passive: false }); 59 | domElementForMoveEvents.addEventListener("touchmove", onDomCursorMove, { passive: false }); 60 | 61 | const onDomCursorUp = (event) => { 62 | const domCursor = cursorUtils.computeDomCursor(event, domElement); 63 | this.domCursorUp(domCursor, event); 64 | } 65 | window.addEventListener("mouseup", onDomCursorUp); 66 | window.addEventListener("touchend", onDomCursorUp); 67 | window.addEventListener("touchcancel", onDomCursorUp); 68 | } 69 | } 70 | 71 | module.exports = DragBehavior; -------------------------------------------------------------------------------- /.github/workflows/py.yml: -------------------------------------------------------------------------------- 1 | name: test-py 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | ENZYME_IMAGE: ghcr.io/juniorrojas/algovivo/llvm11-enzyme:latest 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up node 22 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | 27 | - name: Build JS 28 | run: | 29 | npm ci 30 | npm run build 31 | 32 | - name: Build native library 33 | run: | 34 | python codegen/codegen_csrc.py 35 | docker run \ 36 | -e NATIVE=true \ 37 | -v $(pwd):/workspace \ 38 | -w /workspace \ 39 | ${{ env.ENZYME_IMAGE }} \ 40 | ./build.sh 41 | 42 | - name: Set up python 3.11 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: 3.11 46 | 47 | - name: Install uv 48 | uses: astral-sh/setup-uv@v6 49 | 50 | - name: Install python dependencies 51 | run: | 52 | uv pip install --system -e "utils/py[dev]" 53 | 54 | - name: Test 55 | run: | 56 | export ALGOVIVO_LIB_FILENAME=$(pwd)/build/algovivo.so 57 | pytest ./utils/py/test 58 | 59 | - name: Generate trajectory 60 | run: | 61 | python utils/py/test/trajectory/test_trajectory.py 62 | 63 | - name: Build WASM 64 | run: | 65 | python codegen/codegen_csrc.py 66 | docker run \ 67 | -v $(pwd):/workspace \ 68 | -w /workspace \ 69 | ${{ env.ENZYME_IMAGE }} \ 70 | ./build.sh 71 | 72 | - name: Generate trajectory frames 73 | run: | 74 | node utils/trajectory/renderTrajectory.js \ 75 | --mesh utils/py/test/trajectory/data/mesh.json \ 76 | --steps steps.out \ 77 | --output frames.out 78 | 79 | - name: Install ffmpeg 80 | run: | 81 | sudo apt-get update 82 | sudo apt-get install ffmpeg 83 | 84 | - name: Make trajectory video 85 | run: | 86 | ffmpeg -framerate 30 -i frames.out/%d.png -pix_fmt yuv420p video.out.mp4 -y 87 | 88 | - name: Upload trajectory video 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: video 92 | path: video.out.mp4 -------------------------------------------------------------------------------- /codegen/algovivo_codegen/codegen/args.py: -------------------------------------------------------------------------------- 1 | class Arg: 2 | def __init__(self, t, name, differentiable=False, mut=False): 3 | self.t = t 4 | self.name = name 5 | self.differentiable = differentiable 6 | self.mut = mut 7 | 8 | class Args: 9 | def __init__(self): 10 | self.args = [] 11 | 12 | def __len__(self): 13 | return len(self.args) 14 | 15 | def __getitem__(self, i): 16 | return self.args[i] 17 | 18 | def add_arg(self, t, name, differentiable=False, mut=False): 19 | arg = Arg(t, name, differentiable, mut) 20 | self.args.append(arg) 21 | 22 | def codegen_fun_signature(self): 23 | s = "" 24 | num_args = len(self.args) 25 | for i, arg in enumerate(self.args): 26 | t, name = arg.t, arg.name 27 | if t[-1] == "*" and not arg.mut: 28 | s += "const " 29 | s += f"{t} {name}" 30 | if i < num_args - 1: 31 | s += ", " 32 | return s 33 | 34 | def codegen_call(self): 35 | s = "" 36 | num_args = len(self.args) 37 | for i, arg in enumerate(self.args): 38 | t, name = arg.t, arg.name 39 | s += f"{name}" 40 | if i < num_args - 1: 41 | s += ", " 42 | return s 43 | 44 | def codegen_enzyme_call(self): 45 | s = "" 46 | num_args = len(self.args) 47 | for i, arg in enumerate(self.args): 48 | t, name = arg.t, arg.name 49 | if not arg.differentiable: 50 | s += f"enzyme_const, {name}" 51 | else: 52 | s += f"enzyme_dup, {name}, {name}_grad" 53 | if i < num_args - 1: 54 | s += ",\n" 55 | return s 56 | 57 | def codegen_struct_attrs(self): 58 | s = "" 59 | for arg in self.args: 60 | t, name = arg.t, arg.name 61 | if t[-1] == "*" and not arg.mut: 62 | s += "const " 63 | s += f"{t} {name};\n" 64 | return s 65 | 66 | def codegen_struct_set(self, struct_name): 67 | s = "" 68 | for arg in self.args: 69 | t, name = arg.t, arg.name 70 | s += f"{struct_name}.{name} = {name};\n" 71 | return s 72 | 73 | def with_tangent_args(self): 74 | new_args = Args() 75 | for arg in self.args: 76 | new_args.add_arg(arg.t, arg.name) 77 | if arg.differentiable: 78 | new_args.add_arg(f"{arg.t}", f"{arg.name}_grad") 79 | return new_args -------------------------------------------------------------------------------- /.github/workflows/build-enzyme-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: build-enzyme-docker-image 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | latest: 7 | description: 'Tag as latest' 8 | type: boolean 9 | required: false 10 | default: false 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | platform: [amd64, arm64] 18 | permissions: 19 | contents: read 20 | packages: write 21 | steps: 22 | - name: Clone repo 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Log in to GitHub Container Registry 32 | uses: docker/login-action@v3 33 | with: 34 | registry: ghcr.io 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Build and push ${{ matrix.platform }} 39 | uses: docker/build-push-action@v5 40 | with: 41 | context: . 42 | file: enzyme/Dockerfile-llvm11 43 | platforms: linux/${{ matrix.platform }} 44 | push: true 45 | tags: ghcr.io/${{ github.repository }}/llvm11-enzyme:${{ github.sha }}-${{ matrix.platform }} 46 | cache-from: type=gha,scope=${{ matrix.platform }} 47 | cache-to: type=gha,mode=max,scope=${{ matrix.platform }} 48 | 49 | create-manifest: 50 | runs-on: ubuntu-latest 51 | needs: [build] 52 | permissions: 53 | contents: read 54 | packages: write 55 | steps: 56 | - name: Log in to GitHub Container Registry 57 | uses: docker/login-action@v3 58 | with: 59 | registry: ghcr.io 60 | username: ${{ github.actor }} 61 | password: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | - name: Create and push SHA manifest 64 | run: | 65 | docker buildx imagetools create -t ghcr.io/${{ github.repository }}/llvm11-enzyme:${{ github.sha }} \ 66 | ghcr.io/${{ github.repository }}/llvm11-enzyme:${{ github.sha }}-amd64 \ 67 | ghcr.io/${{ github.repository }}/llvm11-enzyme:${{ github.sha }}-arm64 68 | 69 | - name: Create and push latest manifest 70 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.latest == true }} 71 | run: | 72 | docker buildx imagetools create -t ghcr.io/${{ github.repository }}/llvm11-enzyme:latest \ 73 | ghcr.io/${{ github.repository }}/llvm11-enzyme:${{ github.sha }}-amd64 \ 74 | ghcr.io/${{ github.repository }}/llvm11-enzyme:${{ github.sha }}-arm64 -------------------------------------------------------------------------------- /utils/py/algovivo/triangles.py: -------------------------------------------------------------------------------- 1 | from .utils import as_float_ptr, as_int_ptr 2 | import torch 3 | import ctypes 4 | 5 | class Triangles: 6 | def __init__(self, lib): 7 | self.lib = lib 8 | self.indices = None 9 | self.rsi = None 10 | self.mu = None 11 | self.lambd = None 12 | self.simplex_order = 3 13 | 14 | def ctypes_argtypes(self): 15 | return [ 16 | ctypes.c_int, # num_triangles 17 | ctypes.POINTER(ctypes.c_int), # indices 18 | ctypes.POINTER(ctypes.c_float), # rsi 19 | ctypes.POINTER(ctypes.c_float), # mu 20 | ctypes.POINTER(ctypes.c_float), # lambda 21 | ] 22 | 23 | def to_step_args(self): 24 | return [ 25 | self.num_triangles, 26 | as_int_ptr(self.indices), 27 | as_float_ptr(self.rsi), 28 | as_float_ptr(self.mu), 29 | as_float_ptr(self.lambd), 30 | ] 31 | 32 | @property 33 | def num_elements(self): 34 | if self.indices is None: 35 | return 0 36 | return self.indices.shape[0] 37 | 38 | @property 39 | def num_triangles(self): 40 | return self.num_elements 41 | 42 | def set(self, indices=None, pos=None, rsi=None): 43 | if pos is not None and isinstance(pos, list): 44 | pos = torch.tensor(pos, dtype=torch.float32) 45 | if pos is not None: 46 | assert isinstance(pos, torch.Tensor) 47 | 48 | self.indices = torch.tensor(indices, dtype=torch.int32) 49 | 50 | if rsi is not None and isinstance(rsi, list): 51 | rsi = torch.tensor(rsi, dtype=torch.float32) 52 | 53 | if rsi is not None: 54 | assert isinstance(rsi, torch.Tensor) 55 | assert rsi.shape == (self.num_triangles, self.simplex_order - 1, self.simplex_order - 1) 56 | self.rsi = rsi 57 | else: 58 | num_vertices = len(pos) 59 | 60 | self.rsi = torch.zeros( 61 | self.num_triangles, 62 | self.simplex_order - 1, 63 | self.simplex_order - 1, 64 | dtype=torch.float32 65 | ) 66 | 67 | self.lib.rsi_of_pos( 68 | num_vertices, 69 | as_float_ptr(pos), 70 | self.num_triangles, 71 | as_int_ptr(self.indices), 72 | as_float_ptr(self.rsi), 73 | ) 74 | 75 | self.mu = torch.empty(self.num_triangles, dtype=torch.float32) 76 | self.mu.fill_(500.0) 77 | 78 | self.lambd = torch.empty(self.num_triangles, dtype=torch.float32) 79 | self.lambd.fill_(5.0) -------------------------------------------------------------------------------- /demo/scripts/deploy-local: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pathlib import Path 3 | import os 4 | this_filepath = Path(os.path.abspath(__file__)) 5 | this_dirpath = this_filepath.parent 6 | import sys 7 | sys.path.insert(0, str(this_dirpath)) 8 | import shutil 9 | import argparse 10 | from datetime import datetime 11 | from glob import glob 12 | 13 | def clean_deploy_dir(deploy_path): 14 | # remove all existing files in deploy_path (from last deployment), except .git 15 | for filename in os.listdir(deploy_path): 16 | if filename == ".git": 17 | continue 18 | full_filename = deploy_path.joinpath(filename) 19 | if os.path.isdir(full_filename): 20 | shutil.rmtree(full_filename) 21 | else: 22 | os.remove(full_filename) 23 | 24 | def transfer_dir(src_dirname, dst_dirname): 25 | gitignore_filename = os.path.join(src_dirname, ".gitignore") 26 | if os.path.exists(gitignore_filename): 27 | shutil.copy( 28 | gitignore_filename, 29 | os.path.join(dst_dirname, ".gitignore") 30 | ) 31 | 32 | for filename in ( 33 | glob(os.path.join(src_dirname, "*.html")) + 34 | glob(os.path.join(src_dirname, "*.css")) + 35 | glob(os.path.join(src_dirname, "*.js")) + 36 | glob(os.path.join(src_dirname, "*.wasm")) + 37 | glob(os.path.join(src_dirname, "*.json")) + 38 | glob(os.path.join(src_dirname, "*.svg")) 39 | ): 40 | shutil.copy( 41 | filename, 42 | os.path.join(dst_dirname, os.path.basename(filename)) 43 | ) 44 | 45 | for dirname in os.listdir(src_dirname): 46 | if os.path.isdir(os.path.join(src_dirname, dirname)): 47 | os.makedirs(os.path.join(dst_dirname, dirname), exist_ok=True) 48 | transfer_dir( 49 | os.path.join(src_dirname, dirname), 50 | os.path.join(dst_dirname, dirname) 51 | ) 52 | 53 | 54 | def populate_deploy_path(deploy_dirname): 55 | public_dirname = str(this_dirpath.parent.joinpath("public")) 56 | transfer_dir(public_dirname, str(deploy_dirname)) 57 | 58 | with open(deploy_path.joinpath("deploy_info.txt"), "w") as f: 59 | f.write(str(datetime.now().timestamp())) 60 | 61 | if __name__ == "__main__": 62 | arg_parser = argparse.ArgumentParser() 63 | arg_parser.add_argument("--deploy-dirname", type=str, default=str(this_dirpath.parent.joinpath("deploy.out"))) 64 | args = arg_parser.parse_args() 65 | 66 | os.makedirs(args.deploy_dirname, exist_ok=True) 67 | deploy_path = Path(args.deploy_dirname) 68 | clean_deploy_dir(deploy_path) 69 | populate_deploy_path(deploy_path) 70 | print(args.deploy_dirname) -------------------------------------------------------------------------------- /test/nn/trajectory.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("../../algovivo"); 2 | const utils = require("../utils"); 3 | const fsp = require("fs/promises"); 4 | const path = require("path"); 5 | const Trajectory = require("../../utils/trajectory/Trajectory"); 6 | 7 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 8 | 9 | const dataDirname = path.join(__dirname, "data"); 10 | 11 | async function loadMeshData() { 12 | const meshFilename = path.join(dataDirname, "mesh.json"); 13 | return JSON.parse(await fsp.readFile(meshFilename)); 14 | } 15 | 16 | async function loadPolicyData() { 17 | const policyFilename = path.join(dataDirname, "policy.json"); 18 | return JSON.parse(await fsp.readFile(policyFilename)); 19 | } 20 | 21 | test("neural frame policy", async () => { 22 | const [wasmInstance, meshData, policyData] = await Promise.all( 23 | [utils.loadWasm, loadMeshData, loadPolicyData].map(f => f()) 24 | ); 25 | 26 | const system = new algovivo.System({ wasmInstance }); 27 | 28 | system.set({ 29 | pos: meshData.pos, 30 | muscles: meshData.muscles, 31 | musclesL0: meshData.l0, 32 | triangles: meshData.triangles, 33 | trianglesRsi: meshData.rsi 34 | }); 35 | expect(system.l0.toArray()).toEqual(meshData.l0); 36 | expect(system.rsi.toArray()).toEqual(meshData.rsi); 37 | const policy = new algovivo.nn.NeuralFramePolicy({ 38 | system: system, 39 | stochastic: false, 40 | active: true 41 | }); 42 | policy.loadData(policyData); 43 | 44 | const trajectoryDirname = path.join(dataDirname, "trajectory"); 45 | const trajectory = new Trajectory(trajectoryDirname); 46 | 47 | let expectedNumReservedBytes = null; 48 | const mgr = system.memoryManager; 49 | 50 | const n = await trajectory.numSteps(); 51 | expect(n).toBe(100); 52 | for (let i = 0; i < n; i++) { 53 | const data = await trajectory.loadStep(i); 54 | 55 | system.pos0.set(data.pos0); 56 | system.vel0.set(data.vel0); 57 | system.a.set(data.a0); 58 | 59 | const policyTrace = {}; 60 | policy.step({ trace: policyTrace }); 61 | system.step(); 62 | 63 | if (i == 0) expectedNumReservedBytes = mgr.numReservedBytes(); 64 | expect(expectedNumReservedBytes).not.toBeNull(); 65 | expect(expectedNumReservedBytes).toBeGreaterThan(0); 66 | expect(mgr.numReservedBytes()).toBe(expectedNumReservedBytes); 67 | 68 | expect(policyTrace.policyInput).toBeCloseToArray(data.policy_input); 69 | expect(policyTrace.policyOutput).toBeCloseToArray(data.policy_output); 70 | expect(system.pos0.toArray()).toBeCloseToArray(data.pos1); 71 | expect(system.vel0.toArray()).toBeCloseToArray(data.vel1); 72 | expect(system.a.toArray()).toBeCloseToArray(data.a1); 73 | } 74 | }); -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import algovivo from "../../build/algovivo.mjs"; 2 | import BrainButton from "./BrainButton.js"; 3 | import { initStyle } from "./ui.js"; 4 | import AgentViewport from "./AgentViewport.js"; 5 | import Header from "./Header.js"; 6 | import Sections from "./Sections.js"; 7 | import Footer from "./Footer.js"; 8 | 9 | async function loadWasm() { 10 | const response = await fetch("algovivo.wasm"); 11 | const wasm = await WebAssembly.instantiateStreaming(response); 12 | return wasm.instance; 13 | } 14 | 15 | const dataRoot = "data"; 16 | const agentNames = ["biped", "quadruped"]; 17 | 18 | async function main() { 19 | initStyle(); 20 | 21 | const header = new Header(); 22 | document.body.appendChild(header.domElement); 23 | 24 | const divContent = document.createElement("div"); 25 | divContent.style.display = "flex"; 26 | divContent.style.flexDirection = "column"; 27 | divContent.style.alignItems = "center"; 28 | divContent.style.width = "100%"; 29 | document.body.appendChild(divContent); 30 | 31 | document.documentElement.style.height = "100%"; 32 | document.body.style.height = "100%"; 33 | document.body.style.display = "flex"; 34 | document.body.style.flexDirection = "column"; 35 | document.body.style.margin = 0; 36 | document.body.style.padding = 0; 37 | document.body.style.alignItems = "center"; 38 | 39 | const wasmInstance = await loadWasm(); 40 | const system = new algovivo.System({ 41 | wasmInstance: wasmInstance 42 | }); 43 | 44 | const agentViewport = new AgentViewport({ 45 | system: system, 46 | algovivo: algovivo, 47 | dataRoot: dataRoot, 48 | agentNames: agentNames 49 | }); 50 | divContent.appendChild(agentViewport.domElement); 51 | 52 | await agentViewport.preloadMiniButtonData(); 53 | await agentViewport.switchToAgent("biped"); 54 | 55 | const btnBrain = new BrainButton(); 56 | btnBrain.domElement.style.marginTop = "8px"; 57 | btnBrain.domElement.style.marginBottom = "16px"; 58 | btnBrain.domElement.addEventListener("click", () => { 59 | const isActive = agentViewport.togglePolicy(); 60 | if (isActive) btnBrain.setActiveStyle(); 61 | else btnBrain.setInactiveStyle(); 62 | }); 63 | divContent.appendChild(btnBrain.domElement); 64 | 65 | const sections = new Sections(); 66 | divContent.appendChild(sections.domElement); 67 | 68 | const footer = new Footer(); 69 | document.body.appendChild(footer.domElement); 70 | 71 | agentViewport.render(); 72 | setInterval(() => { 73 | if (agentViewport.agentManager.policy != null) { 74 | agentViewport.agentManager.policy.step(); 75 | } 76 | system.step(); 77 | agentViewport.render(); 78 | }, 1000 / 30); 79 | 80 | window.system = system; 81 | } 82 | 83 | main(); -------------------------------------------------------------------------------- /algovivo/render/mm2d/sorted/index.js: -------------------------------------------------------------------------------- 1 | const MeshTopology = require("./MeshTopology"); 2 | const Simplices = require("./Simplices"); 3 | 4 | function makeSortedElements(args = {}) { 5 | if (args.sortedVertexIds == null) { 6 | throw new Error("sortedVertexIds required"); 7 | } 8 | if (args.triangles == null) { 9 | throw new Error("triangles required"); 10 | } 11 | if (args.edges == null) { 12 | throw new Error("edges required"); 13 | } 14 | const sortedVertexIds = args.sortedVertexIds; 15 | 16 | const vertexIdToOrder = new Map(); 17 | sortedVertexIds.forEach((id, order) => { 18 | vertexIdToOrder.set(id, order); 19 | }); 20 | 21 | const triangleTopology = new MeshTopology({ 22 | triangles: args.triangles 23 | }); 24 | const edgeTopology = new MeshTopology({ 25 | edges: args.edges 26 | }); 27 | 28 | const sortedElements = []; 29 | const trianglesAdded = new Simplices({ order: 3 }); 30 | const edgesAdded = new Simplices({ order: 2 }); 31 | 32 | sortedVertexIds.forEach((vertexId) => { 33 | const triangles = triangleTopology.getVertexById(vertexId, true).triangles; 34 | const edges = edgeTopology.getVertexById(vertexId, true).edges; 35 | 36 | const sortedSimplices = []; 37 | triangles.forEach(t => { 38 | sortedSimplices.push(t); 39 | }); 40 | edges.forEach(e => { 41 | sortedSimplices.push(e); 42 | }); 43 | 44 | sortedSimplices.sort((a, b) => { 45 | // TODO max order vertex could be pre-sorted in the simplex 46 | const aOrders = a.vertexIds.map(i => vertexIdToOrder.get(i)); 47 | const bOrders = b.vertexIds.map(i => vertexIdToOrder.get(i)); 48 | const ai1 = Math.max(...aOrders); 49 | const bi1 = Math.max(...bOrders); 50 | if (ai1 < bi1) { 51 | return 1; 52 | } else 53 | if (ai1 == bi1) { 54 | return 0; 55 | } else { 56 | return -1; 57 | } 58 | }); 59 | 60 | sortedSimplices.forEach(simplex => { 61 | if (simplex.order == 2) { 62 | const edge = simplex; 63 | if (!edgesAdded.has(edge)) { 64 | sortedElements.push(edge); 65 | edgesAdded.add(edge); 66 | } 67 | } else { 68 | const triangle = simplex; 69 | if (!trianglesAdded.has(triangle)) { 70 | sortedElements.push(triangle); 71 | trianglesAdded.add(triangle); 72 | } 73 | } 74 | }); 75 | 76 | sortedElements.push({ 77 | order: 1, 78 | id: vertexId 79 | }); 80 | }); 81 | return sortedElements; 82 | } 83 | 84 | module.exports = { 85 | makeSortedElements: makeSortedElements, 86 | MeshTopology: require("./MeshTopology"), 87 | Simplex: require("./Simplex"), 88 | Simplices: require("./Simplices") 89 | }; -------------------------------------------------------------------------------- /test/vertices.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("set vertices", async () => { 7 | const ten = await utils.loadTen(); 8 | const vertices = new algovivo.Vertices({ ten }); 9 | expect(vertices.numVertices).toBe(0); 10 | 11 | vertices.set([ 12 | [0, 0], 13 | [3, 0], 14 | [2, 4] 15 | ]); 16 | 17 | expect(vertices.numVertices).toBe(3); 18 | expect(vertices.pos.toArray()).toBeCloseToArray([ 19 | [0, 0], 20 | [3, 0], 21 | [2, 4] 22 | ]); 23 | }); 24 | 25 | test("set system vertices", async () => { 26 | const ten = await utils.loadTen(); 27 | const system = new algovivo.System({ ten }); 28 | expect(system.numVertices).toBe(0); 29 | 30 | system.setVertices([ 31 | [0, 0], 32 | [3, 0], 33 | [2, 4] 34 | ]); 35 | 36 | expect(system.numVertices).toBe(3); 37 | expect(system.pos.toArray()).toBeCloseToArray([ 38 | [0, 0], 39 | [3, 0], 40 | [2, 4] 41 | ]); 42 | }); 43 | 44 | test("add vertex", async () => { 45 | const ten = await utils.loadTen(); 46 | const memoryManager = ten.mgr; 47 | 48 | const vertices = new algovivo.Vertices({ ten, spaceDim: 2 }); 49 | expect(vertices.numVertices).toBe(0); 50 | 51 | expect(memoryManager.numReservedBytes()).toBe(0); 52 | 53 | vertices.addVertex({ pos: [1, 2], vel: [3, 4]}); 54 | expect(vertices.numVertices).toBe(1); 55 | expect(vertices.pos.toArray()).toBeCloseToArray([[1, 2]]); 56 | expect(vertices.vel.toArray()).toBeCloseToArray([[3, 4]]); 57 | expect(memoryManager.numReservedBytes()).not.toBe(0); 58 | const reservedBytesForOneVertex = memoryManager.numReservedBytes(); 59 | 60 | vertices.setVertexPos(0, [11, 22]); 61 | expect(vertices.pos.toArray()).toBeCloseToArray([[11, 22]]); 62 | 63 | vertices.dispose(); 64 | vertices.addVertex(); 65 | expect(vertices.numVertices).toBe(1); 66 | expect(vertices.pos.toArray()).toBeCloseToArray([[0, 0]]); 67 | expect(vertices.vel.toArray()).toBeCloseToArray([[0, 0]]); 68 | expect(memoryManager.numReservedBytes()).toBe(reservedBytesForOneVertex); 69 | 70 | vertices.addVertex(); 71 | expect(vertices.numVertices).toBe(2); 72 | expect(memoryManager.numReservedBytes()).toBeGreaterThan(reservedBytesForOneVertex); 73 | const reservedDataBytesForOneVertex = memoryManager.numReservedBytes() - reservedBytesForOneVertex; 74 | const reservedMetadataBytesForOneVertex = reservedBytesForOneVertex - reservedDataBytesForOneVertex; 75 | 76 | vertices.addVertex(); 77 | expect(vertices.numVertices).toBe(3); 78 | expect(memoryManager.numReservedBytes()).toBe(reservedMetadataBytesForOneVertex + 3 * reservedDataBytesForOneVertex); 79 | 80 | vertices.dispose(); 81 | expect(memoryManager.numReservedBytes()).toBe(0); 82 | }); -------------------------------------------------------------------------------- /test/frameProjection.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const mmgrten = algovivo.mmgrten; 3 | const { loadWasm, loadTen } = require("./utils"); 4 | const utils = require("./utils"); 5 | 6 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 7 | 8 | test("dot2d", async () => { 9 | const wasmInstance = await loadWasm(); 10 | const r = wasmInstance.exports.dot2d( 11 | 1, 2, 12 | 3, 4 13 | ); 14 | expect(r).toBe(11); 15 | }); 16 | 17 | test("frame_projection simple", async () => { 18 | const ten = await loadTen(); 19 | 20 | const numVertices = 2; 21 | const spaceDim = 2; 22 | 23 | const x = ten.tensor([ 24 | [0, 0], 25 | [1, 0] 26 | ]); 27 | 28 | const projectedX = ten.zeros([numVertices, spaceDim]); 29 | 30 | const centerId = 0; 31 | const forwardId = 1; 32 | 33 | ten.wasmInstance.exports.frame_projection( 34 | numVertices, 35 | x.ptr, 36 | centerId, 37 | forwardId, 38 | x.ptr, 39 | projectedX.ptr, 40 | false 41 | ); 42 | 43 | expect(projectedX.toArray()).toEqual([ 44 | [0, 0], 45 | [1, 0] 46 | ]); 47 | }); 48 | 49 | test("frame_projection", async () => { 50 | const ten = await loadTen(); 51 | 52 | const x = ten.tensor([ 53 | [-1.3, 2.7], 54 | [2.3, 0.9], 55 | [1, 1.3] 56 | ]); 57 | 58 | const numVertices = x.shape.get(0); 59 | const spaceDim = x.shape.get(1); 60 | 61 | const projectedX = ten.zeros([numVertices, spaceDim]); 62 | 63 | const centerId = 1; 64 | const forwardId = 2; 65 | 66 | const subtractPositon = true; 67 | const clockwise = true; 68 | 69 | ten.wasmInstance.exports.frame_projection( 70 | numVertices, 71 | x.ptr, 72 | centerId, 73 | forwardId, 74 | x.ptr, 75 | projectedX.ptr, 76 | subtractPositon, 77 | clockwise 78 | ); 79 | 80 | const result = projectedX.toArray(); 81 | 82 | expect(result).toBeCloseToArray([ 83 | [3.9701590538024902, 0.6616933345794678], 84 | [0, 0], 85 | [1.3601469993591309, 2.9802322387695312e-8] 86 | ]); 87 | 88 | x.dispose(); 89 | projectedX.dispose(); 90 | }); 91 | 92 | test("make_policy_input", async () => { 93 | const ten = mmgrten.engine({ 94 | wasmInstance: await loadWasm() 95 | }); 96 | 97 | const numVertices = 3; 98 | const spaceDim = 2; 99 | 100 | const x = ten.tensor([ 101 | [1, 2], 102 | [3, 4], 103 | [5, 6] 104 | ]); 105 | 106 | const v = ten.tensor([ 107 | [7, 8], 108 | [9, 10], 109 | [11, 12] 110 | ]); 111 | 112 | const policyInput = ten.zeros([spaceDim * numVertices * 2]); 113 | 114 | ten.wasmInstance.exports.cat_pos_vel( 115 | numVertices, 116 | x.ptr, 117 | v.ptr, 118 | policyInput.ptr 119 | ); 120 | 121 | expect(policyInput.toArray()).toEqual([ 122 | 1, 2, 3, 4, 5, 6, 123 | 7, 8, 9, 10, 11, 12 124 | ]); 125 | }); -------------------------------------------------------------------------------- /algovivo/mmgrten/Engine.js: -------------------------------------------------------------------------------- 1 | const mmgr = require("./mmgr"); 2 | const utils = require("./utils"); 3 | const Tensor = require("./Tensor"); 4 | const IntTuple = require("./IntTuple"); 5 | const Functional = require("./Functional"); 6 | const nn = require("./nn"); 7 | 8 | class Engine { 9 | constructor(args = {}) { 10 | if (args.wasmInstance != null) this.init(args); 11 | } 12 | 13 | init(args = {}) { 14 | if (args.wasmInstance == null) { 15 | throw new Error("wasmInstance required"); 16 | } 17 | this.wasmInstance = args.wasmInstance; 18 | const arr = args.wasmInstance.exports.memory.buffer; 19 | const mgr = new mmgr.MemoryManager(arr, args.wasmInstance.exports.__heap_base); 20 | this.mgr = mgr; 21 | 22 | this.functional = this.F = new Functional({ 23 | engine: this 24 | }); 25 | this.nn = new nn({ 26 | engine: this 27 | }); 28 | this._mergeF(); 29 | } 30 | 31 | _mergeF() { 32 | Object.getOwnPropertyNames(Object.getPrototypeOf(this.F)).forEach(k => { 33 | if (k != "constructor") { 34 | this[k] = this.F[k]; 35 | } 36 | }) 37 | } 38 | 39 | tensor(data) { 40 | const shapeArr = utils.inferShape(data); 41 | const shape = this.intTuple(shapeArr); 42 | const numel = utils.numelOfShape(shapeArr); 43 | const slot = this.mgr.malloc32(numel); 44 | const tensor = new Tensor({ 45 | engine: this, 46 | shape: shape, 47 | slot: slot 48 | }); 49 | tensor.setFromArray(data); 50 | return tensor; 51 | } 52 | 53 | intTuple(data) { 54 | if (!Array.isArray(data)) { 55 | throw new Error(`expected array, found ${typeof data}: ${data}`); 56 | } 57 | const length = data.length; 58 | const slot = this.mgr.malloc32(length); 59 | const intTuple = new IntTuple({ 60 | engine: this, 61 | length: length, 62 | slot: slot 63 | }); 64 | for (let i = 0; i < length; i++) { 65 | intTuple.set(i, data[i]); 66 | } 67 | return intTuple; 68 | } 69 | 70 | zerosLike(x) { 71 | if (!(x instanceof Tensor)) { 72 | throw new Error(`expected tensor, found ${typeof x}: ${x}`); 73 | } 74 | return this.zeros(x.shape.toArray()); 75 | } 76 | 77 | empty(_shape) { 78 | let shape; 79 | if (_shape instanceof IntTuple) { 80 | shape = _shape; 81 | } else { 82 | if (!Array.isArray(_shape)) { 83 | throw new Error(`expected array, found ${typeof _shape}: ${_shape}`); 84 | } 85 | shape = this.intTuple(_shape); 86 | } 87 | const numel = utils.numelOfShape(shape); 88 | const slot = this.mgr.malloc32(numel); 89 | const x = new Tensor({ 90 | engine: this, 91 | shape: shape, 92 | slot: slot 93 | }); 94 | return x; 95 | } 96 | 97 | zeros(shape) { 98 | const x = this.empty(shape); 99 | x.zero_(); 100 | return x; 101 | } 102 | 103 | ones(shape) { 104 | const x = this.empty(shape); 105 | x.fill_(1); 106 | return x; 107 | } 108 | } 109 | 110 | module.exports = Engine; -------------------------------------------------------------------------------- /demo/src/AgentMini.js: -------------------------------------------------------------------------------- 1 | export default class AgentMini { 2 | constructor(args = {}) { 3 | if (!args.mm2d) throw new Error("mm2d required"); 4 | 5 | this.mm2d = args.mm2d; 6 | this.shapeColor = args.shapeColor ?? "white"; 7 | this.size = args.size ?? 50; 8 | this.worldWidth = args.worldWidth ?? 5.2; 9 | 10 | this.initContainer(); 11 | this.initRenderer(); 12 | this.initMesh(args); 13 | this.initRenderers(); 14 | this.render(); 15 | this.setActive(false); 16 | } 17 | 18 | initContainer() { 19 | this.domElement = document.createElement("div"); 20 | this.domElement.style.borderRadius = "50px"; 21 | this.domElement.style.width = `${this.size}px`; 22 | this.domElement.style.height = `${this.size}px`; 23 | this.domElement.style.pointerEvents = "auto"; 24 | } 25 | 26 | initRenderer() { 27 | this.renderer = new this.mm2d.Renderer(); 28 | this.renderer.setSize({ width: this.size, height: this.size }); 29 | this.scene = new this.mm2d.Scene(); 30 | this.camera = new this.mm2d.Camera(); 31 | this.domElement.appendChild(this.renderer.domElement); 32 | } 33 | 34 | initMesh(args) { 35 | this.mesh = this.scene.addMesh(); 36 | this.mesh.pos = args.pos || []; 37 | this.mesh.triangles = args.triangles || []; 38 | } 39 | 40 | initRenderers() { 41 | this.mesh.pointShader.renderPoint = () => {}; 42 | 43 | this.mesh.triangleShader.renderTriangle = (args) => { 44 | this.renderTriangle(args); 45 | }; 46 | } 47 | 48 | renderTriangle(args) { 49 | const borderWidth = 0.029; 50 | const ctx = args.ctx; 51 | const [a, b, c] = [args.a, args.b, args.c]; 52 | const scale = args.camera.inferScale(); 53 | 54 | ctx.beginPath(); 55 | ctx.lineJoin = "round"; 56 | ctx.lineCap = "round"; 57 | ctx.strokeStyle = this.shapeColor; 58 | ctx.lineWidth = (borderWidth * 2) * scale; 59 | ctx.moveTo(a[0], a[1]); 60 | ctx.lineTo(b[0], b[1]); 61 | ctx.lineTo(c[0], c[1]); 62 | ctx.closePath(); 63 | ctx.stroke(); 64 | 65 | ctx.beginPath(); 66 | ctx.fillStyle = this.shapeColor; 67 | ctx.moveTo(a[0], a[1]); 68 | ctx.lineTo(b[0], b[1]); 69 | ctx.lineTo(c[0], c[1]); 70 | ctx.closePath(); 71 | ctx.fill(); 72 | } 73 | 74 | updateMesh({ pos, triangles }) { 75 | if (pos) this.mesh.pos = pos; 76 | if (triangles) this.mesh.triangles = triangles; 77 | this.render(); 78 | } 79 | 80 | setActive(active = true) { 81 | if (active) { 82 | this.domElement.style.backgroundColor = "rgba(0, 0, 0, 1)"; 83 | } else { 84 | this.domElement.style.backgroundColor = "rgba(0, 0, 0, 0.2)"; 85 | } 86 | } 87 | 88 | setColor(color) { 89 | this.shapeColor = color; 90 | this.render(); 91 | } 92 | 93 | render() { 94 | if (this.mesh.pos && this.mesh.pos.length > 0) { 95 | const center = this.mesh.computeCenter(); 96 | this.camera.center({ 97 | worldCenter: center, 98 | worldWidth: this.worldWidth, 99 | viewportWidth: this.renderer.width, 100 | viewportHeight: this.renderer.height, 101 | }); 102 | } 103 | this.renderer.render(this.scene, this.camera); 104 | } 105 | } -------------------------------------------------------------------------------- /algovivo/render/VertexRenderer.js: -------------------------------------------------------------------------------- 1 | const mm2d = require("./mm2d"); 2 | 3 | function renderCircle(ctx, scale, p, radius, borderWidth, borderColor, fillColor) { 4 | const radius1 = (radius + borderWidth * 0.5) * scale; 5 | 6 | ctx.fillStyle = fillColor; 7 | ctx.beginPath(); 8 | ctx.arc(p[0], p[1], radius1, 0, 2 * Math.PI); 9 | ctx.fill(); 10 | 11 | ctx.lineWidth = borderWidth * scale; 12 | ctx.strokeStyle = borderColor; 13 | ctx.stroke(); 14 | } 15 | 16 | class VertexRenderer { 17 | constructor(args = {}) { 18 | this.system = args.system; 19 | this.renderVertexIds = args.renderVertexIds ?? false; 20 | this.radius = args.radius ?? 0.028; 21 | this.borderColor = args.borderColor ?? "black"; 22 | this.fillColor = args.fillColor ?? "white"; 23 | this.borderWidth = args.borderWidth ?? 0.023; 24 | } 25 | 26 | renderVertex(args = {}) { 27 | const radius = this.radius; 28 | const borderColor = this.borderColor; 29 | const fillColor = this.fillColor; 30 | const borderWidth = this.borderWidth; 31 | 32 | const ctx = args.ctx; 33 | const p = args.p; 34 | const camera = args.camera; 35 | const scale = camera.inferScale(); 36 | 37 | renderCircle(ctx, scale, p, radius, borderWidth, borderColor, fillColor); 38 | 39 | if (this.renderVertexIds) { 40 | ctx.beginPath(); 41 | ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; 42 | ctx.arc(p[0], p[1], 0.1 * scale, 0, 2 * Math.PI); 43 | ctx.fill(); 44 | 45 | const fontSize = Math.floor(0.15 * scale); 46 | ctx.font = `${fontSize}px monospace`; 47 | ctx.fillStyle = "black"; 48 | ctx.textAlign = "center"; 49 | ctx.textBaseline = "middle"; 50 | ctx.fillText(args.id, p[0], p[1]); 51 | } 52 | } 53 | 54 | getVertexPos(i) { 55 | const pF32 = this.system.pos.slot.f32(); 56 | const offset = i * this.system.spaceDim; 57 | return [pF32[offset], pF32[offset + 1]]; 58 | } 59 | 60 | get numVertices() { 61 | return this.system.numVertices; 62 | } 63 | 64 | hitTest(p, hitTestRadius = 0.31) { 65 | const numVertices = this.numVertices; 66 | if (numVertices == 0) return null; 67 | let closestVertexId = null; 68 | let closestQuadrance = Infinity; 69 | const hitTestRadius2 = hitTestRadius * hitTestRadius; 70 | for (let i = 0; i < numVertices; i++) { 71 | const pi = this.getVertexPos(i); 72 | const d = mm2d.math.Vec2.sub(pi, p); 73 | const q = mm2d.math.Vec2.quadrance(d); 74 | if (q < hitTestRadius2 && q < closestQuadrance) { 75 | closestVertexId = i; 76 | closestQuadrance = q; 77 | } 78 | } 79 | return closestVertexId; 80 | } 81 | 82 | setVertexPos(i, p) { 83 | if (i == null) throw new Error("vertex id required"); 84 | const system = this.system; 85 | const pF32 = system.pos.slot.f32(); 86 | const offset = i * 2; 87 | pF32[offset] = p[0]; 88 | pF32[offset + 1] = p[1]; 89 | } 90 | 91 | setVertexVel(i, v) { 92 | const system = this.system; 93 | const vF32 = system.vel.slot.f32(); 94 | const offset = i * 2; 95 | vF32[offset] = v[0]; 96 | vF32[offset + 1] = v[1]; 97 | } 98 | } 99 | 100 | module.exports = VertexRenderer; -------------------------------------------------------------------------------- /algovivo/mmgrten/mmgr/MemoryManager.js: -------------------------------------------------------------------------------- 1 | const linked = require("./linked"); 2 | const FreeSlot = require("./FreeSlot"); 3 | 4 | class MemoryManager { 5 | constructor(array, heapBase) { 6 | this.array = array; 7 | 8 | if (heapBase == null) heapBase = 0; 9 | 10 | this.ptrToSlot = new Map(); 11 | 12 | this.slots = new linked.List(); 13 | this.freeSlots = new linked.List(); 14 | this.reservedSlots = new linked.List(); 15 | 16 | const slot = new FreeSlot({ 17 | manager: this, 18 | ptr: heapBase, 19 | size: array.byteLength - heapBase 20 | }); 21 | 22 | const node = this.slots.append(); 23 | node.data = slot; 24 | slot.node = node; 25 | 26 | const freeNode = this.freeSlots.append(); 27 | freeNode.data = slot; 28 | slot.freeNode = freeNode; 29 | } 30 | 31 | numReservedSlots() { 32 | return this.reservedSlots.size; 33 | } 34 | 35 | numFreeSlots() { 36 | return this.freeSlots.size; 37 | } 38 | 39 | numFreeBytes() { 40 | let bytes = 0; 41 | const it = this.freeSlots.iter(); 42 | let r = it.next(); 43 | while (!r.done) { 44 | const slot = r.value; 45 | bytes += slot.size; 46 | r = it.next(); 47 | } 48 | return bytes; 49 | } 50 | 51 | numReservedBytes() { 52 | let bytes = 0; 53 | const it = this.reservedSlots.iter(); 54 | let r = it.next(); 55 | while (!r.done) { 56 | const slot = r.value; 57 | bytes += slot.size; 58 | r = it.next(); 59 | } 60 | return bytes; 61 | } 62 | 63 | mallocBytes(bytes) { 64 | return this._malloc(bytes); 65 | } 66 | 67 | malloc32(n) { 68 | return this.mallocBytes(n * 4); 69 | } 70 | 71 | _addReservedSlot(slot) { 72 | const node = this.reservedSlots.append(slot); 73 | slot.reservedNode = node; 74 | } 75 | 76 | _removeReservedSlot(slot) { 77 | if (slot.reservedNode == null) { 78 | throw new Error("reservedNode cannot be null"); 79 | } 80 | slot.reservedNode.remove(); 81 | } 82 | 83 | _addFreeSlot(slot) { 84 | const node = this.freeSlots.append(slot); 85 | slot.freeNode = node; 86 | } 87 | 88 | _removeFreeSlot(slot) { 89 | if (slot.freeNode == null) { 90 | throw new Error("freeNode cannot be null"); 91 | } 92 | slot.freeNode.remove(); 93 | } 94 | 95 | _malloc(size) { 96 | if (!Number.isInteger(size)) { 97 | throw new Error(`expected integer, found ${size}`); 98 | } 99 | let validFreeSlot = null; 100 | const it = this.freeSlots.iter(); 101 | let r = it.next(); 102 | while (!r.done) { 103 | const freeSlot = r.value; 104 | if (freeSlot.size >= size) { 105 | validFreeSlot = freeSlot; 106 | break; 107 | } 108 | r = it.next(); 109 | } 110 | if (validFreeSlot == null) { 111 | throw new Error("no valid free slot available"); 112 | } 113 | return validFreeSlot.reserve(size); 114 | } 115 | 116 | malloc(n) { 117 | const slot = this._malloc(n); 118 | this.ptrToSlot.set(slot.ptr, slot); 119 | return slot.ptr; 120 | } 121 | 122 | free(ptr) { 123 | const slot = this.ptrToSlot.get(ptr); 124 | slot.free(); 125 | } 126 | } 127 | 128 | module.exports = MemoryManager; -------------------------------------------------------------------------------- /demo/src/AgentManager.js: -------------------------------------------------------------------------------- 1 | import AgentSystem from "./AgentSystem.js"; 2 | 3 | export default class AgentManager { 4 | constructor(system, algovivo, dataRoot = "data", agentNames = []) { 5 | this.algovivo = algovivo; 6 | this.dataRoot = dataRoot; 7 | this.currentAgent = null; 8 | this.agents = agentNames; 9 | this.meshCache = new Map(); 10 | this.policyCache = new Map(); 11 | 12 | this.agentSystem = new AgentSystem({ algovivo, system }); 13 | } 14 | 15 | get system() { 16 | return this.agentSystem.system; 17 | } 18 | 19 | get policy() { 20 | return this.agentSystem.policy; 21 | } 22 | 23 | async loadMeshData(agentName) { 24 | if (this.meshCache.has(agentName)) { 25 | return this.meshCache.get(agentName); 26 | } 27 | const response = await fetch(`${this.dataRoot}/${agentName}/mesh.json`); 28 | const data = await response.json(); 29 | this.meshCache.set(agentName, data); 30 | return data; 31 | } 32 | 33 | async loadPolicyData(agentName) { 34 | if (this.policyCache.has(agentName)) { 35 | return this.policyCache.get(agentName); 36 | } 37 | const response = await fetch(`${this.dataRoot}/${agentName}/policy.json`); 38 | const data = await response.json(); 39 | this.policyCache.set(agentName, data); 40 | return data; 41 | } 42 | 43 | async preloadAllData() { 44 | await Promise.all( 45 | this.agents.map(agentName => 46 | Promise.all([ 47 | this.loadMeshData(agentName), 48 | this.loadPolicyData(agentName) 49 | ]) 50 | ) 51 | ); 52 | } 53 | 54 | async switchToAgent(agentName) { 55 | if (this.currentAgent === agentName) return; 56 | 57 | const [meshData, policyData] = await Promise.all([ 58 | this.loadMeshData(agentName), 59 | this.loadPolicyData(agentName) 60 | ]); 61 | 62 | // recenter mesh to avoid camera jump 63 | let processedPos = meshData.pos; 64 | if (this.policy !== null && this.policy.centerVertexId != null) { 65 | const currentCenterPos = this.system.vertices.getVertexPos(this.policy.centerVertexId); 66 | const newCenterPos = meshData.pos[policyData.center_vertex_id]; 67 | const offsetX = currentCenterPos[0] - newCenterPos[0]; 68 | 69 | processedPos = meshData.pos.map(vertexPos => [ 70 | vertexPos[0] + offsetX, 71 | vertexPos[1] 72 | ]); 73 | } 74 | 75 | const activePolicy = this.policy ? this.policy.active : false; 76 | 77 | this.agentSystem.set({ 78 | mesh: { 79 | pos: processedPos, 80 | muscles: meshData.muscles, 81 | musclesL0: meshData.l0, 82 | triangles: meshData.triangles, 83 | trianglesRsi: meshData.rsi 84 | }, 85 | policy: policyData 86 | }); 87 | this.agentSystem.policy.active = activePolicy; 88 | 89 | this.currentAgent = agentName; 90 | 91 | return { meshData, policyData }; 92 | } 93 | 94 | togglePolicy() { 95 | if (this.policy) { 96 | this.policy.active = !this.policy.active; 97 | return this.policy.active; 98 | } 99 | return false; 100 | } 101 | 102 | getCurrentAgent() { 103 | return this.currentAgent; 104 | } 105 | 106 | isActive() { 107 | return this.policy && this.policy.active; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /demo/src/BrainButton.js: -------------------------------------------------------------------------------- 1 | export default class BrainButton { 2 | constructor(args = {}) { 3 | const div = this.domElement = document.createElement("div"); 4 | 5 | div.style.userSelect = "none"; 6 | div.style.webkitTapHighlightColor = "transparent"; 7 | div.style.padding = "12px"; 8 | div.style.boxSizing = "content-box"; 9 | div.style.cursor = "pointer"; 10 | div.style.borderRadius = "50%"; 11 | div.style.minHeight = div.style.height; 12 | div.style.margin = "4px"; 13 | div.style.display = "flex"; 14 | div.style.alignItems = "center"; 15 | div.style.justifyContent = "center"; 16 | div.style.boxShadow = "0 0 8px rgba(0, 0, 0, 0.2)"; 17 | div.style.padding = "20px"; 18 | div.style.overflow = "hidden"; 19 | 20 | this.initSvg(); 21 | this.setSize(40); 22 | this.setInactiveStyle(); 23 | } 24 | 25 | setSize(size) { 26 | const div = this.domElement; 27 | div.style.width = `${size}px`; 28 | div.style.height = `${size}px`; 29 | div.style.minHeight = `${size}px`; 30 | } 31 | 32 | initSvg() { 33 | const color = "white"; 34 | 35 | const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 36 | this.svg = svg; 37 | svg.style.width = "100%"; 38 | svg.setAttribute("width", "198"); 39 | svg.setAttribute("height", "217"); 40 | svg.setAttribute("viewBox", "0 0 198 217"); 41 | svg.setAttribute("fill", "none"); 42 | 43 | const pathRight = document.createElementNS("http://www.w3.org/2000/svg", "path"); 44 | pathRight.setAttribute("fill-rule", "evenodd"); 45 | pathRight.setAttribute("clip-rule", "evenodd"); 46 | pathRight.setAttribute("d", "M156.5 186.309L173 178.837V161.992L152.888 150.729L130.5 162.127V194.062H120.5V155.996L148 141.996V121.915L173 109.915V86.0615H183V116.208L158 128.208V142.131L177.975 153.317L197.5 142.16V76.5834L179.133 63.0079L157 75.4829L156.5 102H146.5L147 75.4371L119.5 59.4371V22.5615H129.5V53.6859L152.039 66.7998L174.42 54.185L174.077 41.482L156.5 31.2553V48.5615H146.5V13.2817L125.974 0L105 13.3103V128.519L121 117.442V89.0615H131V122.681L105 140.681V204.221L125.975 216.725L146.5 204.249V169.562H156.5V186.309Z"); 47 | pathRight.setAttribute("fill", color); 48 | svg.appendChild(pathRight); 49 | 50 | const pathLeft = document.createElementNS("http://www.w3.org/2000/svg", "path"); 51 | pathLeft.setAttribute("fill-rule", "evenodd"); 52 | pathLeft.setAttribute("clip-rule", "evenodd"); 53 | pathLeft.setAttribute("d", "M41 30.4169L24.5 37.8885V54.7333L44.6122 65.9962L67 54.5988V22.664H77V60.7291L49.5 74.7291V94.8101L24.5 106.81V130.664H14.5V100.518L39.5 88.5178V74.5946L19.5249 63.4085L0 74.5656V140.142L18.3669 153.718L40.5 141.243L41 114.725H51L50.5 141.288L78 157.288V194.164H68V163.04L45.4606 149.926L23.0796 162.54L23.4229 175.243L41 185.47V168.164H51V203.444L71.5262 216.725L92.5 203.415V88.2068L76.5 99.2837V127.664H66.5V94.0442L92.5 76.0442V12.5043L71.525 -3.05176e-05L51 12.476V47.164H41V30.4169Z"); 54 | pathLeft.setAttribute("fill", color); 55 | svg.appendChild(pathLeft); 56 | 57 | this.domElement.appendChild(svg); 58 | } 59 | 60 | setActiveStyle(active = true) { 61 | if (active) { 62 | this.domElement.style.backgroundColor = "black"; 63 | } else { 64 | this.setInactiveStyle(); 65 | } 66 | } 67 | 68 | setInactiveStyle() { 69 | this.domElement.style.backgroundColor = "rgba(1, 1, 1, 0.2)"; 70 | } 71 | } -------------------------------------------------------------------------------- /algovivo/Muscles.js: -------------------------------------------------------------------------------- 1 | class Muscles { 2 | constructor(args = {}) { 3 | const ten = args.ten; 4 | if (ten == null) throw new Error("ten required"); 5 | this.ten = ten; 6 | 7 | this.indices = null; 8 | this.k = Math.fround(90); 9 | this.l0 = null; 10 | this.a = null; 11 | } 12 | 13 | get wasmInstance() { 14 | return this.ten.wasmInstance; 15 | } 16 | 17 | get memoryManager() { 18 | return this.ten.mgr; 19 | } 20 | 21 | get numMuscles() { 22 | if (this.indices == null) return 0; 23 | return this.indices.u32().length / 2; 24 | } 25 | 26 | set(args = {}) { 27 | if (args.indices == null) { 28 | throw new Error("indices required"); 29 | } 30 | const indices = args.indices; 31 | const numMuscles = indices.length; 32 | const numMuscles0 = this.numMuscles; 33 | 34 | const mgr = this.memoryManager; 35 | const ten = this.ten; 36 | 37 | if (args.k != null) this.k = args.k; 38 | 39 | const muscles = mgr.malloc32(numMuscles * 2); 40 | if (this.indices != null) this.indices.free(); 41 | this.indices = muscles; 42 | 43 | const musclesU32 = muscles.u32(); 44 | indices.forEach((m, i) => { 45 | const offset = i * 2; 46 | musclesU32[offset ] = m[0]; 47 | musclesU32[offset + 1] = m[1]; 48 | }); 49 | 50 | if (this.l0 != null) this.l0.dispose(); 51 | this.l0 = null; 52 | 53 | if (numMuscles != 0) { 54 | const l0 = ten.zeros([numMuscles]); 55 | this.l0 = l0; 56 | 57 | if (args.l0 == null) { 58 | const pos = args.pos; 59 | if (pos == null) { 60 | throw new Error("pos required to compute l0"); 61 | } 62 | if (pos.ptr == null) { 63 | throw new Error("pos.ptr not available"); 64 | } 65 | this.wasmInstance.exports.l0_of_pos( 66 | this.numVertices, 67 | pos.ptr, 68 | numMuscles, 69 | this.indices.ptr, 70 | this.l0.ptr 71 | ); 72 | } else { 73 | this.l0.set(args.l0); 74 | } 75 | } 76 | 77 | const keepA = args.keepA ?? false; 78 | if (numMuscles != numMuscles0) { 79 | if (keepA) { 80 | throw new Error(`keepA can only be true when the number of muscles is the same (${numMuscles} != ${numMuscles0})`); 81 | } 82 | if (this.a != null) { 83 | this.a.dispose(); 84 | this.a = null; 85 | } 86 | if (numMuscles != 0) { 87 | const a = ten.zeros([numMuscles]); 88 | this.a = a; 89 | a.fill_(1); 90 | } 91 | } else 92 | if (numMuscles == 0) { 93 | if (this.a != null) this.a.dispose(); 94 | this.a = null; 95 | } else { 96 | // numMuscles == numMuscles0 != 0 97 | if (!keepA) { 98 | this.a.fill_(1); 99 | } 100 | } 101 | } 102 | 103 | toStepArgs() { 104 | const numMuscles = this.numMuscles; 105 | return [ 106 | numMuscles, 107 | numMuscles == 0 ? 0 : this.indices.ptr, 108 | this.k, 109 | numMuscles == 0 ? 0 : this.a.ptr, 110 | numMuscles == 0 ? 0 : this.l0.ptr 111 | ]; 112 | } 113 | 114 | dispose() { 115 | if (this.indices != null) { 116 | this.indices.free(); 117 | this.indices = null; 118 | } 119 | if (this.l0 != null) { 120 | this.l0.dispose(); 121 | this.l0 = null; 122 | } 123 | if (this.a != null) { 124 | this.a.dispose(); 125 | this.a = null; 126 | } 127 | } 128 | } 129 | 130 | module.exports = Muscles; -------------------------------------------------------------------------------- /test/muscles.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("set pos and muscles", async () => { 7 | const ten = await utils.loadTen(); 8 | const system = new algovivo.System({ ten }); 9 | system.set({ 10 | pos: [ 11 | [0, 0], 12 | [2, 0], 13 | [1, 1], 14 | [3, 7], 15 | [5, 7] 16 | ], 17 | muscles: [ 18 | [0, 2], 19 | [1, 2], 20 | [3, 4] 21 | ] 22 | }); 23 | expect(system.numMuscles).toBe(3); 24 | const expectedL0 = [1.4142135381698608, 1.4142135381698608, 2]; 25 | expect(system.l0.toArray()).toBeCloseToArray(expectedL0); 26 | }); 27 | 28 | test("set muscles", async () => { 29 | const ten = await utils.loadTen(); 30 | const system = new algovivo.System({ ten }); 31 | system.set({ 32 | pos: [ 33 | [0, 0], 34 | [2, 0], 35 | [1, 1], 36 | [3, 7], 37 | [5, 7] 38 | ] 39 | }); 40 | expect(system.numMuscles).toBe(0); 41 | expect(system.l0).toBeNull(); 42 | expect(system.a).toBeNull(); 43 | 44 | system.setMuscles({ 45 | indices: [ 46 | [0, 2], 47 | [1, 2], 48 | [3, 4] 49 | ] 50 | }); 51 | expect(system.numMuscles).toBe(3); 52 | const expectedL0 = [1.4142135381698608, 1.4142135381698608, 2]; 53 | expect(system.l0.toArray()).toBeCloseToArray(expectedL0); 54 | expect(system.a.toArray()).toBeCloseToArray([1, 1, 1]); 55 | 56 | system.setMuscles({ 57 | indices: [ 58 | [0, 2], 59 | [3, 4] 60 | ], 61 | l0: [10, 15] 62 | }); 63 | expect(system.numMuscles).toBe(2); 64 | expect(system.l0.toArray()).toBeCloseToArray([10, 15]); 65 | expect(system.a.toArray()).toBeCloseToArray([1, 1]); 66 | }); 67 | 68 | test("update l0, keep a", async () => { 69 | const ten = await utils.loadTen(); 70 | const system = new algovivo.System({ ten }); 71 | system.setVertices([ 72 | [0.5, 0.5], 73 | [1.5, 0.5] 74 | ]); 75 | system.setMuscles({ 76 | indices: [ 77 | [0, 1] 78 | ], 79 | l0: [ 80 | 1.0 81 | ] 82 | }); 83 | expect(system.a.toArray()).toBeCloseToArray([1.0]); 84 | system.a.set([0.3]); 85 | expect(system.a.toArray()).toBeCloseToArray([0.3]); 86 | system.setMuscles({ 87 | indices: [ 88 | [0, 1] 89 | ], 90 | l0: [ 91 | 2.0 92 | ], 93 | keepA: true 94 | }); 95 | expect(system.a.toArray()).toBeCloseToArray([0.3]); 96 | system.setMuscles({ 97 | indices: [ 98 | [0, 1] 99 | ], 100 | l0: [ 101 | 2.0 102 | ] 103 | }); 104 | expect(system.a.toArray()).toBeCloseToArray([1]); 105 | }); 106 | 107 | test("add and remove muscles", async () => { 108 | const ten = await utils.loadTen(); 109 | const system = new algovivo.System({ ten }); 110 | system.setVertices([ 111 | [0, 0], 112 | [2, 0] 113 | ]); 114 | 115 | expect(system.numMuscles).toBe(0); 116 | system.setMuscles({ 117 | indices: [ 118 | [0, 1] 119 | ] 120 | }); 121 | expect(system.numMuscles).toBe(1); 122 | system.setMuscles({ 123 | indices: [] 124 | }); 125 | expect(system.numMuscles).toBe(0); 126 | system.setMuscles({ 127 | indices: [ 128 | [0, 1] 129 | ] 130 | }); 131 | expect(system.numMuscles).toBe(1); 132 | }); 133 | 134 | test("set indices only", async () => { 135 | const ten = await utils.loadTen(); 136 | const system = new algovivo.System({ ten }); 137 | 138 | expect(() => { 139 | system.muscles.set({ 140 | indices: [[0, 1]] 141 | }); 142 | }).toThrow("pos required to compute l0"); 143 | }) -------------------------------------------------------------------------------- /algovivo/Triangles.js: -------------------------------------------------------------------------------- 1 | class Triangles { 2 | constructor(args = {}) { 3 | const ten = args.ten; 4 | if (ten == null) throw new Error("ten required"); 5 | this.ten = ten; 6 | 7 | this.simplexOrder = args.simplexOrder ?? 3; 8 | this.indices = null; 9 | this.rsi = null; 10 | this.mu = null; 11 | this.lambda = null; 12 | } 13 | 14 | get wasmInstance() { 15 | return this.ten.wasmInstance; 16 | } 17 | 18 | get memoryManager() { 19 | return this.ten.mgr; 20 | } 21 | 22 | get numElements() { 23 | if (this.indices == null) return 0; 24 | return this.indices.u32().length / this.simplexOrder; 25 | } 26 | 27 | get numTriangles() { 28 | return this.numElements; 29 | } 30 | 31 | toStepArgs() { 32 | const numElements = this.numElements; 33 | return [ 34 | numElements, 35 | numElements == 0 ? 0 : this.indices.ptr, 36 | numElements == 0 ? 0 : this.rsi.ptr, 37 | numElements == 0 ? 0 : this.mu.ptr, 38 | numElements == 0 ? 0 : this.lambda.ptr 39 | ]; 40 | } 41 | 42 | set(args = {}) { 43 | const indices = args.indices; 44 | const rsi = args.rsi; 45 | const numTriangles = indices ? indices.length : this.numTriangles; 46 | 47 | if (indices == null && (!rsi || rsi.length !== numTriangles)) { 48 | throw new Error("rsi is not consistent with the number of indices"); 49 | } 50 | 51 | const mgr = this.memoryManager; 52 | const ten = this.ten; 53 | 54 | const triangles = indices ? mgr.malloc32(numTriangles * this.simplexOrder) : this.indices; 55 | if (indices && this.indices != null) this.indices.free(); 56 | this.indices = triangles; 57 | 58 | if (indices != null) { 59 | const trianglesU32 = triangles.u32(); 60 | indices.forEach((t, i) => { 61 | const offset = i * this.simplexOrder; 62 | for (let j = 0; j < this.simplexOrder; j++) { 63 | trianglesU32[offset + j] = t[j]; 64 | } 65 | }); 66 | } 67 | 68 | if (this.rsi != null) this.rsi.dispose(); 69 | this.rsi = ten.zeros([numTriangles, this.simplexOrder - 1, this.simplexOrder - 1]); 70 | 71 | if (rsi == null) { 72 | let pos = null; 73 | let tmpPos = false; 74 | if (args.pos != null) { 75 | if (Array.isArray(args.pos)) { 76 | pos = ten.tensor(args.pos); 77 | tmpPos = true; 78 | } else { 79 | pos = args.pos; 80 | if (pos.ptr == null) throw new Error("invalid pos"); 81 | } 82 | } 83 | 84 | this.wasmInstance.exports.rsi_of_pos( 85 | this.numVertices, 86 | pos.ptr, 87 | numTriangles, 88 | this.indices.ptr, 89 | this.rsi.ptr 90 | ); 91 | 92 | if (tmpPos) pos.dispose(); 93 | } else { 94 | this.rsi.set(rsi); 95 | } 96 | 97 | if (this.mu != null) this.mu.dispose(); 98 | this.mu = ten.zeros([numTriangles]); 99 | this.mu.fill_(Math.fround(500)); 100 | 101 | if (this.lambda != null) this.lambda.dispose(); 102 | this.lambda = ten.zeros([numTriangles]); 103 | this.lambda.fill_(Math.fround(50)); 104 | } 105 | 106 | dispose() { 107 | if (this.indices != null) { 108 | this.indices.free(); 109 | this.indices = null; 110 | } 111 | if (this.rsi != null) { 112 | this.rsi.dispose(); 113 | this.rsi = null; 114 | } 115 | if (this.mu != null) { 116 | this.mu.dispose(); 117 | this.mu = null; 118 | } 119 | if (this.lambda != null) { 120 | this.lambda.dispose(); 121 | this.lambda = null; 122 | } 123 | } 124 | } 125 | 126 | module.exports = Triangles; -------------------------------------------------------------------------------- /codegen/algovivo_codegen/csrc/frame_projection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace algovivo { 4 | 5 | void normalize2d_(float* vx, float* vy) { 6 | const auto q = *vx * *vx + *vy * *vy; 7 | if (q == 0) { 8 | *vx = 1.0; 9 | *vy = 0.0; 10 | } else { 11 | float norm = __builtin_sqrt(q); 12 | *vx /= norm; 13 | *vy /= norm; 14 | } 15 | } 16 | 17 | extern "C" 18 | inline float dot2d(float ax, float ay, float bx, float by) { 19 | return ax * bx + ay * by; 20 | } 21 | 22 | extern "C" 23 | void frame_projection( 24 | int num_vertices, 25 | const float* pos, 26 | int center_vertex_id, 27 | int forward_vertex_id, 28 | const float* data, 29 | float* projected_data, 30 | bool subtract_origin, 31 | bool clockwise 32 | ) { 33 | // A frame of reference is defined by two vertices: center_vertex_id 34 | // (serving as the origin of the frame) and forward_vertex_id. 35 | // The forward direction of the frame is defined as the normalized vector 36 | // from the center vertex to the forward vertex, using the positions of 37 | // these vertices in the pos array. 38 | 39 | const auto space_dim = 2; 40 | 41 | // retrieve the positions of the center and forward vertices 42 | vec2_get(c, pos, center_vertex_id); 43 | vec2_get(f, pos, forward_vertex_id); 44 | 45 | // a and b represent the basis vectors of the frame, both normalized to unit length. 46 | // a is the forward direction vector, pointing from the center vertex to the forward vertex. 47 | // b is the orthogonal direction vector, computed to be perpendicular to a 48 | auto ax = fx - cx; 49 | auto ay = fy - cy; 50 | normalize2d_(&ax, &ay); 51 | const auto bx = clockwise ? ay : -ay; 52 | const auto by = clockwise ? -ax : ax; 53 | 54 | for (int i = 0; i < num_vertices; i++) { 55 | const auto offset = i * space_dim; 56 | auto px = data[offset]; 57 | auto py = data[offset + 1]; 58 | if (subtract_origin) { 59 | px -= cx; 60 | py -= cy; 61 | } 62 | projected_data[offset ] = dot2d(ax, ay, px, py); 63 | projected_data[offset + 1] = dot2d(bx, by, px, py); 64 | } 65 | } 66 | 67 | extern "C" 68 | void cat_pos_vel( 69 | int num_vertices, 70 | const float* pos, 71 | const float* vel, 72 | float* policy_input 73 | ) { 74 | const auto space_dim = 2; 75 | for (int i = 0; i < num_vertices; i++) { 76 | const auto offset = i * space_dim; 77 | policy_input[offset ] = pos[offset ]; 78 | policy_input[offset + 1] = pos[offset + 1]; 79 | } 80 | for (int i = 0; i < num_vertices; i++) { 81 | const auto offset = i * space_dim; 82 | const auto offset2 = num_vertices * 2 + offset; 83 | policy_input[offset2 ] = vel[offset ]; 84 | policy_input[offset2 + 1] = vel[offset + 1]; 85 | } 86 | } 87 | 88 | extern "C" 89 | void make_neural_policy_input( 90 | int num_vertices, 91 | const float* pos, 92 | const float* vel, 93 | int center_vertex_id, 94 | int forward_vertex_id, 95 | float* projected_pos, 96 | float* projected_vel, 97 | float* policy_input, 98 | bool clockwise 99 | ) { 100 | frame_projection( 101 | // frame of reference 102 | num_vertices, 103 | pos, 104 | center_vertex_id, 105 | forward_vertex_id, 106 | // position projection 107 | pos, 108 | projected_pos, 109 | true, 110 | clockwise 111 | ); 112 | 113 | frame_projection( 114 | // frame of reference 115 | num_vertices, 116 | pos, 117 | center_vertex_id, 118 | forward_vertex_id, 119 | // velocity projection 120 | vel, 121 | projected_vel, 122 | false, 123 | clockwise 124 | ); 125 | 126 | cat_pos_vel(num_vertices, projected_pos, projected_vel, policy_input); 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /algovivo/render/LineRenderer.js: -------------------------------------------------------------------------------- 1 | function hashSimplex(vids) { 2 | vids.sort(); 3 | return vids.join("_"); 4 | } 5 | 6 | function renderLine(ctx, scale, a, b, borderWidth, borderColor) { 7 | ctx.beginPath(); 8 | ctx.lineJoin = "round"; 9 | ctx.lineCap = "round"; 10 | ctx.strokeStyle = borderColor; 11 | ctx.lineWidth = borderWidth * scale; 12 | ctx.moveTo(a[0], a[1]); 13 | ctx.lineTo(b[0], b[1]); 14 | ctx.closePath(); 15 | ctx.stroke(); 16 | } 17 | 18 | function renderMuscle(ctx, scale, a, b, t, width, borderWidth, borderColor, color0, color1) { 19 | ctx.beginPath(); 20 | ctx.lineCap = "butt"; 21 | ctx.strokeStyle = borderColor; 22 | ctx.lineWidth = (width + borderWidth * 2) * scale; 23 | ctx.moveTo(a[0], a[1]); 24 | ctx.lineTo(b[0], b[1]); 25 | ctx.stroke(); 26 | 27 | ctx.beginPath(); 28 | 29 | const cr0 = color0[0]; 30 | const cr1 = color1[0]; 31 | 32 | const cg0 = color0[1]; 33 | const cg1 = color1[1]; 34 | 35 | const cb0 = color0[2]; 36 | const cb1 = color1[2]; 37 | 38 | const cr = (1 - t) * cr0 + t * cr1; 39 | const cg = (1 - t) * cg0 + t * cg1; 40 | const cb = (1 - t) * cb0 + t * cb1; 41 | 42 | ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`; 43 | ctx.lineWidth = width * scale; 44 | ctx.moveTo(a[0], a[1]); 45 | ctx.lineTo(b[0], b[1]); 46 | 47 | ctx.stroke(); 48 | } 49 | 50 | class LineRenderer { 51 | constructor(args = {}) { 52 | this.system = args.system; 53 | } 54 | 55 | makeEdgesFromTriangles(triangles) { 56 | const edges = new Map(); 57 | 58 | function addEdge(i1, i2) { 59 | const hash = hashSimplex([i1, i2]); 60 | edges.set(hash, [i1, i2]); 61 | } 62 | 63 | triangles.forEach(t => { 64 | addEdge(t[0], t[1]); 65 | addEdge(t[1], t[2]); 66 | addEdge(t[0], t[2]); 67 | }); 68 | return Array.from(edges.values()); 69 | } 70 | 71 | makeLineShaderFunction(args = {}) { 72 | const activeMuscleColor = args.activeMuscleColor ?? [255, 0, 0]; 73 | const inactiveMuscleColor = args.inactiveMuscleColor ?? [0, 0, 255]; 74 | const borderColor = args.borderColor ?? "black"; 75 | 76 | return (args = {}) => { 77 | const ctx = args.ctx; 78 | const a = args.a; 79 | const b = args.b; 80 | const camera = args.camera; 81 | const scale = camera.inferScale(); 82 | 83 | const lineIdToMuscleId = args.mesh.getCustomAttribute("lineIdToMuscleId"); 84 | let muscleId = null; 85 | if (lineIdToMuscleId != null) { 86 | muscleId = lineIdToMuscleId[args.id]; 87 | } 88 | if (muscleId == null) { 89 | const borderWidth = 0.029; 90 | renderLine(ctx, scale, a, b, borderWidth, borderColor); 91 | } else { 92 | const color0 = activeMuscleColor; 93 | const color1 = inactiveMuscleColor; 94 | 95 | const width = 0.065; 96 | const borderWidth = 0.017; 97 | const muscleIntensityAttributeName = "muscleIntensity"; 98 | 99 | const muscleIntensity = args.mesh.getCustomAttribute(muscleIntensityAttributeName); 100 | if (muscleIntensity == null) { 101 | throw new Error(`muscle intensity attribute (${muscleIntensityAttributeName}) not found, call setCustomAttribute("${muscleIntensityAttributeName}", value) before rendering.`); 102 | } 103 | if (!Array.isArray(muscleIntensity)) { 104 | throw new Error(`muscle intensity attribute must be an array with values for each fiber, found ${typeof muscleIntensity}`); 105 | } 106 | 107 | const t = muscleIntensity[muscleId]; 108 | renderMuscle(ctx, scale, a, b, t, width, borderWidth, borderColor, color0, color1); 109 | } 110 | } 111 | } 112 | } 113 | 114 | module.exports = LineRenderer; -------------------------------------------------------------------------------- /test/triangles.test.js: -------------------------------------------------------------------------------- 1 | const algovivo = require("algovivo"); 2 | const utils = require("./utils"); 3 | 4 | expect.extend({ toBeCloseToArray: utils.toBeCloseToArray }); 5 | 6 | test("set pos and triangles", async () => { 7 | const ten = await utils.loadTen(); 8 | const system = new algovivo.System({ ten }); 9 | system.set({ 10 | pos: [ 11 | [0, 0], 12 | [2, 0], 13 | [1, 1], 14 | [-0.3, 0.8] 15 | ], 16 | triangles: [ 17 | [0, 1, 2], 18 | [0, 2, 3] 19 | ] 20 | }); 21 | expect(system.numTriangles).toBe(2); 22 | const expectedRsi = [ 23 | [ 24 | [0.5, -0.5], 25 | [0, 1] 26 | ], 27 | [ 28 | [0.7272727489471436, 0.27272728085517883], 29 | [-0.9090909361839294, 0.90909093618392940] 30 | ] 31 | ]; 32 | expect(system.rsi.toArray()).toBeCloseToArray(expectedRsi); 33 | 34 | const trianglesArray = system.getTrianglesArray(); 35 | expect(trianglesArray).toEqual([ 36 | [0, 1, 2], 37 | [0, 2, 3] 38 | ]); 39 | }); 40 | 41 | test("set rsi", async () => { 42 | const ten = await utils.loadTen(); 43 | const system = new algovivo.System({ ten }); 44 | system.set({ 45 | pos: [ 46 | [0, 0], 47 | [2, 0], 48 | [1, 1], 49 | [-0.3, 0.8] 50 | ], 51 | triangles: [ 52 | [0, 1, 2], 53 | [0, 2, 3] 54 | ], 55 | trianglesRsi: [ 56 | [[1, 2], 57 | [3, 4]], 58 | [[5, 6], 59 | [7, 8]], 60 | ] 61 | }); 62 | expect(system.numTriangles).toBe(2); 63 | const expectedRsi = [ 64 | [[1, 2], 65 | [3, 4]], 66 | [[5, 6], 67 | [7, 8]], 68 | ]; 69 | expect(system.rsi.toArray()).toBeCloseToArray(expectedRsi); 70 | 71 | system.setTriangles({ 72 | rsi: [ 73 | [[11, 12], 74 | [13, 14]], 75 | [[15, 16], 76 | [17, 18]] 77 | ] 78 | }); 79 | expect(system.rsi.toArray()).toBeCloseToArray([ 80 | [[11, 12], 81 | [13, 14]], 82 | [[15, 16], 83 | [17, 18]] 84 | ]); 85 | 86 | expect(() => { 87 | system.setTriangles({ 88 | rsi: [ 89 | [[11, 12], 90 | [13, 14]] 91 | ] 92 | }); 93 | }).toThrow(); 94 | }); 95 | 96 | test("set triangles", async () => { 97 | const ten = await utils.loadTen(); 98 | const system = new algovivo.System({ ten }); 99 | system.set({ 100 | pos: [ 101 | [0, 0], 102 | [2, 0], 103 | [1, 1], 104 | [-0.3, 0.8] 105 | ] 106 | }); 107 | expect(system.numTriangles).toBe(0); 108 | 109 | system.setTriangles({ 110 | indices: [ 111 | [0, 1, 2], 112 | [0, 2, 3] 113 | ] 114 | }); 115 | expect(system.numTriangles).toBe(2); 116 | }); 117 | 118 | test("set triangles with pos", async () => { 119 | const ten = await utils.loadTen(); 120 | const system = new algovivo.System({ ten }); 121 | system.set({ 122 | pos: [ 123 | [0, 0], 124 | [1, 0], 125 | [1, 1], 126 | [0, 1] 127 | ] 128 | }); 129 | expect(system.numTriangles).toBe(0); 130 | 131 | system.setTriangles({ 132 | indices: [ 133 | [0, 1, 2], 134 | [0, 2, 3] 135 | ] 136 | }); 137 | expect(system.numTriangles).toBe(2); 138 | 139 | system.setTriangles({ 140 | pos: [ 141 | [0, 0], 142 | [2, 0], 143 | [1, 1], 144 | [-0.3, 0.8] 145 | ], 146 | indices: [ 147 | [0, 1, 2], 148 | [0, 2, 3] 149 | ] 150 | }); 151 | 152 | expect(system.numTriangles).toBe(2); 153 | const expectedRsi = [ 154 | [ 155 | [0.5, -0.5], 156 | [0, 1] 157 | ], 158 | [ 159 | [0.7272727489471436, 0.27272728085517883], 160 | [-0.9090909361839294, 0.90909093618392940] 161 | ] 162 | ]; 163 | expect(system.rsi.toArray()).toBeCloseToArray(expectedRsi); 164 | }); -------------------------------------------------------------------------------- /.github/workflows/trajectory.yml: -------------------------------------------------------------------------------- 1 | name: trajectory 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | ENZYME_IMAGE: ghcr.io/juniorrojas/algovivo/llvm11-enzyme:latest 14 | 15 | jobs: 16 | build-wasm: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Build WASM 23 | run: | 24 | python codegen/codegen_csrc.py 25 | docker run \ 26 | -v $(pwd):/workspace \ 27 | -w /workspace \ 28 | ${{ env.ENZYME_IMAGE }} \ 29 | ./build.sh 30 | 31 | - name: Upload WASM 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: algovivo.wasm 35 | path: build/algovivo.wasm 36 | 37 | generate-trajectory: 38 | runs-on: ubuntu-latest 39 | needs: build-wasm 40 | env: 41 | OUTPUT_DIRNAME: trajectory.out 42 | steps: 43 | - name: Clone repo 44 | uses: actions/checkout@v4 45 | 46 | - name: Download WASM build 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: algovivo.wasm 50 | path: build/ 51 | 52 | - name: Set up node 22 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: 22 56 | 57 | - name: Generate trajectory data 58 | run: node test/nn/generateTrajectory.js 59 | 60 | - name: Upload trajectory output 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: trajectory 64 | path: ${{ env.OUTPUT_DIRNAME }} 65 | 66 | - name: Check trajectory output 67 | run: | 68 | test -d ${{ env.OUTPUT_DIRNAME }} || (echo "Directory does not exist" && exit 1) 69 | diff -r ${{ env.OUTPUT_DIRNAME }} test/nn/data/trajectory || (echo "Files do not match" && exit 1) 70 | 71 | # render-trajectory does not depend on generate-trajectory because render-trajectory uses the reference trajectory data 72 | # which already exists in the repo, while generate-trajectory checks that the current version of the code generates the same 73 | # trajectory that is already stored in the repo. 74 | render-trajectory: 75 | runs-on: ubuntu-latest 76 | needs: build-wasm 77 | steps: 78 | - name: Clone repo 79 | uses: actions/checkout@v4 80 | 81 | - name: Download WASM build 82 | uses: actions/download-artifact@v4 83 | with: 84 | name: algovivo.wasm 85 | path: build/ 86 | 87 | 88 | - name: Set up node 22 89 | uses: actions/setup-node@v4 90 | with: 91 | node-version: 22 92 | 93 | - name: Build 94 | run: | 95 | npm ci 96 | npm run build 97 | 98 | - name: Generate trajectory frames 99 | run: | 100 | node utils/trajectory/renderTrajectory.js \ 101 | --mesh test/nn/data/mesh.json \ 102 | --steps test/nn/data/trajectory \ 103 | --output frames.out 104 | 105 | - name: Check number of frames 106 | run: | 107 | NUM_FRAMES=$(ls frames.out/*.png 2>/dev/null | wc -l) 108 | if [ "$NUM_FRAMES" -ne 101 ]; then 109 | echo "Expected 101 frames, found $NUM_FRAMES" 110 | exit 1 111 | fi 112 | 113 | - name: Install ffmpeg 114 | run: | 115 | sudo apt-get update 116 | sudo apt-get install ffmpeg 117 | 118 | - name: Make trajectory video 119 | run: | 120 | ffmpeg -framerate 30 -i frames.out/%d.png -pix_fmt yuv420p video.out.mp4 -y 121 | 122 | - name: Upload trajectory video 123 | uses: actions/upload-artifact@v4 124 | with: 125 | name: video 126 | path: video.out.mp4 --------------------------------------------------------------------------------