Skip to content

Changelog

All notable changes to this project will be documented in this file.

[Unreleased]

[0.8.1] - 2026-09-07

Changed

  • Expanded Ruff lint checks and enabled all ty diagnostics as errors, with targeted exceptions for numerical annotations and established domain patterns.
  • Added formatting and spelling checks to CI, and enforced American spelling in source code and tests while preserving existing public identifiers.
  • Updated annotations and lint fixes across the solver, CLI, and tests to meet the stricter checks.

[0.8.0] - 2026-09-06

Added

  • Added wheel-center recession and contact-patch lateral migration relative to the design state, with positive values for rearward and inboard movement, respectively, plus their hub-travel derivatives.
  • Added installed linear spring length and its hub-travel derivative, separate from damper length. Coilovers share spring and damper endpoints; torsion springs have no linear spring-length metric.
  • Added longitudinal and lateral steering-axis offsets at wheel-center height for both physical and virtual steering axes. The offsets use a road-parallel plane and are positive rearward and inboard, respectively.
  • Added dimensionless braking and traction anti ratios and their corresponding angles alongside the existing anti-dive, anti-lift, and anti-squat percentages.
  • Added axle Ackermann percentage for rack-steered axles. Perfect Ackermann is 100 percent and parallel steering is zero; the result is undefined at straight ahead or when the required steering geometry is unavailable.
  • Added per-corner U-bar anti-roll-bar end displacement, measured as signed arc length from design, and its derivative with respect to that corner’s hub travel.

[0.7.0] - 2026-08-29

Added

  • Added simple pushrod/pullrod and toe setup shims. The setup-minus-design thickness changes the corresponding pushrod, pullrod, track-rod, or toe-link constraint length during the solve, with side-local corner and axle setup support. Pushrod shims require pushrod-rocker actuation.

[0.6.2] - 2026-08-18

Changed

  • Quick jargon cleanup.

[0.6.1] - 2026-08-18

Added

  • Added the multi-link locating architecture (type: multi_link) at corner and axle scope: four independent two-joint locating rods (upper front/rear, lower front/rear), each with its own outboard ball joint on the rigid wheel carrier, plus the configured track rod or toe link. Direct-coilover and pushrod-rocker actuation mount on the upright, and a direct spring pickup may alternatively ride a lower link’s centerline as a derived point (a damper fork clamped around the two-force rod), so no rigid off-axis pickup is ever asked of a two-joint member. The corner publishes a locked-internals (damper length) suspension hold, and its steering geometry reports exclusively through the virtual screw-axis metric family because no two ball joints define a physical kingpin.

  • Made the physical steering axis optional on corner architectures. steering_axis_points() may now return None, in which case the physical caster/KPI/scrub/trail/offset columns and their hub-travel derivatives are omitted from metric rows and metadata while the *_virtual family still reports. Plane-intersection instant centers may likewise be undefined for a multi-link carrier; the dependent swing-arm and anti-geometry metrics degrade to null values.

  • Added topology-owned scalar drive coordinates for named actuator positions and true element lengths. Element-length targets support relative displacement or absolute pin-center length, analytical residuals and Jacobians, analytical tangent fields, structured result metadata, and CSV/Parquet export.

  • Added a geometry-independent sweep-target vocabulary with stable labels, units, featured choices, and corner/shared side ownership. Suspensions remain authoritative for whether a selected coordinate is actually driveable.

  • Added an optional independent linear damper to pushrod-rocker double-wishbone corners. Its chassis and rocker pickups participate in the physical assembly, visualization topology, damper-length metric, and motion-ratio derivatives; axle assemblies publish independently side-qualified dampers.

  • Added a shared rack actuator-position coordinate so steering intent is represented explicitly instead of being inferred from a physical track-rod point target.

Changed

  • Upright presentation geometry is now a spoke set: one rendered path from the synthetic axle midpoint to each carried joint (e.g. axle midpoint to UBJ, LBJ, and track-rod end on a double wishbone), replacing the authored outline segments. An upright-mounted actuation pickup (pushrod outboard or direct spring pickup) receives its own spoke. The midpoint is a presentation-only point and draws no hardpoint marker.
  • Generalized sweep solving around one scalar-target protocol. Point projections, named actuator positions, and element lengths now share value expansion, absolute conversion, measurement, point partials, remapping, presentation metadata, validation, and export machinery.
  • Sweep documents and public target metadata now use the required type discriminator with point, actuator_position, or element_length values. The same spelling is used by SweepParameter, DriveCoordinateInfo, and the public target vocabulary.
  • Side policy and units are owned by the coordinate enums rather than repeated in resolver, export, and presentation tables. Corner-owned targets require an explicit side; shared coordinates are unsided. Standalone corner geometry is canonically left, while right-hand corners exist only within an axle.
  • The CLI writes measured values for every scalar sweep dimension in authored order and rejects ambiguous duplicate export column names instead of silently overwriting one projection.
  • Axle coordinate composition now remaps any number of coordinate points rather than assuming every non-actuator coordinate has exactly two endpoints.

Fixed

  • Absolute or resolved element lengths below the geometric tolerance are rejected before solving with a target-indexed error. Coincident endpoints at an intermediate solver iterate use the finite zero derivative of the soft norm instead of raising from inside SciPy’s Jacobian callback.
  • Missing target points, duplicate scalar coordinates, unavailable sides, and structurally undriveable target rows now produce precise, human-readable validation errors with their offending target indices.

Breaking changes

  • Replaced the public sweep-target kind field with type; there is no legacy input alias. Every target must provide the discriminator explicitly.
  • Removed the TargetPositionMode alias. Scalar targets use TargetValueMode for both relative and absolute values.
  • A physical trackrod_inboard point target no longer satisfies the steering rack actuator requirement; use type: actuator_position with actuator: rack.
  • Standalone right-corner geometry is rejected. Model a right-hand corner as part of an axle instead.

[0.6.0] - 2026-08-16

Changed

  • Relicensed from Apache-2.0 to AGPL-3.0-only with a separate commercial licensing option. Releases up to and including v0.5.1 remain available under Apache-2.0. External contributions now require acceptance of the contributor terms and copyright assignment described in CONTRIBUTING.md.

[0.5.1] - 2026-07-31

Added

  • Added the trailing_arm suspension architecture for standalone corners and mirrored or explicitly authored axles. The rigid wheel carrier rotates about the oblique line through the two fixed arm pivots, producing the expected semi-trailing-arm camber and toe motion.
  • Added coilover and pivot-mounted torsion-bar spring layouts for trailing arms. The torsion-bar layout models arm rotation about its authored transverse axis and includes a separate chassis-to-carrier damper.
  • Added renderer-neutral trailing-arm, carrier, wheel, damper, coilover, and torsion-bar elements together with side/front-view instant centers, torsion_bar_twist, damper motion ratio, and torsion motion-ratio metrics.
  • Added schema and physical validation for pivot geometry, spring selection, torsion-axis placement, steering exclusion, and unsupported shared axle mechanisms.

[0.5.0] - 2026-07-30

Added

  • A WorldSpace API maps the axle-local road plane to the straight, level world Z = 0 plane for presentation. Gravity is always world -Z; local axle heave and roll are represented, while unobservable whole-vehicle pitch and yaw are not inferred. The structured transform is carried on axle analysis frames.

Changed

  • Steering metrics now report both toe_angle (the project toe-in-positive convention) and ISO vehicle-fixed steer_angle; both have hub-Z and, where applicable, rack-displacement derivatives.
  • A two-corner axle now derives both wheel contact center points from the solved wheel geometry using one shared zero-grade ground plane, extruded along chassis X, instead of letting each corner independently assume a flat +Z ground. Standalone corners keep the local +Z assumption, since one wheel cannot define a ground line.
  • Metric calculation and world presentation now reconstruct the same axle-local RoadPlane independently from the two stored wheel contact centers. Metrics no longer depend on WorldSpace, and the opposite-axle height input and inferred-pitch model have been removed.
  • Steering-axis ground metrics follow the ISO 8855 tire-axis decomposition on the full shared ground plane, including bank, with the right-handed X_T × Y_T = Z_T basis. steering_axis_offset_ground is the signed lateral component along tire Y_T, mechanical_trail is the signed longitudinal component along tire X_T, and scrub_radius is the unsigned distance between the tire contact center and steering-axis ground intersection.
  • The coupled axle tangency solve is a stateless post-solve closure rather than a derived point. AxleSuspension drops the composed WHEEL_CONTACT_CENTER entries from its derived specification and writes both contact centers once per accepted state, so no per-corner flat-road center can reach an axle state.
  • State finalization has one authoritative boundary. The low-level solver requires an accepted-state finaliser — a caller that passes a no-op receives kinematic intermediates, never complete-looking solved states — and solve_sweep() passes the ground closure through it, so every state is closed inside the solver’s accept path. evaluate_solved_sweep() copies externally supplied states and finalizes the copies without mutating the caller’s, while solve_evaluated_sweep() evaluates its own already finalized states directly, so nothing is closed twice and analyze_solved_sweep() can no longer evaluate scalar metrics against stale tangents while derivative evaluation closes its own dual positions.
  • PointCatalog separates how a point is computed from whether it may be driven. Suspensions declare closure_points() — points the post-solve closure writes — and those classify as derived, so the catalog no longer publishes an axle’s coupled wheel contact centers as fixed geometry. output_only is pure targeting policy, never changes classification, and is invariantly a subset of derived.
  • Ground-root branch continuity is threaded explicitly through a seed argument instead of solver-side continuation state. The previously accepted ground-normal angle is passed forward, and with no seed the closure recovers one from the contact centers already stored in the state; the earlier per-geometry root cache inside the derived-point graph is gone. Identical inputs and an identical seed always reproduce the same root.
  • Anti-dive, anti-lift, and anti-squat are resolved consistently in the longitudinal–ground-normal plane: CG height is the perpendicular distance to the shared axle ground plane and the reaction-line rise is measured along the ground normal, while the run remains chassis X, which lies exactly in the X-extruded plane. On flat ground this reduces bit-for-bit to the previous chassis-Z formulation. The side-view swing-arm metrics remain chassis-frame descriptive constructions and are documented as such.
  • Axle ride-height change compares current and design ground height at chassis centerline, avoiding contamination from bank angle, asymmetric track, tire radii, or lateral track migration.
  • Axle roll is ISO 8855 suspension roll angle (§5.2.5), calculated from the current line joining the wheel centers with positive rotation about vehicle X. track is the ISO §4.4 rest dimension on horizontal ground. track_change is the generic current road-lateral contact-center separation minus that design value; it is not called ride track change because ISO §8.1.1 reserves that term for symmetric wheel-to-body displacement.
  • The public rigid-disc support point is named wheel_contact_center, matching ISO 8855 §4.1.4 and distinguishing it from the deformable tire contact patch.
  • RoadPlane rejects normals without a resolvable positive upward component. Static visualization tests contact centers against the reconstructed road plane rather than assuming chassis Z = 0.
  • Geometry inputs now reject non-millimeter unit declarations. Length normalization is not implemented, so accepting another label would silently mis-scale tire, ground, tolerance, and metric calculations.
  • Residual and Jacobian evaluation updates only the derived-point chains that the active constraints and step targets actually require, which keeps the coupled axle ground derivation out of the least-squares hot loop. The complete derived set is still recomputed once a step is accepted.

Breaking changes

  • Renamed the geometric tire-road support point everywhere it is exposed. Released PointID.CONTACT_PATCH_CENTER and the interim ground-plane branch’s PointID.WHEEL_GROUND_TANGENT become PointID.WHEEL_CONTACT_CENTER. Likewise, ElementType.CONTACT_PATCH and interim ElementType.WHEEL_GROUND_TANGENT become ElementType.WHEEL_CONTACT_CENTER; the WheelElement and WheelReferences fields become wheel_contact_center. Exported point columns change from released contact_patch_center_x/y/z or interim wheel_ground_tangent_x/y/z to wheel_contact_center_x/y/z, so anything reading those names from CSV, Parquet, or the structured payload must be updated.
  • The released derived-point function get_contact_patch_center and interim get_wheel_ground_tangent are replaced by get_wheel_contact_center in kinematics.core.points.derived.ground alongside the coupled axle ground derivation.
  • Removed the get_wheel_plane_down_vector derived-point helper. The ground support-point construction in kinematics.core.points.derived.ground projects the ground normal into the wheel plane directly, so the separate flat-ground down-vector helper no longer had a caller.
  • The wheel contact center point is a derived output that cannot be driven, so sweeps that target it are rejected during validation. On an axle the point comes from a coupled derivation across both corners, with a bounded validity domain and non-unique roots, so supporting it as an actuator would require an explicit branch policy.
  • The static geometry check reports raw wheel-contact-center chassis Z and signed distance to the reconstructed road plane. The released contact_patch_z/contact_patch_on_ground and interim wheel_ground_tangent_z/wheel_ground_tangent_on_ground result fields are replaced by wheel_contact_center_z, wheel_contact_center_road_distance_mm, and wheel_contact_centers_on_road.
  • Steering-axis ground metrics now use their ISO meanings. steering_axis_offset_ground replaces the signed lateral quantity previously reported as scrub_radius; scrub_radius is now the total unsigned ground distance; and mechanical_trail is projected along the wheel-relative tire longitudinal axis rather than chassis X.

[0.4.1] - 2026-07-19

Added

  • Added Suspension Explorer wordmarks for light and dark GitHub themes.

Changed

  • Renamed the project from open-kinematics to Suspension Explorer and rewrote the README around the supported architectures, mechanisms, workflows, outputs, and current limitations.

[0.4.0] - 2026-07-18

Added

  • Generic point references support ordinary corner points and side-qualified axle points throughout the constraint, state, solver, and derived-point systems.
  • Validated geometry schemas and a shared registry select explicit double-wishbone corner, coilover, pushrod-rocker, axle, and shared anti-roll-bar topologies.
  • Declarative derivative metrics use analytical solution-manifold tangents and forward-mode automatic differentiation for arbitrary scalar responses and drivers.
  • Advisory sweep diagnostics report convergence, residual acceptance, branch continuity, derivative availability, rocker and anti-roll-bar chirality, and transmission margin.
  • Coupled axle models solve left and right corners together and support either mirrored or independently authored geometry.
  • The public analyze_sweep() and initial_pose() APIs return structured positions, metrics, locations, metadata, renderer-neutral element paths, diagnostics, references, and solved frames.

Changed

  • Split the package into a transport-independent kinematics.core solver API and a kinematics.cli adapter for YAML files, result writing, terminal behavior, and optional visualization. CLI-only dependencies now live in the cli extra.
  • Core suspension models now compose explicit rigid-link, variable-link, rack, upright, torsion, rocker, and wheel elements in a validated SuspensionAssembly. Its identifier-only PointCatalog relates elements to fixed, free, and derived points without duplicating solver state. Matplotlib styles are owned by the CLI visualization adapter.
  • Sweep analysis can consume existing solved states, so CLI animation reuses the primary solve instead of solving the same sweep twice.
  • File-based sweeps use a lean evaluated result containing solved states, metrics, solver statistics, and diagnostics. Rich presentation analysis is built only for consumers that request it.
  • The root kinematics package is no longer a second API facade. Public solver workflows and value types are imported from their defining kinematics.core modules, and transport flattening helpers live in kinematics.core.export.
  • Sweep target definitions and target-direction resolution now share the canonical kinematics.core.targeting module. Presentation-model helpers live in kinematics.core.presentation.
  • Optional diagnostic, derivative, and setup-reference failures are exposed as structured advisory warnings instead of disappearing silently.
  • Diagnostic categories and severities use typed string enums across the core and CLI boundary.
  • Physical elements and assemblies own renderer-neutral path topology. CLI plots and animations add Matplotlib styling client-side, and structured analysis exposes the same unstyled paths for external clients. Static visualization supports both corner and axle assemblies and checks every wheel contact patch.
  • Core-only CI now exercises numerical, constraint, Jacobian, state, target, derived-point, and rigid-body tests without CLI or visualization dependencies.
  • Derived-point target Jacobians now evaluate only the target’s transitive dependency chain and seed only relevant free points, substantially reducing solve time.
  • Geometry parsing, validation, and construction now pass through the transport-neutral kinematics.core.input facade; filesystem access remains in kinematics.cli.io.
  • Core schemas now decode canonical enum names, coordinate mappings, and coordinate sequences directly. The CLI schema-tree parser was removed, and malformed coordinates report field-located validation errors instead of leaking KeyError.
  • Axle inputs now group data under vehicle_config, axle_config, and hardpoints. The axle configuration includes a required front/rear position, wheel and tire data, steering state, shared mechanisms, and one symmetric actuation and spring selection. Hardpoints contain a mandatory left map plus optional explicit right and shared center maps.
  • Omitting right hardpoints and side-local setup mirrors the complete left geometry and setup. Explicit right hardpoints support asymmetric geometry, while optional right_setup supports a different camber-shim setup.
  • Geometry configuration is separated by ownership: vehicle inputs hold CG, wheelbase, brake bias, and driven axle; axle inputs hold steering, wheel/tire, and axle-position data; corner inputs hold side-local setup such as camber shims.
  • Steering configuration now selects an explicit rack or none actuator. Double-wishbone and MacPherson corners use a rack-driven track rod when steered and a chassis-fixed toe link when non-steered. Selecting none removes rack coupling, presentation, metrics, derivatives, and sweep targeting.
  • Rack-steered sweeps now require exactly one explicit rack target. The shared rack is one actuator coordinate across both axle corners, so rack-displacement derivatives are emitted for both sides regardless of which pickup is targeted.
  • Explicit asymmetric right hardpoints now require explicit right_setup when the left corner contains side-local setup geometry.
  • The camber-shim assembly solve includes rocker rotation when the pushrod is upright-mounted, preserving pushrod length and rotating every rocker-mounted pickup together.
  • Metric identities are lowercase, unit-free snake_case. Units use typed metadata and are written in CSV metadata or Parquet field metadata.
  • Corner locations remain structural in the analysis API and are rendered as _left and _right suffixes only in flat result files.
  • Axle topology metrics now retain typed Side locations until analysis or export, and installed mechanisms contribute only the state metric metadata they emit.
  • Rocker-to-rocker heave links reject design-state pickup separations at or below the geometric tolerance, where their length derivative would be undefined.
  • Steering metrics use roadwheel_angle; the concrete steering input is trackrod_inboard, and wheel-center longitudinal motion is expressed directly as deriv_wheel_center_x_wrt_hub_z.
  • Half-track is exported as the absolute half_track state metric rather than a design-condition delta.

Breaking changes

  • Moved implementation imports under kinematics.core; moved low-level geometry and numerical types under kinematics.core.primitives.
  • Replaced visualization-specific suspension methods and style-bearing core links with physical declarations in core.elements and core.assembly.
  • Workflows and adapters are imported from their defining modules rather than re-exported from kinematics.core.
  • Suspension capabilities now come from the physical assembly instead of matching free-form suspension type strings in visualization code.
  • Suspension type selection accepts only the architecture keys double_wishbone and macpherson.
  • Renamed SweepFile to SweepSpec.
  • Removed units from metric keys and changed flat axle corner columns from side prefixes to side suffixes, for example left_camber_deg to camber_left.
  • Replaced the flat axle configuration and corner blocks with explicit vehicle_config, axle_config, and hardpoints ownership blocks.
  • Replaced the steered boolean with steering: {type: rack | none}. Rack steering retains track-rod point and element identifiers; non-steered corners use distinct toe-link identifiers.
  • AxleMetricRows.corners now uses Side keys instead of serialized side names.

[0.3.1] - 2026-04-09

Changed

  • Expanded the README to cover analytical Jacobians, camber-shim simulation, suspension metrics, and the current sweep workflow, with refreshed plot and animation media.

Fixed

  • Removed the duplicate contact-patch marker from four-view plots and shortened the model point label to “Contact Patch”.

[0.3.0] - 2026-04-09

Added

  • Added a split-body camber-shim assembly solver. It solves the upper ball joint position, camber-block rotation, and upright-body rotation together to satisfy wishbone arc constraints, shim-face closure, normal alignment, and trackrod length preservation.
  • Added ResidualComputer.validate_target_count to enforce a consistent target count across evaluations, with a regression test for Jacobian shape consistency.
  • Added a front-view comparison plot to visualize_camber_shim.py, overlaying design and setup suspensions with distinct colors.
  • Added direct sign and known-value tests for camber_deg, caster_deg, and roadwheel_angle_deg, plus catalog coverage for the trusted corner-metric export set.
  • Added kingpin inclination (kpi_deg), scrub radius (scrub_radius_mm), and mechanical trail (mechanical_trail_mm) metrics.

Changed

  • Relaxed the Vec3 type alias from NDArray[np.float64] to NDArray[np.floating[Any]] so NumPy arithmetic results satisfy the type checker without wrapping. make_vec3 remains at system boundaries but was removed from internal arithmetic call sites.
  • Adopted ISO/SAE wheel offset (ET) convention in get_wheel_center.
  • Positive wheel.offset now places the wheel centerline inboard of the hub face, reducing track as positive ET increases. Configuration documentation and derived-point expectations were updated to match.
  • ResidualComputer now uses a fixed-size residual vector and Jacobian matrix, with the target count validated once per evaluation.
  • Made the residual-computer internals n_vars, jac_buffer, jac_plan, and validate_target_count public, and renamed Jacobian “scatter” operations to “distribute”.
  • Moved the underdetermined-system check out of the per-step loop in solve_suspension_sweep.
  • Simplified DoubleWishboneSuspension._apply_camber_shim docstring.
  • Default corner-metric exports now use roadwheel_angle_deg as the canonical steer column and no longer export duplicate toe_deg or placeholder anti-dive / anti-squat metrics.

Removed

  • Removed the WHEEL_CENTER_ON_GROUND point and get_wheel_center_on_ground derived-point function. The Z = 0 ground-plane assumption was incorrect in a chassis-fixed frame; intersections now use the contact-patch Z through MetricContext.ground_z.

Fixed

  • Scrub radius now projects along the wheel-axle direction in the ground plane instead of global Y, giving correct values for steered or cambered wheels.
  • Scrub radius and mechanical trail now intersect the steering axis at the contact-patch Z rather than Z = 0, giving correct values through bump travel.
  • Clarified get_contact_patch_center as the lowest point on an ideal tire circle in the wheel-center plane.
  • Dashboard plots now show KPI, mechanical trail, and scrub radius instead of swing-arm lengths and FVIC height. The camber plot Y-axis is tuned to [-2.5, -1.5] degrees.