An assembly/disassembly planning environment with packed Boolean state, GPU-oriented batches, a Gymnasium interface and an interactive 3D workbench. Explore which components can be removed next, enforce declared dependencies, and compare planning methods without rendering inside the simulation loop.
python -m pip install "ev-battery-disassembly-tm[gym] @ git+https://github.com/pa-research/ev-assembly-lab.git"
python -m ev_battery_disassembly_tm list-envs
python -m ev_battery_disassembly_tm.acceptance
python -m ev_battery_disassembly_tm.viewer --port 8877Open http://127.0.0.1:8877/. Choose a battery and keep Open the pack as your first task. The highlighted button names the next component to remove and explains why. Follow the nine modeled steps, then continue to Extract the first module. Undo step reverses an action; Start over restores the assembly. These tasks navigate declared dependencies, not a learned policy.
- View detailed CAD: choose Detailed CAD in the viewer, or open
http://127.0.0.1:8877/cad.html. Five source-derived GLB views cover the VW ID.3 58 kWh pack/module/cell and Tesla Model 3 LFP pack/cell. Orbit, zoom and download the models. This is view-only; connecting their geometry to disassembly actions is deferred. - Understand battery structure: see what blocks a cover or module and explore removal and reassembly one step at a time.
- Inspect a model: expand Inspect a component for its constraints, dimensions and source evidence. Extract a module to inspect its cell fixture.
- Build planning experiments: use Gymnasium for individual environments or packed CPU/CUDA batches for high-throughput state transitions.
- Explore recorded teardowns: the Observed teardowns page shows ten KIT recordings, separately from the reconstructed interactive models.
Actual workbench showing a reconstructed assembly. The top banner is a concept illustration, not a simulator screenshot or an OEM battery model.
Advanced inspection modes live under Advanced tools. The component tree, full-sequence autoplay, view settings and JSON import/export are optional sections rather than prerequisites for starting.
For reproducibility, append @<commit-sha> to the Git URL. Python 3.11–3.13
is the CI matrix; the base package needs only NumPy. No new data downloads are
needed. Viewer assets and catalogue/graph data are bundled.
The detailed CAD meshes and their source attribution are bundled too. Original STEP files remain excluded. The CAD view uses a locally bundled MIT-licensed Three.js renderer; no CDN or new model downloads are required.
On an NVIDIA RTX 3090, the optimized CUDA implementation processed about 1.33 billion model steps per second, compared with 99 million for the single-step implementation: 13.36× higher throughput in the matched test.
One model step means one action on one battery model—for example, removing or reinstalling a component. The code checks the model's removal/assembly rules, updates which parts are present, and updates which actions are allowed next. It does not simulate the physical movement of that part.
| Implementation | How work is sent to the GPU | Model steps per second |
|---|---|---|
| Single-step CUDA | Launch GPU work for each action | 99.21 million |
| Optimized CUDA | Keep battery states on the GPU and process 32 actions per launch | 1,325.27 million |
These rates are the combined throughput of many independent battery models processed together, not the speed of one battery or the time to finish a disassembly. Both rows use the same code snapshot and test workloads.
This measures how quickly the environment can update states—a useful building block for testing many assembly/disassembly sequences. The benchmark follows a preloaded action sequence; it does not learn or search for a good sequence. It does not measure TM training, the Python Gymnasium interface, 3D rendering, or contact physics.
How the benchmark was measured
Recorded on 13 September 2026, using six battery configurations. Each was tested with 256, 4,096 and 16,384 simultaneous battery models, with three repetitions per combination: 54 measurements in each run.
For each of the 18 configuration/batch combinations, we took the median of its three measurements. The table reports the median of those 18 values, rather than total steps divided by the duration of the entire test suite.
Timing includes the CPU's work to launch the GPU operations and the final wait for GPU completion. Setup, compilation and copying results back for validation are excluded. Profiling tools were disabled for these speed measurements. Runs that process actions in groups use the actual number of steps executed, including rounding up to complete groups.
Every test finished with the same battery state as the CPU reference and no invalid actions. These are recorded results, not benchmarks rerun after every documentation or interface change.
Separate Nsight Systems checks found no CUDA memory-copy operations or GPU memory allocation/free calls inside the inspected steady-state update loops. Setup and reading results back still transfer data. The kernels still read and write GPU memory; this is not a claim of zero memory traffic.
Earlier implementations and recorded run numbers
| Implementation | Code snapshot group / run | Million model steps/s |
|---|---|---|
| Single-step CUDA graph | A / 745 | 97.03 |
| Single-step CUDA graph with incremental state updates | A / 746 | 233.40 |
| Incremental updates, 32 steps per replay chunk | A / 747 | 513.57 |
| Single-step CUDA graph, matched comparison | B / 753 | 99.21 |
| Action selection and 32 state updates combined in one GPU kernel | B / 752 | 1,325.27 |
Groups A and B were measured from different saved code versions. Only the two group-B rows support the 13.36× matched comparison above. None of these results uses a trained Tsetlin Machine to choose actions.
GPU tuning: why the default is still 128 threads per block
A thread block is a group of GPU workers. We tested different block sizes to see whether changing this setting improved the optimized implementation. Each run used the same saved code version and workloads, processing 1,088,888,832 model steps:
| Threads per block | Run | Million model steps/s |
|---|---|---|
| 128 — initial control | 761 | 1,342.15 |
| 32 | 762 | 1,420.59 |
| 64 | 766 | 1,386.56 |
| 128 — repeated control | 767 | 1,400.36 |
Using 32 threads was only 1.44% faster than the second 128-thread run. Repeating the unchanged 128-thread setting itself changed the measured speed by 4.34%. That variation is larger than the apparent improvement, so we kept the 128-thread default. These tests do not prove that another block size is consistently faster or that the implementation is theoretically optimal.
Exact code and container versions used for the tests
- Group A source snapshot:
b820ab55e1097a7daf01b4eb3931f9bd8babf4dbe2529cbe16f89eec87a67bbe. - Group B source snapshot:
0618f0880da25d68a089a1a4f2dc902a85cfbc72cd21964668f8d97a2b4a0207. - Launch sweep source snapshot:
22a9c5508a6285d66da6aa5a005f362d8fb028cc39d56b8029370c4b41e2e2b3. - Group B container image:
sha256:2200ffc021b5ff37dd963bd46d30b3ca8d2ae447e19d8f6132e0b8f88e5d633f. - Launch sweep container image:
sha256:e3abd6727a6e8fb413986601ee0c55a5f6df11d01861a0e12e313a8798edc7d0. - Source snapshot hashes identify archived benchmark inputs, not Git commits in this public repository. Run numbers identify the original recorded diagnostics. The sweep used seed 7 for case ordering.
One registered Gym environment supports the following pack_id configurations:
pack_id |
Battery variant |
|---|---|
audi_etron_95kwh_2018 |
Audi e-tron 95 kWh, 2018 |
bmw_i3_60ah_2013 |
BMW i3 60 Ah, 2013 |
nissan_leaf_24kwh_2012 |
Nissan LEAF 24 kWh, 2012 |
tesla_model3_sr_lfp_2020 |
Tesla Model 3 SR+ LFP, 2020 |
vw_id3_58kwh_2020 |
Volkswagen ID.3 58 kWh, 2020 |
vw_id3_77kwh_2020 |
Volkswagen ID.3 77 kWh, 2020 |
Each supports goal="disassemble" or goal="assemble". These are reconstructed
semantic models, not exact OEM digital twins. Ten additional KIT graph
recordings are available in the CLI/viewer, not registered as Gym tasks.
Use ev-assembly-lab list-envs --json for identities, counts and provenance.
import gymnasium as gym
import numpy as np
env = gym.make("ev_battery_disassembly_tm.gym_env:EVAssembly/Disassembly-v0",
pack_id="vw_id3_58kwh_2020", render_mode="ansi")
try:
observation, info = env.reset(seed=7)
while True:
mask = info["action_mask"].astype(bool)
mask[1::2] = False # even: remove; odd: reinstall
action = int(np.flatnonzero(mask)[0])
observation, reward, terminated, truncated, info = env.step(action)
if terminated or truncated:
break
print(env.render())
print(info["is_success"])
finally:
env.close()This is a deterministic diagnostic policy, not a trained agent. For assembly,
select odd actions instead. A mask expresses semantic legality, not physical
safety. Reset is required after termination/truncation. Default horizon is
four times the removable part count. Accepted progress earns 1/N - 0.01,
reversal -1/N - 0.01, rejected prerequisites -1, and completion adds 1.
Goal completion terminates; horizon exhaustion without completion truncates.
Observations are detached uint32 arrays: present_words, legal_words,
and remaining_steps. Bit order is least-significant first. Never cast packed
words directly to float32: 2**24 + 1 loses a bit. For numeric consumers:
from ev_battery_disassembly_tm.gym_env import BinaryAssemblyObservation
env = BinaryAssemblyObservation(gym.make(
"ev_battery_disassembly_tm.gym_env:EVAssembly/Disassembly-v0"))
observation, info = env.reset()
env.close()The wrapper returns MultiBinary features present, legal, and 32
remaining_step_bits. It allocates CPU arrays; packed TM consumers should use
the default representation. Vectorized Gym instances must share pack shapes.
Gym rendering is ANSI text; interactive 3D is a separate snapshot consumer.
Gym is CPU-based. CudaAssemblyBatch is a separate device-resident API with
shared sparse topology, packed state and fused/incremental execution options.
Do not attribute native batch throughput to Gym or physical simulation.
python -m pip install "ev-battery-disassembly-tm[gpu] @ git+https://github.com/pa-research/ev-assembly-lab.git"
NUMBA_ENABLE_CUDASIM=0 python -m ev_battery_disassembly_tm.acceptance --cudaReal CUDA acceptance requires an NVIDIA GPU and compatible driver; it fails
closed instead of falling back to CPU. [contact] supplies optional Newton/Warp
diagnostics and [cad] offline geometry tools. Source CAD is not bundled, and
contact diagnostics are not qualified extraction operations. Hosted CPU CI
does not validate GPU throughput, stream ordering or physical fidelity.
git clone https://github.com/pa-research/ev-assembly-lab.git
cd ev-assembly-lab
python -m pip install '.[gym,test]'
make test
make smokeCI builds a wheel through the source distribution, installs it and runs the
portable package tests on Python 3.11–3.13. Browser logic tests run separately
with Node. make test deliberately selects portable tests: legacy experiment,
source-CAD and optional-device tests require their corresponding research
checkout/assets/backends and are not claimed as hosted-CI coverage.
Semantic acceptance covers six packs, ten KIT recordings, 123 topology round
trips and 7,534 batched steps. It must report physics_qualified: false and
controlled_removal_verified: false. See CONTRIBUTING.md,
CHANGELOG.md and DISTRIBUTION.md.
This release uses existing datasets only. It supports semantic sequence planning and reconstructed geometry inspection, not calibrated robot/tool control, exact fastener/weld mechanics, or safe physical battery servicing. No held-out TM learning result is claimed by this software release.
Project-owned code: Apache-2.0. Third-party data and assets retain their separate terms and attribution. The viewer binds to loopback and is not an authenticated hosted service. Do not expose it publicly.

