nbody_sim_core
Engine-agnostic N-body simulation core package for Dart. High-iteration N-body vibes, production-ready core APIs.
nbody_sim_core provides:
- Physics domain models and contracts.
- Multiple engine backends (
Dart,Isolate,Rust FFI). - Scenario and snapshot serialization utilities.
- Schema migration and validation helpers.
- Embedded Rust crate + native build scripts.
Install
dependencies:
nbody_sim_core: ^0.1.2
Public API
import 'package:nbody_sim_core/nbody_sim_core.dart';
Barrel exports:
models.dart: configs, vectors, bodies, state, telemetry, edit contracts.engine.dart:SimulationEngine+ concrete backends.scenario.dart: schema validator + migrator APIs.
Core Concepts
SimulationConfig- Integrator:
semiImplicitEuler,velocityVerlet,rk4 - Collision:
elastic,inelasticMerge,ignore - dt policy:
fixed,adaptive - Solver:
pairwise,barnesHut,auto
- Integrator:
SimulationBody- Required:
id,mass,radius,position,velocity,colorValue
- Required:
SimulationEnginecontractinitialize,setConfig,applyEdit,steploadScenario,saveScenariosnapshot,restoreSnapshotgetState,dispose
Quick Start (Pure Dart backend)
import 'package:nbody_sim_core/nbody_sim_core.dart';
Future<void> main() async {
final engine = DartSimulationEngine();
await engine.initialize(
config: SimulationConfig.scientificDefault,
bodies: const [
SimulationBody(
id: 'sun',
mass: 1000,
radius: 2.0,
position: Vec2.zero,
velocity: Vec2.zero,
colorValue: 0xFFFFD54F,
),
SimulationBody(
id: 'planet',
mass: 1,
radius: 0.5,
position: Vec2(12, 0),
velocity: Vec2(0, 9.2),
colorValue: 0xFF64B5F6,
),
],
);
final summary = await engine.step(240);
final state = engine.getState();
print('tick=${state.tick}, simTime=${state.simTime}, mode=${summary.lastSolverMode}');
await engine.dispose();
}
Backend Options
Use whichever backend fits your runtime and performance goals.
DartSimulationEngine- Easiest setup.
- No native prerequisites.
IsolateSimulationEngine- Runs simulation off the UI/main isolate.
- Backend selection:
EngineBackend.auto,EngineBackend.rust,EngineBackend.dart.
RustFfiSimulationEngine- Direct native Rust backend.
- Highest performance path when native library is present.
Example:
import 'package:nbody_sim_core/engine.dart';
import 'package:nbody_sim_core/models.dart';
final engine = IsolateSimulationEngine(
backend: EngineBackend.auto,
// Optional:
// rustLibraryPath: '/absolute/path/to/libgravity_engine.dylib',
);
Direct Rust backend:
import 'package:nbody_sim_core/engine.dart';
final engine = RustFfiSimulationEngine(
// Optional if GRAVITY_ENGINE_LIB is set or library is in ./native:
libraryPath: '/absolute/path/to/libgravity_engine.dylib',
);
Rust Native Build
The package includes:
- Rust crate:
rust/gravity_engine - Build scripts:
tool/build_rust_engine.shtool/build_rust_engine.ps1
From package root:
# Run Rust tests
cargo test --manifest-path rust/gravity_engine/Cargo.toml
# Build native library for host target and copy to ./native/<abi>/
./tool/build_rust_engine.sh
# Build specific targets and copy to ./native/<abi>/
./tool/build_rust_engine.sh aarch64-apple-darwin x86_64-apple-darwin
On Windows PowerShell:
.\tool\build_rust_engine.ps1
Generated library names:
- macOS:
libgravity_engine.dylib - Linux:
libgravity_engine.so - Windows:
gravity_engine.dll
Bundled ABI folders:
native/macos-arm64/native/macos-x64/native/linux-x64/native/windows-x64/
Rust Library Discovery
When using Rust backends, RustFfiBindings resolves the native library in this order:
- Explicit
libraryPathpassed into engine/bindings. GRAVITY_ENGINE_LIBenvironment variable../native/<abi>/<platform-library-name>./native/<platform-library-name>./rust/gravity_engine/target/release/<platform-library-name><nbody_sim_core package root>/native/<abi>/<platform-library-name><nbody_sim_core package root>/native/<platform-library-name>- Dynamic loader default lookup by filename.
The package root path is resolved from the consumer app's
.dart_tool/package_config.json, so binaries bundled inside the package can be
loaded without manually copying them into app cwd.
Prebuilt Binary Pipeline
Desktop Rust FFI targets are built in CI via:
.github/workflows/build-native-binaries.yml
Current CI matrix:
- macOS:
aarch64-apple-darwin,x86_64-apple-darwin - Linux:
x86_64-unknown-linux-gnu - Windows:
x86_64-pc-windows-msvc
Runtime Edits (Create/Update/Delete Bodies)
import 'package:nbody_sim_core/models.dart';
await engine.applyEdit(
const BodyCreate(
SimulationBody(
id: 'probe',
mass: 0.1,
radius: 0.1,
position: Vec2(0, 20),
velocity: Vec2(6, 0),
colorValue: 0xFFFFFFFF,
),
),
);
await engine.applyEdit(
const BodyUpdate(
id: 'probe',
velocity: Vec2(6.5, 0.2),
label: 'Science Probe',
kind: 'probe',
),
);
await engine.applyEdit(const BodyDelete('probe'));
Scenario and Snapshot APIs
import 'package:nbody_sim_core/models.dart';
// Save current state as scenario
final scenario = await engine.saveScenario();
// Restore scenario
await engine.loadScenario(scenario);
// Point-in-time snapshot
final snap = await engine.snapshot();
await engine.restoreSnapshot(snap);
JSON import/export:
import 'dart:convert';
import 'package:nbody_sim_core/models.dart';
// Export scenario JSON string
final scenario = await engine.saveScenario();
final scenarioJsonString = jsonEncode(scenario.toJson());
// Import scenario JSON string
final parsedScenarioMap = jsonDecode(scenarioJsonString) as Map<String, dynamic>;
final importedScenario = ScenarioModel.fromJson(parsedScenarioMap);
await engine.loadScenario(importedScenario);
// Export snapshot JSON string
final snapshot = await engine.snapshot();
final snapshotJsonString = jsonEncode(snapshot.toJson());
// Import snapshot JSON string
final parsedSnapshotMap = jsonDecode(snapshotJsonString) as Map<String, dynamic>;
final importedSnapshot = SnapshotModel.fromJson(parsedSnapshotMap);
await engine.restoreSnapshot(importedSnapshot);
Schema Migration + Validation
import 'package:nbody_sim_core/scenario.dart';
final migrated = ScenarioSchemaMigrator.migrateToLatest(rawJson);
final issues = ScenarioSchemaValidator.validateScenarioJson(migrated);
if (issues.isNotEmpty) {
for (final issue in issues) {
print(issue); // "<path>: <message>"
}
}
Deterministic vs Adaptive Notes
- Deterministic mode is designed for replayable runs.
SimulationConfig.validate()rejectsdeterministic == truewithDtPolicy.adaptive.- For exact replay workflows, keep:
deterministic: truedtPolicy: DtPolicy.fixed
Minimal Lifecycle Pattern
final engine = IsolateSimulationEngine();
await engine.initialize(config: SimulationConfig.scientificDefault, bodies: initialBodies);
await engine.step(1);
final state = engine.getState();
await engine.dispose();
Troubleshooting
- Error: unable to open gravity engine dynamic library
- Build native library with
./tool/build_rust_engine.sh. - Set
GRAVITY_ENGINE_LIBto an absolute library path. - Or pass
rustLibraryPathexplicitly.
- Build native library with
StateError: Rust engine has not been initialized- Call
initialize()beforestep,setConfig,applyEdit, etc.
- Call
- Schema validation failures
- Run migrator first:
ScenarioSchemaMigrator.migrateToLatest(...). - Re-run validator and inspect issue paths/messages.
- Run migrator first:
License
MIT (see LICENSE).