npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uniscenarios/sim-engine

v0.1.0-rc.44

Published

Downloads

234

Readme

@uniscenarios/sim-engine

The default for new and regenerated simulation is the force-based dynamic-v1 backend. The deterministic kinematic-v1 choreography model remains available explicitly and remains the recorded mode for immutable legacy trace replay. Both carry explicit trace provenance. Validation scope, performance gates, and current non-claims are documented in ../../docs/physics-validation.md.

Layer 3 of docs/agent-authoring-architecture.md: the deterministic scenario simulation engine. Pure TypeScript, zod for the input contract, no rendering dependency — the editor preview and the headless CLI run this code byte for byte, so there is no parity lane to maintain.

import {
  buildLaneGraph,
  parseSimScenarioInput,
  runSimulation,
  evaluateTrace,
} from '@uniscenarios/sim-engine';

const graph = buildLaneGraph(topologyIndexJson); // dev-assets/<map>/topology-index.json.gz
const input = parseSimScenarioInput(doc);
const { trace, issues, arrival } = runSimulation(input, { graph });
const verdict = evaluateTrace(trace); // 'accept' | 'reject' + findings

The seam: SimScenarioInput

SimScenarioInput is a fully resolved concrete scenario. No logical anchors, no parameter references, no expressions, no map queries — every actor already has a pose, a route and numeric rules; every trigger already has numeric thresholds. Producing one from a ScenarioTemplate v2 (site match + parameter draw + expression evaluation) is the adapter's job, in another package. This type is the stable target that adapter builds against.

SimScenarioInput
  schemaVersion, mapId, clipSeconds=20, warmupSeconds=5, dt=0.02, seed
  metricSubject?                       ← which actor the metrics are about
  actors[]      { id, kind: vehicle|pedestrian, dims{l,w,h},
                  initial { laneRef?{rsl,s,tFrac}, pose{x,z,headingRad}, speedMps },
                  behavior { rules{obeySignals,yield,collisionAvoidance,
                                   aggression,speedFactor},
                             route: RouteSpec, cruiseSpeedMps? },
                  presentAtStart, tags[] }
  interactions[]{ id, actorId, trigger, <verb>, dynamics?, until? }
  signalPrograms[]{ id, phases[{phase,durationS}], offsetS, loop,
                    stopLines[{rsl,s,connectingLaneRsls[]}],
                    mapBinding?{junctionId,controllerIds[],headIds[],timingSource} }
  occluders[]   { id, obb{center,lengthM,widthM,headingRad,heightM} }

Seven verbs over five axes (exactly the research doc's vocabulary):

| verb | axis | shape | |---|---|---| | speed | longitudinal | target: absolute \| delta \| factor \| match \| stop | | gap | longitudinal | target{actorId}, value, mode: time \| distance | | changeLane | lateral | target: left \| right \| lane \| actorLane | | laneOffset | lateral | target{mode: meters \| fraction, value} | | route | topology | target: RouteSpec | | exist | existence | target{state: present \| absent} | | set | discrete state | target{key, value} over the typed key registry |

dynamics = {shape: step|linear|sinusoidal|cubic, constraint: rate|time|distance, value} is mandatory on every shaped verb — never defaulted.

Triggers: at(t) · after(id, delayS) · when(condition, byLatest, ifNever: skip|fire) · arrival({of, at, syncWith, ttc|deltaT}). byLatest is mandatory on when, because a condition that never fires is a silent bug. Conditions: distance (alongLane|euclidean), ttc, headway, reaches (circle | polygon | lane s-window), speed, standstill, signal, collision, visible(a, to: b), and shallow and/or/not.

One axis, one owner; later preempts earlier. A newly fired interaction replaces whatever held its axis and emits a preemption event. No priorities, no nesting. set owns one axis per key, so unrelated state writes coexist.

Coordinate frames

The engine computes entirely in the xodr-local frame — x east, y north, metres, headings CCW from +x — because that is the frame the topology index's lane polylines already use, so route arc length and OBB overlap need no per-tick transform.

| surface | frame | |---|---| | SimScenarioInput poses, points, occluder OBBs | scene {x, z} (y-up) | | everything inside the engine | xodr-local {x, y} | | SimTrace.ticks (header.frame === 'xodr-local') | xodr-local {x, y} |

scene = (x, 0, −y), and headingRad is numerically identical in both. Use localFromScene / toSceneXZ, or traceToSceneFrame(trace) for a wholesale conversion. See src/frames.ts.

Lane graph: why orientation is derived

topology-index.json.gz stores lane polylines in geometric s order and its predecessors/successors lists are effectively undirected — on yale-street, 658 of 1534 links fail a naive "my last point is your first point" test, and many lanes list the same neighbour in both arrays. So LaneGraph works with directed lanes ({rsl, reversed}) and derives successors geometrically: a neighbour qualifies when one of its admissible orientations starts within ENDPOINT_TOL_M (0.5 m) of our exit point. Non-junction lanes are pinned to their sign-implied OpenDRIVE direction so the walker cannot drive the wrong way; junction connecting lanes are free, resolved by the approach they were entered from. 93 % of yale-street's driving lanes get a directed successor; the rest are genuine map-boundary dead ends.

Trace

Gzipped canonical JSON, columnar per actor:

header  { engineVersion, inputHash, seed, mapId, topologyDigest, dt,
          clipSeconds, warmupSeconds, frame: 'xodr-local', actorIds[], metricSubject }
ticks   { t[], actors{ id → { x[], y[], headingRad[], speedMps[], laneRsl[], s[], present[] } } }
events  trigger_fired | trigger_skipped | preemption | released | lane_change |
        lane_change_rejected | collision | spawn | despawn | state_set
metrics { minTTC{value,t,pair}, minDistance[], requiredDecelMax{},
          revealToConflict?, collisions[], triggerNeverFired[],
          clippedCriticality, ticksSimulated }

Only t ∈ [0, clipSeconds] is recorded; the warm-up prologue is excluded by construction, and the sample at t = 0 is the prologue's final state.

Determinism

Bit-identical traces for identical inputs, and identical traces for inputs that differ only in declaration order. Enforced by: sorted iteration at every fan-out, a plan/apply split so no actor reads a neighbour that has already stepped, integer-indexed time (t = (i − warmupTicks) · dt, never accumulated), a seeded xoshiro128** instead of Math.random, and channel quantisation before serialisation. determinism.test.ts proves all of it, including a source scan that fails the build if Math.random or a wall-clock read appears anywhere in src/.

Performance

10 actors × 20 s at dt = 20 ms on yale-street geometry: ~75 ms per run (~13 300 recorded ticks/s, ~266× real time) on an M-series laptop. Build the LaneGraph once and share it across runs.

Default dynamic-v1 motion

physics: { mode: 'dynamic-v1' } explicitly pins the force-based motion backend for forward car and generic vehicle actors. It uses deterministic 5 ms substeps by default and records body velocity, yaw rate, steering, wheel speed, axle loads and tyre utilization alongside solver provenance. The scenario layer still owns targets and safety decisions; a speed controller and preview path tracker turn those targets into throttle, brake and steering.

This is one calibrated generic passenger-car model, not vehicle-specific parameter identification and not a CARLA-equivalence claim. It has planar body dynamics, actuator lag, aerodynamic/rolling resistance, quasi-static longitudinal axle load transfer and combined-slip axle friction circles. It does not yet model suspension, pitch/roll/heave, individual wheels, powertrain gears, ABS/ESC, road grade or collision impulses. Collision detection and event timing remain active, but contact does not alter velocity in this slice.

Omitting physics resolves to dynamic-v1 from engine 0.3.0 onward. New and regenerated editable products write that selection explicitly. Existing verified evidence is replayed from its recorded provenance; physics: { mode: 'kinematic-v1' } remains the explicit legacy pin, and its established motion tracks do not pass through the dynamic backend.

Deliberate simplifications

Stated plainly, because they bound what a metric from this engine means:

  • Kinematic-v1 has no tyre physics. Vehicles are path followers with a bicycle-ish body slip (heading = pathHeading + atan2(lateralRate, v)) and per-class acceleration clamps. No yaw inertia, no load transfer, no friction circle.
  • TTC is the closing-speed form (gap / closing speed along the line of centres) using circumscribed radii, not OBBs. Exact for rear-end and head-on geometries; conservative and slightly under-reporting for crossing ones. A true path-intersection TTC needs map-intel's junction conflictPairs, which this package deliberately does not depend on.
  • rules.yield uses a coarse crossing-path scan (14 samples at 5 m, 2.5 m proximity, 2.5 s arrival window, ignored below a 0.4 rad heading difference so car-following is not double-counted) rather than a precomputed conflict-point table. Enough to make junction behaviour sensible; not a substitute for the real table when that lands.
  • Line of sight is a 2-D ground-plane test. Occluder heights are carried but unused — reveal-to-conflict is dominated by plan-view geometry, and a 3-D test would need render meshes.
  • The gap controller is a PD loop, not IDM. Its equilibrium gap equals the commanded gap exactly, unlike IDM's 1/√(1−(v/v₀)⁴) offset, which matters when a scenario declares "2.0 s headway" and a filter later checks it.
  • lights.*, doors.*, pose.*, env.* are recorded state only. They land in stateKeys and the event log for the renderer and exporter; no controller consumes them yet.
  • metrics.invariantResiduals is typed but not populated — invariants live in the template layer, so residual checking belongs to the adapter that knows what was declared.
  • The whole-clip runway guard applies to vehicles on lane routes only. Pedestrians and freeform paths are supposed to finish mid-clip, and an actor the scenario explicitly despawns is exempt.