Novolis Docs
novolis-physics / DESIGN_REVIEW_FINDINGS.md

Novolis.Physics — Design Review Findings

dotnetphysicssimulationnovolis

Review date: 2026-05-16 Version reviewed: 0.1.0-alpha (net10.0) — remediation shipped in 0.2.0-alpha Scope: Full review per library design review plan (API stability, architecture, consumer ergonomics, correctness, performance spot-check)

Test run: dotnet run --project tests/Novolis.Physics.Unit -c Release — 46/46 passed (967 ms)


Executive summary

Novolis.Physics delivers a coherent force-first core (IForceModel → SimulationPipeline → IIntegrator) with well-factored packages and strong scenario tests. Before stable release, address orphan public API and integration-path documentation. KspLite was removed (2026-05-16): it was an example-only DI shim, not a product package; registration patterns live in examples/dependency-injection.md. No physics blockers were found in the test suite.


Phase 1 — Public API inventory

Package dependency graph

Numerics
  └── Abstractions
        ├── Motion, Gravity, Aerodynamics, Collision.Simple
        ├── Ballistics (+ Collision.Simple)
        └── Orbits (Numerics only)
              Novolis.Physics (meta) → all product packages

Public type classification (49 source files, ~52 public types)

PackageTypeRole
NumericsVector3d, Quaterniond, Ray3d, Sphere3d, Capsule3d, AxisAlignedBox3dValue / geometry
AbstractionsIForceModel<>, IIntegrator<>, IStaticWorldContract
Abstractions~~IContactResolver<>~~Removed in 0.2.0-alpha
AbstractionsRigidBodyState, ForceSample, HitInfoState / sample
MotionSimulationPipeline<>, SemiImplicitEulerRigidBodyIntegrator, FixedStepAccumulatorAlgorithm / orchestration
MotionUniformAccelerationEnergyHelper
GravityPointMassField, PatchedConicPairFieldEnvironment
GravityPointMassGravityModel, PatchedConicGravityModelAlgorithm (IForceModel)
AerodynamicsIAtmosphereModel, ExponentialAtmosphereModelContract / algorithm
AerodynamicsSimpleAeroEnvironment, SimpleLiftDragModelEnvironment / algorithm
Collision.SimpleTriangleMesh (Math) + BvhStaticWorld, EmptyStaticWorldEnvironment / algorithm
Collision.SimpleBvhStaticSphereIntegrator, SphereContactKinematicsStandalone integrator / helper
BallisticsProjectileState, ProjectileProfile, *EnvironmentState / environment
BallisticsProjectileQuadraticDragModel, ProjectileSemiImplicitIntegratorPipeline building blocks
BallisticsProjectileBallisticSimulationFacade (monolithic step)
BallisticsProjectileMath, BallisticsQueries, GroundImpactHelper / query
OrbitsOrbitState, LeapfrogCentralBodySoA, CentralOrbitSimulatorParallel integration stack
OrbitsOrbitalMath, KernelModeHelper
TestSupportOrbitalTestConstants, OrbitalTestStateTest fixtures (not published)
Novolis.Physics(none)Meta-package only

Former `Novolis.Physics.KspLite` package removed — see [examples/dependency-injection.md](./examples/dependency-injection.html).

Internal (non-public): TriangleRay in Collision.Simple.

Orphan and unused-contract flags

ItemStatus
~~IContactResolver<TBody>~~Removed in 0.2.0-alpha; contact via SphereContactKinematics + BvhStaticSphereIntegrator
IForceModel.Evaluate(..., timeSeconds)Documented as simulation time; shipped models are time-invariant
~~OrbitalTestConstants / OrbitalTestState~~Moved to Novolis.Physics.TestSupport.Orbits in 0.2.0-alpha

Phase 2 — Architecture coherence

Force-first alignment by package

PackageAligns with pipeline?Notes
NumericsN/A (foundation)Right-handed 3D; no axis convention doc at type level
AbstractionsYesClean split: state vs ForceSample vs HitInfo
MotionYesCanonical orchestration; semi-implicit Euler for rigid bodies
GravityYesPointMassField uses ReadOnlyMemory + Span safely
AerodynamicsYesSimpleLiftDragModel is proper IForceModel; atmosphere via IAtmosphereModel
Collision.SimplePartialQuery-only IStaticWorld; integration via separate BvhStaticSphereIntegrator
BallisticsPartialPipeline path exists; ProjectileBallisticSimulation duplicates gravity+drag inline
OrbitsNoLeapfrog SoA; does not use IForceModel / SimulationPipeline
~~KspLite~~RemovedDI example moved to docs

Integration paths (validated)

Four distinct simulation styles coexist:

  1. `SimulationPipeline` + `IForceModel` + `IIntegrator` — rigid body and projectile (with adapters).
  2. `ProjectileBallisticSimulation.Step` — convenience facade; parity-tested against pipeline (ProjectileDragPipelineParityTests).
  3. `BvhStaticSphereIntegrator` — sphere vs static mesh with contact resolution; used in room/billiards tests; not wired through IContactResolver.
  4. `CentralOrbitSimulator` / `LeapfrogCentralBodySoA` — central-body leapfrog; scalar/vectorized kernels.

Architecture decision records (recommended)

ADR-1: Orbits remains a separate stack (ACCEPT)

Decision: Keep Novolis.Physics.Orbits as a leapfrog SoA integrator, not folded into IForceModel for v1.

Rationale: Different numerical method (symplectic leapfrog vs semi-implicit Euler), SoA layout for N bodies, and existing energy/angular-momentum tests. PointMassGravityModel already covers pipeline-style gravity for games.

Follow-up: Document when to use Orbits vs Gravity+Motion; consider moving OrbitalTestConstants/OrbitalTestState to test assembly or renaming to OrbitalReferenceOrbit before stable.

ADR-2: Collision stays query-only in v1 (ACCEPT)

Decision: IStaticWorld provides ray/sweep queries only; no full rigid-body contact solver in the pipeline.

Rationale: Matches scope; BvhStaticSphereIntegrator covers sphere-in-room scenarios. XML docs already flag approximate sweeps.

Follow-up: Add consumer doc section on sweep limitations and when BvhStaticSphereIntegrator vs manual pipeline stepping applies.

ADR-3: Ballistics — pipeline is canonical; facade is supported (ACCEPT)

Decision: Recommend SimulationPipeline + ProjectileQuadraticDragModel + ProjectileSemiImplicitIntegrator for extensibility; keep ProjectileBallisticSimulation as ergonomic entry point.

Rationale: ProjectileDragPipelineParityTests proves equivalence for gravity + quadratic drag. Facade reduces boilerplate for cannon-style problems.

Follow-up: README example showing both patterns and when to add custom IForceModel instances.

ADR-4: IContactResolver — remove or defer before stable (ACCEPT removal)

Decision: Remove from public API in next breaking window, or move to Novolis.Physics.Abstractions.Experimental if contact pipeline is planned.

Rationale: Misleading contract; actual API is SphereContactKinematics.ReflectWithRestitution inside BvhStaticSphereIntegrator.


Phase 3 — Consumer ergonomics

Persona walkthroughs

Minimal (point mass + Euler)

Path: MinimalSimulationExampleTests — direct construction of FixedStepAccumulator, SimulationPipeline, and PointMassGravityModel.

Friction: Low; see README quick start and INTEGRATION.md.

Game room (gravity + collision + bounce)

Path: BasketballEarthRoomCollisionTests, BouncingBallCollisionTests — build BvhStaticWorld from mesh, loop BvhStaticSphereIntegrator.AdvanceOneStep or AdvanceWithUniformAccelerationAndLinearDrag with UniformAccelerationEnergy for traces.

Friction: Collision is outside SimulationPipeline; gravity/drag applied inside integrator helper, not as IForceModel. Two mental models required.

Ballistics (drag + ground impact)

Path: Most tests use ProjectileBallisticSimulation; parity tests prove pipeline equivalence. BallisticsQueries wraps IStaticWorld sweeps.

Friction: Low for ballistics-only users; unclear which path to choose without reading ProjectileDragPipelineParityTests.

Ergonomics rubric (1 = poor, 5 = excellent)

CriterionScoreEvidence
Discoverability4README quick start + INTEGRATION.md (post-review)
Composition4SimulationPipeline constructor is simple; README + INTEGRATION.md cover wiring
Type clarity4TBody/TEnvironment pairs are consistent; projectile vs rigid body types are distinct
Convention docs3+Y up, −Y gravity documented on ProjectileBallisticSimulation; not centralized
Error surfaces3FixedStepAccumulator and mesh ctor validate inputs; zero mass/inertia guarded in integrator

Documentation gaps

  • TestSupport README — references StarConflictsRevolt paths and project names.
  • No worked example of sweep failure modes (fast sphere, thin geometry, capsule endpoint sampling).

Phase 4 — Correctness and performance

Test → invariant mapping

Test classInvariant proven
SimulationPipelineTestsForces sum before integration
ProjectileDragPipelineParityTestsFacade ≡ pipeline (1 and 200 steps)
PatchedConicGravityTestsSOI switching uses correct μ and source
EllipticalOrbitTwoBodyTestsClosed/half-orbit geometry; energy drift ≤ 1e-5; Lz drift ≤ 1e-6; scalar ≡ vectorized
CollisionSweepScenarioTestsSweep hits ground before full displacement
FixedStepAccumulatorTestsDeterministic step count; fractional carry across frames
AnalyticalProjectileTests / BallisticPropertyTestsVacuum/drag trajectories vs analytic expectations
BvhStaticWorldTestsRaycast/sweep basics on simple meshes
AerodynamicsModelTestsLift/drag direction and magnitude sanity
MinimalSimulationExampleTestsFixed step + pipeline + gravity end-to-end
BasketballEarthRoomCollisionTestsLong-run stability; energy traces (scenario, not strict conservation)

Performance spot-check (hot Step paths)

LocationHeap allocation in steady-state step?Notes
SimulationPipeline.StepNoforeach over force array
SemiImplicitEulerRigidBodyIntegrator.StepNostack Vector3d / Quaterniond
ProjectileSemiImplicitIntegrator.StepNostruct return
ProjectileBallisticSimulation.StepNoreuses instance integrator
BvhStaticSphereIntegrator.AdvanceOneStepNo per iterationSphere3d on stack; loop bounded by maxReflectionsPerStep
LeapfrogCentralBodySoA.Step (scalar)NoSoA arrays allocated at ctor
LeapfrogCentralBodySoA.Step (vectorized)Nostackalloc lanes + Vector<double> (struct, no heap)
BvhStaticWorld ctorYes (once)List<BvhNode> → ToArray() at build
CentralOrbitSimulator.SimulateForYes per callnew LeapfrogCentralBodySoA each invocation

Conclusion: Sim-loop paths are allocation-conscious. Avoid calling CentralOrbitSimulator.SimulateFor inside per-frame hot paths without pooling/reuse.


Findings register

IDSeverityAreaFindingRecommendationEffort
DR-001~~Major~~ ResolvedAbstractionsIContactResolver<TBody> removed; contact API documented in INTEGRATION.mdFixed in 0.2.0-alpha—
DR-002~~Major~~ ResolvedArchitectureFour integration stylesINTEGRATION.md added—
DR-003~~Major~~ ResolvedKspLiteRemoved example packageREADME + INTEGRATION.md + optional DI doc—
DR-004~~Major~~ ResolvedPackagingKspLite/meta mismatchKspLite removed; meta = product packages only—
DR-005~~Major~~ ResolvedOrbitsFixtures moved to Novolis.Physics.TestSupport.OrbitsFixed in 0.2.0-alpha—
DR-006~~Major~~ ResolvedCollisionSweep limitations documented; SweepLimitationScenarioTests addedFixed in 0.2.0-alpha—
DR-007~~Minor~~ ResolvedAbstractionstimeSeconds documented on IForceModel and shipped modelsFixed in 0.2.0-alpha—
DR-008~~Minor~~ ResolvedDocsRoot README had no code exampleREADME quick start added—
DR-009~~Minor~~ ResolvedDocsTestSupport README rewritten for Novolis.PhysicsFixed in 0.2.0-alpha—
DR-010~~Minor~~ ResolvedBallisticsDual path documented in INTEGRATION.md and READMEFixed in 0.2.0-alpha—
DR-011~~Minor~~ ResolvedMotionCaller time advance documented on SimulationPipeline.Step and INTEGRATION.mdFixed in 0.2.0-alpha—
DR-012~~Minor~~ ResolvedKspLiteN/APackage removed—
DR-013~~Nit~~ ResolvedOrbitsReuse overload added; convenience overload documentedFixed in 0.2.0-alpha—
DR-014~~Nit~~ ResolvedCollisionDocumented in INTEGRATION.mdFixed in 0.2.0-alpha—
DR-015~~Nit~~ ResolvedNumericsREADME Conventions + Numerics package descriptionFixed in 0.2.0-alpha—

Blockers: None identified in this review.


Prioritized post-review backlog

P0 — Before stable (0.2.0) — Complete

  1. ~~DR-001~~ — IContactResolver removed.
  2. ~~DR-005~~ — Orbit fixtures moved to TestSupport.

P1 — Early stable — Complete

  1. ~~DR-006~~ — Sweep limitations doc + test.
  2. ~~DR-010~~ — Ballistics path documentation.
  3. ~~Semver policy~~ — VERSIONING.md.

P2 — Nice to have — Complete

  1. ~~DR-007~~ — timeSeconds documented.
  2. ~~DR-013~~ — Orbit simulator reuse API.
  3. ~~DR-009~~ — TestSupport README cleanup.
  4. ~~DR-011, DR-014, DR-015~~ — Documented in 0.2.0-alpha.

Appendix A — Verified pre-seeded hypotheses

HypothesisResult
IContactResolver has zero implementationsConfirmed
Multiple integration styles coexistConfirmed (four paths)
KspLite was example-onlyRemoved; DI recipe in docs/examples
Sweeps are approximateConfirmed (XML on IStaticWorld)

Appendix B — Suggested README example (for DR-008)

using Novolis.Physics.Abstractions;
using Novolis.Physics.Gravity;
using Novolis.Physics.Motion;
using Novolis.Physics.Numerics;

var integrator = new SemiImplicitEulerRigidBodyIntegrator();
var gravity = new PointMassGravityModel();
var pipeline = new SimulationPipeline<RigidBodyState, PointMassField>(integrator, gravity);

var field = new PointMassField([(Vector3d.Zero, 3.986e14)]);
var body = new RigidBodyState(
    new Vector3d(6_771_000, 0, 0),
    new Vector3d(0, 7_500, 0),
    Quaterniond.Identity, Vector3d.Zero, mass: 1.0,
    inertiaDiagonalBody: new Vector3d(1, 1, 1));

body = pipeline.Step(body, field, dtSeconds: 1.0);

End of design review findings.