magna-ev-digital-twin
The magna-ev-digital-twin worked example.
Run it from sema/:
sema check examples/magna-ev-digital-twinSEMA_STRICT=1 sema run examples/magna-ev-digital-twinsema assure examples/magna-ev-digital-twin --grade silverSource
Section titled “Source”src/main.sema
Section titled “src/main.sema”"""CircuitFrame Lab automotive digital-twin entrypoint."""
from magna_ev_digital_twin.aero import aero_solvefrom magna_ev_digital_twin.live import serve_livefrom magna_ev_digital_twin.physics import physics_contextfrom magna_ev_digital_twin.vehicle import completeness_report, vehicle_identity
assure silver
args CircuitFrameCli: live: bool = flag("--live") port: int = option("--port", default=8794)
def main() !{net.listen}: identity = vehicle_identity() report = completeness_report("circuitframe") ensure not report.major_assembly_complete and not report.production_complete if CircuitFrameCli.live: serve_live(CircuitFrameCli.port) return context = physics_context() head_on = aero_solve(context.basis, 27.8, 0.0) yawed = aero_solve(context.basis, 27.8, 0.34906585039886593) print( "configuration=" + identity.id + " revision=" + str(identity.revision) + " profile=" + report.profile + " parts=" + str(report.declared_parts) + " instances=" + str(report.declared_instances) + " nets=" + str(report.declared_nets) + " operations=" + str(report.declared_operations) + " major_assembly_complete=" + str(report.major_assembly_complete) + " production_complete=" + str(report.production_complete) ) print( "aero panels=" + str(context.basis.panel_count) + " cd0=" + str(head_on.cd) + " cd20=" + str(yawed.cd) + " cy20=" + str(yawed.cy) + " drag_n=" + str(head_on.drag_n) + " solver_residual=" + str(head_on.solver_residual) + " closure_residual=" + str(head_on.force_closure_residual) ) print( "surrogate calm_rms=" + str(context.calm.fit_rms_cd) + " crosswind_rms=" + str(context.crosswind.fit_rms_cd) + " active_validation=" + str(context.holdout.active_validation_error) + " candidate_validation=" + str(context.holdout.candidate_validation_error) + " mass_kg=" + str(context.parameters.mass_kg) + " izz=" + str(context.parameters.yaw_inertia_kg_m2) + " cg_height_m=" + str(context.parameters.cg_height_m) )src/adaptation.sema
Section titled “src/adaptation.sema”"""Automotive adaptive-aerodynamics lifecycle over Sema standard-library gates."""
from std.adaptive_dynamics import ActivationDecision, ModelDescriptor, ModelLifecycle, RegimeStatus, ResidualDetector, SelectionPolicy, activate_validated, make_detector, update_detector, validate_candidatefrom std.epistemic import Assumption, Evidence, ValidityRegion
assure silver
pub struct AeroAdaptationSummary: active_model_id: str candidate_model_id: str selected_model_id: str lifecycle: str regime: str mode: str validation_status: str detector_score: f64 training_error: f64 validation_error: f64 invariant_violations: int dwell_steps: int hysteresis_margin: f64 activated: bool reason: str evidence_ids: list[str] invariant len(active_model_id) > 0 invariant len(candidate_model_id) > 0 invariant len(selected_model_id) > 0 invariant detector_score >= 0.0 invariant training_error >= 0.0 invariant validation_error >= 0.0 invariant invariant_violations >= 0 invariant dwell_steps >= 0 invariant hysteresis_margin >= 0.0 invariant len(reason) > 0 invariant len(evidence_ids) <= 32
pub def initial_aero_detector() -> ResidualDetector !{}: return make_detector(32, 4, 1.5, 3, 0.01, 0.01)
pub def update_aero_detector(detector: ResidualDetector, residual: f64, observed_at: f64) -> ResidualDetector !{}: return update_detector(detector, residual, observed_at)
pub def regime_label(detector: ResidualDetector) -> str !{}: if detector.status == RegimeStatus.stable: return "stable" if detector.status == RegimeStatus.suspected: return "suspected" if detector.status == RegimeStatus.shifted: return "shifted" return "unknown"
pub def retained_adaptation(detector: ResidualDetector, active_model_id: str) -> AeroAdaptationSummary !{}: mode = "safe_unknown" if detector.status == RegimeStatus.unknown or detector.status == RegimeStatus.suspected else "retain" reason = "residual history is not yet decisive" if mode == "safe_unknown" else "active aerodynamic model remains inside the observed regime" return AeroAdaptationSummary( active_model_id=active_model_id, candidate_model_id="aero-rom-crosswind-v2", selected_model_id=active_model_id, lifecycle="active", regime=regime_label(detector), mode=mode, validation_status="pending" if mode == "safe_unknown" else "not_required", detector_score=detector.score, training_error=0.0, validation_error=0.0, invariant_violations=0, dwell_steps=12, hysteresis_margin=0.01, activated=false, reason=reason, evidence_ids=[], )
pub def evaluate_yaw_candidate(detector: ResidualDetector, step: int, observed_at: f64, speed_mps: f64, sideslip_deg: f64, holdout: dict[str, f64]) -> AeroAdaptationSummary !{}: require detector.status == RegimeStatus.shifted require step >= 1 and observed_at >= 0.0 and speed_mps >= 0.0 evidence_id = "panel-solve-yaw-holdout-v1" policy = SelectionPolicy(min_validation_improvement=0.25, max_validation_error=0.04, max_invariant_violations=0, min_dwell_steps=12, hysteresis_margin=0.01, required_horizon_steps=12) if speed_mps > 55.0 or abs(sideslip_deg) > 35.0: return AeroAdaptationSummary(active_model_id="aero-rom-calm-v1", candidate_model_id="aero-rom-crosswind-v2", selected_model_id="aero-rom-calm-v1", lifecycle="proposed", regime="shifted", mode="safe_unknown", validation_status="blocked_out_of_scope", detector_score=detector.score, training_error=holdout["candidate_training_error"], validation_error=holdout["candidate_validation_error"], invariant_violations=int(holdout["invariant_violations"]), dwell_steps=policy.min_dwell_steps, hysteresis_margin=policy.hysteresis_margin, activated=false, reason="operating point is outside the declared speed and sideslip validity region", evidence_ids=[]) distance_to_boundary = min(55.0 - speed_mps, 35.0 - abs(sideslip_deg)) validity = ValidityRegion(label="speed 0-55 m/s; absolute apparent-wind sideslip <= 35 deg", in_scope=true, distance_to_boundary=distance_to_boundary, checked_at=observed_at) assumption = Assumption(id="bounded-yaw-envelope", statement="Both reduced-order surrogates are fitted to and scored against the live source-panel solve inside the declared sideslip window; the panel solve itself carries an empirical viscous and base-pressure closure, not a governing CFD field", active=true, checked_at=observed_at, evidence_ids=[evidence_id]) active = ModelDescriptor(id="aero-rom-calm-v1", family="vehicle-aero-reduced", version=1, lifecycle=ModelLifecycle.active, parameter_names=["C_d0", "c_2"], parameters=[0.23, 0.48], structure_signature="quadratic-in-sideslip drag fitted over 0-8 deg", assumptions=[assumption], validity=validity, fit_error=holdout["active_training_error"], validation_error=holdout["active_validation_error"], created_at=0.0, activated_step=0) candidate = ModelDescriptor(id="aero-rom-crosswind-v2", family="vehicle-aero-reduced", version=2, lifecycle=ModelLifecycle.proposed, parameter_names=["C_d0", "c_2", "c_4"], parameters=[0.23, 0.90, -2.19], structure_signature="quartic-in-sideslip drag and cubic side force fitted over 0-35 deg", assumptions=[assumption], validity=validity, fit_error=holdout["candidate_training_error"], validation_error=holdout["candidate_validation_error"], created_at=observed_at, activated_step=0) evidence = Evidence(id=evidence_id, source="CircuitFrame source-panel solve swept over sideslip", observed_at=observed_at, reliability=0.72, summary="Training stations at 4 and 8 deg and held-out stations at 22 and 32 deg of apparent-wind sideslip, scored against the live panel solve; this is reduced-model evidence, not governing CFD", provenance=["aero.sema", "physics.sema", evidence_id]) validation = validate_candidate(active, candidate, holdout["candidate_training_error"], holdout["candidate_validation_error"], int(holdout["invariant_violations"]), [evidence], observed_at, policy) decision = activate_validated(active, validation.model, step, step, [evidence], policy) match decision: case ActivationDecision.activated(previous, current, transition): return AeroAdaptationSummary(active_model_id=previous.id, candidate_model_id=candidate.id, selected_model_id=current.id, lifecycle="active", regime="shifted", mode="structural_switch", validation_status="passed_reduced_analytic", detector_score=detector.score, training_error=validation.training_error, validation_error=validation.validation_error, invariant_violations=validation.invariant_violations, dwell_steps=policy.min_dwell_steps, hysteresis_margin=policy.hysteresis_margin, activated=true, reason=transition.reason, evidence_ids=[evidence_id]) case ActivationDecision.blocked(reason, transition): return AeroAdaptationSummary(active_model_id=active.id, candidate_model_id=candidate.id, selected_model_id=active.id, lifecycle="validated_not_active", regime="shifted", mode="safe_unknown", validation_status="passed_reduced_analytic", detector_score=detector.score, training_error=validation.training_error, validation_error=validation.validation_error, invariant_violations=validation.invariant_violations, dwell_steps=policy.min_dwell_steps, hysteresis_margin=policy.hysteresis_margin, activated=false, reason=reason, evidence_ids=[evidence_id])src/aero.sema
Section titled “src/aero.sema”"""Vehicle aerodynamics by a 3D constant-strength source-panel method with an image ground plane.
Method: Hess-Smith constant-strength source panels over a closed lofted body, with every panelmirrored through the road surface (z = 0) so the ground boundary condition is satisfied exactly.The Neumann condition (zero normal velocity on every panel) is closed by two dense linear solves,one per unit freestream direction; the boundary-value problem is linear in the freestream, sosigma(V, beta) = V (cos beta sigma_x + sin beta sigma_y) is exact for every wind speed and yaw andno matrix work is needed per frame.
Honesty: this is NOT a CFD run and no Navier-Stokes equation is integrated. Potential flow aloneyields exactly zero drag and zero side force on a closed body (d'Alembert's paradox), so everydrag and side-force number here comes from the empirical viscous and base-pressure closure layeredon top of the solved pressure field. Each published quantity is tagged `solved` (out of the linearsystem), `correlated` (empirical closure with a cited form), or `numerical` (a discretisationdiagnostic). See `aero_equations()` for the per-quantity provenance."""
import math
assure silver
NOSE_X_M = 2.31TAIL_X_M = -2.44LENGTH_M = 4.751HALF_WIDTH_M = 0.9605HEIGHT_M = 1.624AXLE_X_M = 1.445RIDE_HEIGHT_M = 0.167FRONTAL_AREA_M2 = 2.48SIDE_AREA_M2 = 4.32PLANFORM_AREA_M2 = 7.95WETTED_AREA_M2 = 19.6AIR_DENSITY_KG_M3 = 1.225AIR_VISCOSITY_PA_S = 0.0000181
RING_COUNT = 21AROUND = 12PANEL_COUNT = 240HALF_PANEL_COUNT = 120HALF_AROUND = 6SECTION_EXPONENT = 3.2REGULARISATION_FACTOR = 0.5FOUR_PI = 12.566370614359172
NOSE_APEX_Z_M = 0.46HOOD_Z_M = 0.97HOOD_END_U = 0.28ROOF_START_U = 0.46FASTBACK_START_U = 0.54TAIL_TOP_Z_M = 0.98DIFFUSER_Z_M = 0.50WIDTH_FULL_U = 0.38TAPER_START_U = 0.48TAPER_DEPTH = 0.26NOSE_FULLNESS = 2.2TUMBLEHOME = 0.30SILL_TUCK = 0.20
STRATFORD_LONGITUDINAL = 0.24STRATFORD_CROSSFLOW = 0.60SIDE_LINE_NORMAL_Z = -0.6BASE_PRESSURE_CP = 0.20BASE_PRESSURE_YAW_GAIN = 0.80CROSSFLOW_BASE_CP = -1.00WHEEL_CD = 0.036947WHEEL_CD_YAW_GAIN = 0.60CALIBRATION_SPEED_MPS = 27.8CALIBRATION_TARGET_CD = 0.23STROUHAL = 0.20CP_FLOOR = -4.0
pub struct AeroPanel: index: int station: int ring_index: int center_x: f64 center_y: f64 center_z: f64 normal_x: f64 normal_y: f64 normal_z: f64 area_m2: f64 invariant index >= 0 and index < 4096 invariant station >= 0 and station < 64 invariant ring_index >= 0 and ring_index < 64 invariant area_m2 > 0.0 invariant center_z > 0.0 and center_z < 3.0 invariant center_x >= -3.0 and center_x <= 3.0 invariant abs(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z - 1.0) < 0.000001
pub struct AeroBasis: panels: list[AeroPanel] source_x: list[f64] source_y: list[f64] mode_x: list[f64] mode_y: list[f64] wake_forward: list[f64] wake_forward_cp: list[f64] wake_reverse: list[f64] wake_reverse_cp: list[f64] wake_crossflow: list[f64] wake_crossflow_cp: list[f64] base_face: list[f64] panel_count: int wetted_area_m2: f64 frontal_area_m2: f64 side_area_m2: f64 planform_area_m2: f64 closure_residual_m2: f64 residual_x: f64 residual_y: f64 separation_x_m: f64 calibrated_wheel_cd: f64 calibration_speed_mps: f64 calibration_target_cd: f64 invariant panel_count == PANEL_COUNT invariant len(panels) == PANEL_COUNT invariant len(source_x) == PANEL_COUNT and len(source_y) == PANEL_COUNT invariant len(mode_x) == 3 * PANEL_COUNT and len(mode_y) == 3 * PANEL_COUNT invariant len(wake_forward) == PANEL_COUNT and len(wake_crossflow) == PANEL_COUNT invariant len(base_face) == PANEL_COUNT invariant wetted_area_m2 > 0.0 and frontal_area_m2 > 0.0 invariant closure_residual_m2 >= 0.0 and closure_residual_m2 < 0.000001 invariant residual_x >= 0.0 and residual_x < 0.00000001 invariant residual_y >= 0.0 and residual_y < 0.00000001 invariant separation_x_m >= TAIL_X_M and separation_x_m <= NOSE_X_M invariant calibrated_wheel_cd > 0.0 invariant calibration_speed_mps > 0.0 invariant calibration_target_cd > 0.0
pub struct AeroForces: air_speed_mps: f64 yaw_rad: f64 dynamic_pressure_pa: f64 reynolds_number: f64 cd: f64 cd_pressure: f64 cd_base: f64 cd_friction: f64 cd_wheels: f64 cy: f64 cl: f64 cmz: f64 drag_n: f64 side_n: f64 lift_n: f64 yaw_moment_nm: f64 front_lift_n: f64 rear_lift_n: f64 pressure_drag_n: f64 cp_min: f64 cp_max: f64 base_pressure_coefficient: f64 separation_x_m: f64 wake_width_m: f64 wake_deficit_fraction: f64 wake_shedding_hz: f64 turbulence_intensity: f64 wake_recirculation_length_m: f64 dalembert_residual_cd: f64 solver_residual: f64 force_closure_residual: f64 surface_cp: list[f64] invariant air_speed_mps >= 0.0 invariant dynamic_pressure_pa >= 0.0 invariant reynolds_number >= 0.0 invariant cd_friction > 0.0 invariant cd_wheels > 0.0 invariant abs(cd - (cd_pressure + cd_base + cd_friction + cd_wheels)) < 0.000000001 invariant cp_min >= CP_FLOOR and cp_min <= 1.0 invariant cp_max >= CP_FLOOR and cp_max <= 1.0 invariant cp_min <= cp_max invariant base_pressure_coefficient < 0.0 invariant separation_x_m >= TAIL_X_M and separation_x_m <= NOSE_X_M invariant wake_width_m >= 0.0 invariant wake_deficit_fraction >= 0.0 and wake_deficit_fraction <= 1.0 invariant wake_shedding_hz >= 0.0 invariant turbulence_intensity >= 0.0 and turbulence_intensity <= 1.0 invariant wake_recirculation_length_m > 0.0 invariant solver_residual >= 0.0 invariant force_closure_residual >= 0.0 invariant len(surface_cp) == PANEL_COUNT
def smooth_step(low: f64, high: f64, value: f64): """Cubic Hermite ramp, clamped to [0, 1] outside the interval.""" span = max(0.000001, high - low) u = min(1.0, max(0.0, (value - low) / span)) return u * u * (3.0 - 2.0 * u)
def roof_height(u: f64): """Upper silhouette: low pointed nose, hood, raked windscreen, roof peak, fastback taper.""" return (NOSE_APEX_Z_M + (HOOD_Z_M - NOSE_APEX_Z_M) * smooth_step(0.0, HOOD_END_U, u) + (HEIGHT_M - HOOD_Z_M) * smooth_step(HOOD_END_U, ROOF_START_U, u) - (HEIGHT_M - TAIL_TOP_Z_M) * smooth_step(FASTBACK_START_U, 1.0, u))
def floor_height(u: f64): """Lower silhouette: approach ramp, flat underbody at ride height, rear diffuser.""" return (RIDE_HEIGHT_M + (NOSE_APEX_Z_M - RIDE_HEIGHT_M) * (1.0 - smooth_step(0.0, 0.09, u)) + (DIFFUSER_Z_M - RIDE_HEIGHT_M) * smooth_step(0.86, 1.0, u))
def half_width(u: f64): """Plan-view half width: blunt nose growth to full width, mild fastback taper aft.""" grow = 1.0 - min(1.0, u / WIDTH_FULL_U) front = math.pow(max(0.0, 1.0 - grow * grow), 1.0 / NOSE_FULLNESS) aft = max(0.0, u - TAPER_START_U) / (1.0 - TAPER_START_U) return HALF_WIDTH_M * front * (1.0 - TAPER_DEPTH * math.pow(aft, 2.4))
def station_ring(u: f64, x_m: f64): """One superelliptic cross-section as AROUND flattened x,y,z points, mirror symmetric in y.""" width = half_width(u) low = floor_height(u) high = roof_height(u) middle = 0.5 * (low + high) reach = 0.5 * (high - low) mut points = [] for k in range(0, AROUND): angle = 0.5 * math.pi - 2.0 * math.pi * float(k) / float(AROUND) across = math.cos(angle) upward = math.sin(angle) height_shape = math.pow(abs(upward), 2.0 / SECTION_EXPONENT) width_shape = math.pow(abs(across), 2.0 / SECTION_EXPONENT) if upward >= 0.0: edge = max(0.0, (height_shape - 0.35) / 0.65) narrow = 1.0 - TUMBLEHOME * edge * edge else: edge = max(0.0, (height_shape - 0.30) / 0.70) narrow = 1.0 - SILL_TUCK * edge * edge y = width * narrow * width_shape if across < 0.0: y = 0.0 - y z = middle + reach * height_shape if upward < 0.0: z = middle - reach * height_shape points = points + [x_m, y, z] return points
def panel_mesh(): """Closed lofted body: two degenerate end caps plus RING_COUNT - 3 superelliptic stations.
Panels are ordered index = station * AROUND + ring_index, and ring_index k mirrors to AROUND - 1 - k through the y = 0 plane, which the symmetry-reduced solve relies on. """ span = NOSE_X_M - TAIL_X_M middle_x = 0.5 * (NOSE_X_M + TAIL_X_M) mut rings = [] for r in range(0, RING_COUNT): x = middle_x + 0.5 * span * math.cos(math.pi * float(r) / float(RING_COUNT - 1)) u = (NOSE_X_M - x) / span if r == 0: mut apex = [] for k in range(0, AROUND): apex = apex + [NOSE_X_M, 0.0, NOSE_APEX_Z_M] rings = rings + [apex] elif r == RING_COUNT - 1: centre = 0.5 * (floor_height(1.0) + roof_height(1.0)) mut tail = [] for k in range(0, AROUND): tail = tail + [TAIL_X_M, 0.0, centre] rings = rings + [tail] else: rings = rings + [station_ring(u, x)] mut panels = [] for r in range(0, RING_COUNT - 1): near = rings[r] far = rings[r + 1] for k in range(0, AROUND): j = 3 * k m = 3 * ((k + 1) % AROUND) ax = far[m] - near[j] ay = far[m + 1] - near[j + 1] az = far[m + 2] - near[j + 2] bx = far[j] - near[m] by = far[j + 1] - near[m + 1] bz = far[j + 2] - near[m + 2] vx = 0.5 * (ay * bz - az * by) vy = 0.5 * (az * bx - ax * bz) vz = 0.5 * (ax * by - ay * bx) area = math.sqrt(vx * vx + vy * vy + vz * vz) panels = panels + [AeroPanel( index=r * AROUND + k, station=r, ring_index=k, center_x=0.25 * (near[j] + near[m] + far[m] + far[j]), center_y=0.25 * (near[j + 1] + near[m + 1] + far[m + 1] + far[j + 1]), center_z=0.25 * (near[j + 2] + near[m + 2] + far[m + 2] + far[j + 2]), normal_x=vx / area, normal_y=vy / area, normal_z=vz / area, area_m2=area, )] return panels
def influence_blocks(tx: list[f64], ty: list[f64], tz: list[f64], sx: list[f64], sy: list[f64], sz: list[f64], area: list[f64], epsilon: list[f64]) -> any: """Velocity induced at every target by unit constant sources plus their z = 0 ground images.
Returns the three dense component matrices [Kx, Ky, Kz]; the near field is regularised with r_eps = REGULARISATION_FACTOR * sqrt(area_j) exactly as the browser-side field evaluator does. """ target_ones = [1.0] * len(tx) source_ones = [1.0] * len(sx) equation: tgx := outer(tx, source_ones) tgy := outer(ty, source_ones) tgz := outer(tz, source_ones) srx := outer(target_ones, sx) sry := outer(target_ones, sy) srz := outer(target_ones, sz) strength := outer(target_ones, area) guard := outer(target_ones, epsilon) dx = tgx - srx dy = tgy - sry dz = tgz - srz dzi = tgz + srz flat = dx * dx + dy * dy direct = math.sqrt(flat + dz * dz) image = math.sqrt(flat + dzi * dzi) rd = where(direct > guard, direct, guard) ri = where(image > guard, image, guard) sd = strength / (FOUR_PI * rd * rd * rd) si = strength / (FOUR_PI * ri * ri * ri) return [(sd + si) * dx, (sd + si) * dy, sd * dz + si * dzi]
def normal_projection(blocks: any, nx: list[f64], ny: list[f64], nz: list[f64], columns: int): """Contract the three influence blocks against the target normals into one dense matrix.""" ones = [1.0] * columns equation: gx := outer(nx, ones) gy := outer(ny, ones) gz := outer(nz, ones) return gx * blocks[0] + gy * blocks[1] + gz * blocks[2]
def identity_matrix(n: int): mut rows = [] for i in range(0, n): mut row = [0.0] * n row[i] = 1.0 rows = rows + [row] return tensor(rows)
def dense_solve(matrix: any, rhs: list[f64]): equation: answer := solve(matrix, rhs) mut out = [] for i in range(0, len(rhs)): out = out + [float(answer[i])] return out
def dense_apply(matrix: any, vector: list[f64], rows: int): equation: product := matvec(matrix, vector) mut out = [] for i in range(0, rows): out = out + [float(product[i])] return out
def stratford_limit(recovery: f64, run_m: f64, gradient: f64, constant: f64): """Stratford (1959) turbulent separation: Cbar (xi dCbar/dxi)^1/2 >= k (1e-6 Re_xi)^1/10.
Re_xi is taken at the calibration speed so the separation topology stays geometry locked, which is what road vehicles show above Re_L ~ 4e6 where separation sits on fixed edges. `correlated`. """ run = max(run_m, 0.0001) reynolds = AIR_DENSITY_KG_M3 * CALIBRATION_SPEED_MPS * run / AIR_VISCOSITY_PA_S return recovery * math.sqrt(max(0.0, run * gradient)) >= constant * math.pow(0.000001 * reynolds, 0.1)
def line_walk(panels: list[AeroPanel], cp: list[f64], side_line: list[f64], forward: bool): """March each longitudinal surface line and freeze the pressure downstream of separation.
Returns 2 * PANEL_COUNT values: a 0/1 separation flag per panel followed by the frozen separation pressure coefficient carried into the wake. """ stations = RING_COUNT - 1 mut flag = [0.0] * PANEL_COUNT mut frozen = [0.0] * PANEL_COUNT for k in range(0, AROUND): if side_line[k] < 0.5: continue mut order = [] for s in range(0, stations): if forward: order = order + [s * AROUND + k] else: order = order + [(stations - 1 - s) * AROUND + k] mut lowest = cp[order[0]] mut run = 0.0 mut previous = 0.0 for j in range(1, stations): here = panels[order[j]] back = panels[order[j - 1]] value = cp[order[j]] if value <= lowest: lowest = value run = 0.0 previous = 0.0 continue dx = here.center_x - back.center_x dy = here.center_y - back.center_y dz = here.center_z - back.center_z step = max(0.000001, math.sqrt(dx * dx + dy * dy + dz * dz)) run = run + step recovery = (value - lowest) / (1.0 - lowest) gradient = max(0.0, (recovery - previous) / step) previous = recovery if stratford_limit(recovery, run, gradient, STRATFORD_LONGITUDINAL): wake_cp = cp[order[j - 1]] for m in range(j, stations): flag[order[m]] = 1.0 frozen[order[m]] = wake_cp break return flag + frozen
def ring_walk(panels: list[AeroPanel], cp: list[f64]): """March each cross-section from its crossflow stagnation point and mark the leeward wake.
Crossflow-analogy separation (Allen & Perkins): the pressure field driving it is the unit lateral mode alone, so the topology is yaw independent and can be precomputed once. """ mut flag = [0.0] * PANEL_COUNT mut frozen = [0.0] * PANEL_COUNT for s in range(1, RING_COUNT - 2): mut start = 0 for k in range(1, AROUND): if cp[s * AROUND + k] > cp[s * AROUND + start]: start = k for direction in range(0, 2): step_sign = 1 if direction == 1: step_sign = -1 mut lowest = cp[s * AROUND + start] mut run = 0.0 mut previous = 0.0 mut wake_cp = 0.0 mut separated = false for d in range(1, AROUND): k = (start + step_sign * d + 2 * AROUND) % AROUND index = s * AROUND + k if separated: flag[index] = 1.0 frozen[index] = wake_cp continue previous_k = (k - step_sign + 2 * AROUND) % AROUND here = panels[index] back = panels[s * AROUND + previous_k] value = cp[index] if value <= lowest: lowest = value run = 0.0 previous = 0.0 continue dx = here.center_x - back.center_x dy = here.center_y - back.center_y dz = here.center_z - back.center_z step = max(0.000001, math.sqrt(dx * dx + dy * dy + dz * dz)) run = run + step recovery = (value - lowest) / (1.0 - lowest) gradient = max(0.0, (recovery - previous) / step) previous = recovery if stratford_limit(recovery, run, gradient, STRATFORD_CROSSFLOW): separated = true wake_cp = min(cp[s * AROUND + previous_k], CROSSFLOW_BASE_CP) flag[index] = 1.0 frozen[index] = wake_cp for s in range(0, RING_COUNT - 1): for k in range(0, HALF_AROUND): a = s * AROUND + k b = s * AROUND + (AROUND - 1 - k) if flag[a] > 0.5 or flag[b] > 0.5: mut wake = frozen[a] if flag[a] < 0.5: wake = frozen[b] elif flag[b] > 0.5: wake = min(frozen[a], frozen[b]) flag[a] = 1.0 flag[b] = 1.0 frozen[a] = wake frozen[b] = wake return flag + frozen
pub def aero_basis() -> AeroBasis !{}: """Build the panel mesh, run both dense source solves once, and freeze the wake topology.""" panels = panel_mesh() half = HALF_PANEL_COUNT mut hx = [] mut hy = [] mut hz = [] mut hnx = [] mut hny = [] mut hnz = [] mut harea = [] mut heps = [] mut mx = [] mut my = [] mut mz = [] mut marea = [] mut meps = [] mut ax = [] mut ay = [] mut az = [] mut anx = [] mut any_y = [] mut anz = [] mut aarea = [] mut aeps = [] for i in range(0, PANEL_COUNT): p = panels[i] radius = REGULARISATION_FACTOR * math.sqrt(p.area_m2) ax = ax + [p.center_x] ay = ay + [p.center_y] az = az + [p.center_z] anx = anx + [p.normal_x] any_y = any_y + [p.normal_y] anz = anz + [p.normal_z] aarea = aarea + [p.area_m2] aeps = aeps + [radius] if p.ring_index < HALF_AROUND: hx = hx + [p.center_x] hy = hy + [p.center_y] hz = hz + [p.center_z] hnx = hnx + [p.normal_x] hny = hny + [p.normal_y] hnz = hnz + [p.normal_z] harea = harea + [p.area_m2] heps = heps + [radius] mx = mx + [p.center_x] my = my + [0.0 - p.center_y] mz = mz + [p.center_z] marea = marea + [p.area_m2] meps = meps + [radius]
near = influence_blocks(hx, hy, hz, hx, hy, hz, harea, heps) across = influence_blocks(hx, hy, hz, mx, my, mz, marea, meps) plus = [near[0] + across[0], near[1] + across[1], near[2] + across[2]] minus = [near[0] - across[0], near[1] - across[1], near[2] - across[2]] unit = identity_matrix(half) * 0.5 symmetric = normal_projection(plus, hnx, hny, hnz, half) + unit antisymmetric = normal_projection(minus, hnx, hny, hnz, half) + unit mut rhs_x = [] mut rhs_y = [] for h in range(0, half): rhs_x = rhs_x + [0.0 - hnx[h]] rhs_y = rhs_y + [0.0 - hny[h]] half_sx = dense_solve(symmetric, rhs_x) half_sy = dense_solve(antisymmetric, rhs_y)
plus_x = dense_apply(plus[0], half_sx, half) plus_y = dense_apply(plus[1], half_sx, half) plus_z = dense_apply(plus[2], half_sx, half) minus_x = dense_apply(minus[0], half_sy, half) minus_y = dense_apply(minus[1], half_sy, half) minus_z = dense_apply(minus[2], half_sy, half)
mut source_x = [0.0] * PANEL_COUNT mut source_y = [0.0] * PANEL_COUNT mut mode_x = [0.0] * (3 * PANEL_COUNT) mut mode_y = [0.0] * (3 * PANEL_COUNT) for h in range(0, half): station = h // HALF_AROUND k = h % HALF_AROUND i = station * AROUND + k mirror = station * AROUND + (AROUND - 1 - k) source_x[i] = half_sx[h] source_x[mirror] = half_sx[h] source_y[i] = half_sy[h] source_y[mirror] = 0.0 - half_sy[h] vxx = 1.0 + 0.5 * half_sx[h] * hnx[h] + plus_x[h] vxy = 0.5 * half_sx[h] * hny[h] + plus_y[h] vxz = 0.5 * half_sx[h] * hnz[h] + plus_z[h] vyx = 0.5 * half_sy[h] * hnx[h] + minus_x[h] vyy = 1.0 + 0.5 * half_sy[h] * hny[h] + minus_y[h] vyz = 0.5 * half_sy[h] * hnz[h] + minus_z[h] mode_x[3 * i] = vxx mode_x[3 * i + 1] = vxy mode_x[3 * i + 2] = vxz mode_x[3 * mirror] = vxx mode_x[3 * mirror + 1] = 0.0 - vxy mode_x[3 * mirror + 2] = vxz mode_y[3 * i] = vyx mode_y[3 * i + 1] = vyy mode_y[3 * i + 2] = vyz mode_y[3 * mirror] = 0.0 - vyx mode_y[3 * mirror + 1] = vyy mode_y[3 * mirror + 2] = 0.0 - vyz
full = influence_blocks(ax, ay, az, ax, ay, az, aarea, aeps) operator = normal_projection(full, anx, any_y, anz, PANEL_COUNT) + identity_matrix(PANEL_COUNT) * 0.5 check_x = dense_apply(operator, source_x, PANEL_COUNT) check_y = dense_apply(operator, source_y, PANEL_COUNT) mut residual_x = 0.0 mut residual_y = 0.0 mut wetted = 0.0 mut frontal = 0.0 mut lateral = 0.0 mut planform = 0.0 mut sum_ax = 0.0 mut sum_ay = 0.0 mut sum_az = 0.0 mut cp_axial = [0.0] * PANEL_COUNT mut cp_crossflow = [0.0] * PANEL_COUNT mut base_face = [0.0] * PANEL_COUNT for i in range(0, PANEL_COUNT): p = panels[i] residual_x = max(residual_x, abs(check_x[i] + p.normal_x)) residual_y = max(residual_y, abs(check_y[i] + p.normal_y)) wetted = wetted + p.area_m2 frontal = frontal + p.area_m2 * max(0.0, p.normal_x) lateral = lateral + p.area_m2 * max(0.0, p.normal_y) planform = planform + p.area_m2 * max(0.0, p.normal_z) sum_ax = sum_ax + p.area_m2 * p.normal_x sum_ay = sum_ay + p.area_m2 * p.normal_y sum_az = sum_az + p.area_m2 * p.normal_z vx = mode_x[3 * i] vy = mode_x[3 * i + 1] vz = mode_x[3 * i + 2] vn = vx * p.normal_x + vy * p.normal_y + vz * p.normal_z tx = vx - vn * p.normal_x ty = vy - vn * p.normal_y tz = vz - vn * p.normal_z cp_axial[i] = max(CP_FLOOR, min(1.0, 1.0 - (tx * tx + ty * ty + tz * tz))) wy = mode_y[3 * i + 1] wz = mode_y[3 * i + 2] cp_crossflow[i] = max(CP_FLOOR, min(1.0, 1.0 - (wy * wy + wz * wz))) if p.station == 0 or p.station == RING_COUNT - 2: base_face[i] = 1.0
mut side_line = [0.0] * AROUND for k in range(0, AROUND): mut mean_nz = 0.0 for s in range(0, RING_COUNT - 1): mean_nz = mean_nz + panels[s * AROUND + k].normal_z if mean_nz / float(RING_COUNT - 1) > SIDE_LINE_NORMAL_Z: side_line[k] = 1.0
forward = line_walk(panels, cp_axial, side_line, true) reverse = line_walk(panels, cp_axial, side_line, false) crossflow = ring_walk(panels, cp_crossflow) mut wake_forward = [0.0] * PANEL_COUNT mut wake_forward_cp = [0.0] * PANEL_COUNT mut wake_reverse = [0.0] * PANEL_COUNT mut wake_reverse_cp = [0.0] * PANEL_COUNT mut wake_crossflow = [0.0] * PANEL_COUNT mut wake_crossflow_cp = [0.0] * PANEL_COUNT mut separated_area = 0.0 mut separated_moment = 0.0 for i in range(0, PANEL_COUNT): p = panels[i] wake_forward[i] = forward[i] wake_forward_cp[i] = forward[PANEL_COUNT + i] wake_reverse[i] = reverse[i] wake_reverse_cp[i] = reverse[PANEL_COUNT + i] wake_crossflow[i] = crossflow[i] wake_crossflow_cp[i] = crossflow[PANEL_COUNT + i] if p.station == RING_COUNT - 2: wake_forward[i] = 1.0 wake_forward_cp[i] = 1.0 if p.station == 0: wake_reverse[i] = 1.0 wake_reverse_cp[i] = 1.0 if wake_forward[i] > 0.5: separated_area = separated_area + p.area_m2 separated_moment = separated_moment + p.area_m2 * p.center_x
return AeroBasis( panels=panels, source_x=source_x, source_y=source_y, mode_x=mode_x, mode_y=mode_y, wake_forward=wake_forward, wake_forward_cp=wake_forward_cp, wake_reverse=wake_reverse, wake_reverse_cp=wake_reverse_cp, wake_crossflow=wake_crossflow, wake_crossflow_cp=wake_crossflow_cp, base_face=base_face, panel_count=PANEL_COUNT, wetted_area_m2=wetted, frontal_area_m2=frontal, side_area_m2=lateral, planform_area_m2=planform, closure_residual_m2=math.sqrt(sum_ax * sum_ax + sum_ay * sum_ay + sum_az * sum_az), residual_x=residual_x, residual_y=residual_y, separation_x_m=separated_moment / separated_area, calibrated_wheel_cd=WHEEL_CD, calibration_speed_mps=CALIBRATION_SPEED_MPS, calibration_target_cd=CALIBRATION_TARGET_CD, )
pub def aero_solve(basis: AeroBasis, air_speed_mps: f64, yaw_rad: f64) -> AeroForces !{}: """Integrate the surface pressure for one wind speed and yaw. O(panels), no matrix work.
The apparent wind in the vehicle frame is U = -V (cos beta, sin beta, 0): air arrives at the nose and leaves past the tail at zero yaw, and beta > 0 puts the wind on the vehicle's left. The solved Cp field is invariant under freestream reversal, so it is evaluated straight from the exported +x and +y modes; only the viscous closure needs the physical flow direction. """ speed = max(0.0, air_speed_mps) cb = math.cos(yaw_rad) sb = math.sin(yaw_rad) q = 0.5 * AIR_DENSITY_KG_M3 * speed * speed base_cp = 0.0 - (BASE_PRESSURE_CP + BASE_PRESSURE_YAW_GAIN * abs(sb)) weight_forward = max(0.0, cb) weight_reverse = max(0.0, 0.0 - cb) weight_cross = abs(sb) lee = 0.0 if sb > 0.0: lee = 1.0 if sb < 0.0: lee = -1.0 across_x = 0.0 - sb across_y = cb
panels = basis.panels mode_x = basis.mode_x mode_y = basis.mode_y forward_flag = basis.wake_forward forward_cp = basis.wake_forward_cp reverse_flag = basis.wake_reverse reverse_cp = basis.wake_reverse_cp cross_flag = basis.wake_crossflow cross_cp = basis.wake_crossflow_cp base_flag = basis.base_face mut surface_cp = [0.0] * basis.panel_count mut potential_x = 0.0 mut potential_y = 0.0 mut potential_z = 0.0 mut face_x = 0.0 mut face_y = 0.0 mut face_z = 0.0 mut shear_x = 0.0 mut shear_y = 0.0 mut shear_z = 0.0 mut closure_x = 0.0 mut lift_moment = 0.0 mut yaw_moment = 0.0 mut cp_min = 1.0 mut cp_max = CP_FLOOR mut wake_area = 0.0 mut wake_moment = 0.0 mut wake_low = 1000.0 mut wake_high = -1000.0 mut wake_floor = 1000.0 mut wake_ceiling = -1000.0 for i in range(0, basis.panel_count): p = panels[i] nx = p.normal_x ny = p.normal_y nz = p.normal_z px = p.center_x py = p.center_y b = 3 * i ux = cb * mode_x[b] + sb * mode_y[b] uy = cb * mode_x[b + 1] + sb * mode_y[b + 1] uz = cb * mode_x[b + 2] + sb * mode_y[b + 2] un = ux * nx + uy * ny + uz * nz tx = ux - un * nx ty = uy - un * ny tz = uz - un * nz potential = max(CP_FLOOR, min(1.0, 1.0 - (tx * tx + ty * ty + tz * tz))) mut cp = potential mut active = 0.0 if forward_flag[i] > 0.5 and weight_forward > 0.0: cp = cp + weight_forward * (min(forward_cp[i], base_cp) - cp) active = weight_forward if reverse_flag[i] > 0.5 and weight_reverse > 0.0: cp = cp + weight_reverse * (min(reverse_cp[i], base_cp) - cp) active = max(active, weight_reverse) if cross_flag[i] > 0.5 and lee * py < 0.0: cp = cp + weight_cross * (cross_cp[i] - cp) active = max(active, weight_cross) cp = max(CP_FLOOR, min(1.0, cp)) surface_cp[i] = cp cp_min = min(cp_min, cp) cp_max = max(cp_max, cp) wx = 0.0 - p.area_m2 * nx wy = 0.0 - p.area_m2 * ny wz = 0.0 - p.area_m2 * nz potential_x = potential_x + potential * wx potential_y = potential_y + potential * wy potential_z = potential_z + potential * wz delta = cp - potential dx = delta * wx dy = delta * wy dz = delta * wz closure_x = closure_x + dx if base_flag[i] > 0.5: face_x = face_x + dx face_y = face_y + dy face_z = face_z + dz else: shear_x = shear_x + dx shear_y = shear_y + dy shear_z = shear_z + dz lift_moment = lift_moment + cp * wz * px yaw_moment = yaw_moment + cp * (px * wy - py * wx) if active > 0.0: reach = across_x * px + across_y * py weighted = p.area_m2 * active wake_area = wake_area + weighted wake_moment = wake_moment + weighted * px wake_low = min(wake_low, reach) wake_high = max(wake_high, reach) wake_floor = min(wake_floor, p.center_z) wake_ceiling = max(wake_ceiling, p.center_z)
cd_pressure = 0.0 - shear_x / FRONTAL_AREA_M2 cd_base = 0.0 - face_x / FRONTAL_AREA_M2 reynolds = AIR_DENSITY_KG_M3 * speed * LENGTH_M / AIR_VISCOSITY_PA_S effective = max(reynolds, AIR_DENSITY_KG_M3 * 1.0 * LENGTH_M / AIR_VISCOSITY_PA_S) skin_friction = 0.455 / math.pow(math.log(effective) / math.log(10.0), 2.58) cd_friction = skin_friction * WETTED_AREA_M2 / FRONTAL_AREA_M2 cd_wheels = WHEEL_CD * (1.0 + WHEEL_CD_YAW_GAIN * abs(sb)) cd = cd_pressure + cd_base + cd_friction + cd_wheels cy = 0.0 - (face_y + shear_y) / SIDE_AREA_M2 cl = (potential_z + face_z + shear_z) / PLANFORM_AREA_M2 cmz = 0.0 - yaw_moment / (SIDE_AREA_M2 * LENGTH_M)
lift_unit = potential_z + face_z + shear_z front_unit = (lift_moment + lift_unit * AXLE_X_M) / (2.0 * AXLE_X_M) drag_n = cd * q * FRONTAL_AREA_M2 pressure_drag_n = 0.0 - closure_x * q recomposed = pressure_drag_n + (cd_friction + cd_wheels) * q * FRONTAL_AREA_M2 closure_residual = abs(drag_n - recomposed) / max(1.0, abs(drag_n))
separation_x = basis.separation_x_m if wake_area > 0.0: separation_x = wake_moment / wake_area width = max(0.0, wake_high - wake_low) frontal_wake = max(0.05, width * max(0.05, wake_ceiling - wake_floor)) deficit = min(1.0, max(0.0, (cd_pressure + cd_base) * FRONTAL_AREA_M2 / (2.0 * frontal_wake))) recirculation = HEIGHT_M * max(1.2, min(2.5, 2.5 - 1.3 * min(1.0, abs(base_cp) / 0.5)))
return AeroForces( air_speed_mps=speed, yaw_rad=yaw_rad, dynamic_pressure_pa=q, reynolds_number=reynolds, cd=cd, cd_pressure=cd_pressure, cd_base=cd_base, cd_friction=cd_friction, cd_wheels=cd_wheels, cy=cy, cl=cl, cmz=cmz, drag_n=drag_n, side_n=cy * q * SIDE_AREA_M2, lift_n=lift_unit * q, yaw_moment_nm=cmz * q * SIDE_AREA_M2 * LENGTH_M, front_lift_n=front_unit * q, rear_lift_n=(lift_unit - front_unit) * q, pressure_drag_n=pressure_drag_n, cp_min=cp_min, cp_max=cp_max, base_pressure_coefficient=base_cp, separation_x_m=separation_x, wake_width_m=width, wake_deficit_fraction=deficit, wake_shedding_hz=STROUHAL * speed / HEIGHT_M, turbulence_intensity=min(1.0, max(0.0, 0.14 + 0.45 * deficit)), wake_recirculation_length_m=recirculation, dalembert_residual_cd=0.0 - potential_x / FRONTAL_AREA_M2, solver_residual=max(basis.residual_x, basis.residual_y), force_closure_residual=closure_residual, surface_cp=surface_cp, )
def rounded(value: f64): """Half-up rounding to five decimals so the exported panel payload stays compact.""" if value < 0.0: return 0.0 - float(int(0.0 - value * 100000.0 + 0.5)) / 100000.0 return float(int(value * 100000.0 + 0.5)) / 100000.0
pub def aero_basis_export(basis: AeroBasis) -> dict[str, any] !{}: """`sema.circuitframe-aero-basis/v1`: geometry, source strengths and the solver provenance.""" mut rows = [] for i in range(0, basis.panel_count): p = basis.panels[i] rows = rows + [{ "i": p.index, "c": [rounded(p.center_x), rounded(p.center_y), rounded(p.center_z)], "n": [rounded(p.normal_x), rounded(p.normal_y), rounded(p.normal_z)], "a": rounded(p.area_m2), "sx": rounded(basis.source_x[i]), "sy": rounded(basis.source_y[i]), }] calibration = aero_solve(basis, basis.calibration_speed_mps, 0.0) return { "schema": "sema.circuitframe-aero-basis/v1", "method": "constant-source-panel + image ground plane", "panel_count": basis.panel_count, "reference": { "frontal_area_m2": FRONTAL_AREA_M2, "planform_area_m2": PLANFORM_AREA_M2, "side_area_m2": SIDE_AREA_M2, "wetted_area_m2": WETTED_AREA_M2, "length_m": LENGTH_M, "width_m": 2.0 * HALF_WIDTH_M, "height_m": HEIGHT_M, "wheelbase_m": 2.0 * AXLE_X_M, "ride_height_m": RIDE_HEIGHT_M, }, "mesh": { "wetted_area_m2": rounded(basis.wetted_area_m2), "frontal_area_m2": rounded(basis.frontal_area_m2), "side_area_m2": rounded(basis.side_area_m2), "planform_area_m2": rounded(basis.planform_area_m2), "closure_residual_m2": basis.closure_residual_m2, "stations": RING_COUNT - 1, "around": AROUND, }, "panels": rows, "solver": { "residual_max": max(basis.residual_x, basis.residual_y), "modes": 2, "ground_image": true, "regularisation": "r_eps = 0.5 sqrt(a_i)", "reduction": "mirror-symmetry split: two 120x120 dense solves span the 240-panel system", "freestream_convention": "U_inf = V (cos yaw, sin yaw, 0); render -u for nose-to-tail smoke", "render_sign": -1.0, }, "calibration": { "cd_pressure": calibration.cd_pressure, "cd_friction": calibration.cd_friction, "cd_base": calibration.cd_base, "cd_wheels": calibration.cd_wheels, "cd_total": calibration.cd, "speed_mps": basis.calibration_speed_mps, "target_cd": basis.calibration_target_cd, "free_parameter": "wheel and underbody increment", "wheel_increment_zero_yaw": basis.calibrated_wheel_cd, }, }
pub def aero_equations() -> list[dict[str, str]] !{}: """Equation manifest rows; every owner states solved, correlated or numerical provenance.""" return [ {"id": "panel-neumann", "source": "aero_basis", "expression": "sum_j A_ij sigma_j = -n_i . e_inf, A_ii = 1/2", "unit": "m/s", "owner": "Sema source-panel solve (solved)"}, {"id": "panel-influence", "source": "influence_blocks", "expression": "A_ij = a_j n_i . (c_i - c_j) / (4 pi max(r, r_eps)^3) + ground image", "unit": "1/s", "owner": "Sema source-panel solve (solved)"}, {"id": "panel-ground-image", "source": "influence_blocks", "expression": "every source mirrored through z = 0 so u_z(z=0) = 0 exactly", "unit": "1", "owner": "Sema source-panel solve (solved)"}, {"id": "panel-superposition", "source": "aero_solve", "expression": "sigma(V, beta) = V (cos beta sigma_x + sin beta sigma_y)", "unit": "m/s", "owner": "Sema source-panel solve (solved, exact in the freestream)"}, {"id": "surface-pressure", "source": "aero_solve", "expression": "Cp_i = 1 - |v_i|^2 / V^2, v_i tangential", "unit": "1", "owner": "Sema source-panel solve (solved)"}, {"id": "pressure-integral", "source": "aero_solve", "expression": "F = -q sum_i Cp_i a_i n_i, q = 1/2 rho V^2", "unit": "N", "owner": "Sema surface integration (solved)"}, {"id": "dalembert", "source": "aero_solve", "expression": "closed-body potential flow gives F_x = F_y = 0; the residual is discretisation error", "unit": "1", "owner": "Sema solver diagnostic (numerical)"}, {"id": "stratford-separation", "source": "line_walk", "expression": "Cbar (xi dCbar/dxi)^1/2 >= k (1e-6 Re_xi)^1/10", "unit": "1", "owner": "Stratford 1959 turbulent recovery (correlated)"}, {"id": "crossflow-separation", "source": "ring_walk", "expression": "same criterion walked around each section on the unit crossflow mode", "unit": "1", "owner": "Allen-Perkins crossflow analogy (correlated)"}, {"id": "base-pressure", "source": "aero_solve", "expression": "Cp_b = -(0.20 + 0.80 |sin beta|) held over the separated wake", "unit": "1", "owner": "Sema bluff-body base closure (correlated)"}, {"id": "free-streamline", "source": "aero_solve", "expression": "separated panels hold min(Cp at separation, Cp_b)", "unit": "1", "owner": "Kirchhoff constant-pressure wake (correlated)"}, {"id": "skin-friction", "source": "aero_solve", "expression": "C_f = 0.455 / (log10 Re_L)^2.58, cd_f = C_f A_wet / A_front", "unit": "1", "owner": "Schlichting turbulent flat plate (correlated)"}, {"id": "wheel-increment", "source": "aero_solve", "expression": "cd_w = 0.036947 (1 + 0.60 |sin beta|)", "unit": "1", "owner": "Sema wheel and underbody increment (correlated, calibrated)"}, {"id": "drag-buildup", "source": "aero_solve", "expression": "C_D = cd_pressure + cd_base + cd_friction + cd_wheels", "unit": "1", "owner": "Sema drag build-up (solved + correlated)"}, {"id": "axle-lift-split", "source": "aero_solve", "expression": "F_zf = (M_y + F_z a) / 2a about the mid-wheelbase origin", "unit": "N", "owner": "Sema surface integration (solved + correlated)"}, {"id": "yaw-moment", "source": "aero_solve", "expression": "M_z = sum_i (x_i dF_y,i - y_i dF_x,i), C_Mz = -M_z / (q A_side L)", "unit": "N.m", "owner": "Sema surface integration (solved + correlated)"}, {"id": "wake-shedding", "source": "aero_solve", "expression": "f_s = St V / H, St = 0.20", "unit": "Hz", "owner": "Sema bluff-body wake scalar (correlated)"}, {"id": "wake-deficit", "source": "aero_solve", "expression": "delta_u = C_D,pressure A_front / (2 A_wake), bounded to [0, 1]", "unit": "fraction", "owner": "Sema momentum-deficit estimate (correlated)"}, {"id": "wake-recirculation", "source": "aero_solve", "expression": "L_r = H (2.5 - 1.3 min(1, |Cp_b|/0.5)) bounded to 1.2..2.5 H", "unit": "m", "owner": "Sema bluff-body wake scalar (correlated)"}, {"id": "separation-front", "source": "aero_solve", "expression": "x_sep = area-weighted centroid of the panels whose wake closure is active", "unit": "m", "owner": "Sema wake geometry (solved topology, correlated weighting)"}, {"id": "force-closure", "source": "aero_solve", "expression": "|C_D q A - (F_pressure + (cd_f + cd_w) q A)| / max(1, |C_D q A|)", "unit": "1", "owner": "Sema bookkeeping check (numerical)"}, ]
def yaw_forces(basis: AeroBasis, degrees: f64): """Evaluate one yaw station at the calibration wind speed. Used by the tests below.""" return aero_solve(basis, CALIBRATION_SPEED_MPS, degrees * math.pi / 180.0)
test "panel mesh closes and matches the declared reference areas": basis = aero_basis() ensure basis.panel_count == 240 ensure len(basis.panels) == 240 ensure basis.closure_residual_m2 < 0.000000001 ensure abs(basis.wetted_area_m2 / WETTED_AREA_M2 - 1.0) < 0.15 ensure abs(basis.frontal_area_m2 - FRONTAL_AREA_M2) < 0.15 ensure basis.separation_x_m < 0.0
test "both dense source solves satisfy the Neumann condition on every panel": basis = aero_basis() ensure basis.residual_x < 0.00000001 ensure basis.residual_y < 0.00000001 mut worst = 0.0 for i in range(0, basis.panel_count): p = basis.panels[i] vn = (basis.mode_x[3 * i] * p.normal_x + basis.mode_x[3 * i + 1] * p.normal_y + basis.mode_x[3 * i + 2] * p.normal_z) worst = max(worst, abs(vn)) ensure worst < 0.00000001
test "zero yaw is symmetric and lands on the calibrated drag coefficient": basis = aero_basis() forces = yaw_forces(basis, 0.0) ensure abs(forces.cd - 0.23) < 0.005 ensure abs(forces.cy) < 0.000001 * forces.cd ensure abs(forces.cmz) < 0.000001 * forces.cd ensure abs(forces.side_n) < 0.000001 * forces.drag_n ensure abs(forces.yaw_moment_nm) < 0.000001 * forces.drag_n
test "yaw sweep raises drag and grows side force nearly linearly": basis = aero_basis() calm = yaw_forces(basis, 0.0) five = yaw_forces(basis, 5.0) ten = yaw_forces(basis, 10.0) fifteen = yaw_forces(basis, 15.0) twenty = yaw_forces(basis, 20.0) ensure fifteen.cd > calm.cd ensure twenty.cd > fifteen.cd ensure ten.cy > 0.0 ensure abs(ten.cy / five.cy - 2.0) < 0.5 ensure yaw_forces(basis, -10.0).cy < 0.0 ensure abs(yaw_forces(basis, -10.0).cd - ten.cd) < 0.000000001
test "pressure forces scale with the square of the wind speed": basis = aero_basis() low = aero_solve(basis, 12.0, 0.2) high = aero_solve(basis, 24.0, 0.2) ensure low.pressure_drag_n > 0.0 ensure abs(high.pressure_drag_n / low.pressure_drag_n - 4.0) < 0.000000001 ensure abs(high.dynamic_pressure_pa / low.dynamic_pressure_pa - 4.0) < 0.000000001
test "a stationary car in moving air still reports drag": basis = aero_basis() forces = aero_solve(basis, 27.8, 0.0) ensure forces.drag_n > 100.0 ensure forces.dynamic_pressure_pa > 0.0 ensure aero_solve(basis, 0.0, 0.0).drag_n == 0.0
test "coefficient build-up reproduces the integrated pressure force": basis = aero_basis() for step in range(0, 7): forces = yaw_forces(basis, float(step) * 6.0) ensure forces.force_closure_residual < 0.000000001 ensure len(forces.surface_cp) == 240 ensure forces.cp_max <= 1.0 and forces.cp_min >= -4.0
test "exported basis carries the panel field the browser recombines": basis = aero_basis() payload = aero_basis_export(basis) ensure payload["schema"] == "sema.circuitframe-aero-basis/v1" ensure payload["panel_count"] == 240 ensure len(payload["panels"]) == 240 ensure len(aero_equations()) == 21src/assembly.sema
Section titled “src/assembly.sema”"""Physical assembly model: ports, mates, joints, mass properties, load paths, and the coupled thermal network."""
import math
from magna_ev_digital_twin.domain import PartGroup, PortDefinition, PortKindfrom magna_ev_digital_twin.vehicle import vehicle_parts
assure silver
enum JointKind: fixed | revolute | prismatic | spherical | cylindrical | gear | rigid_bond
enum PlacementPattern: single | distributed | lateral_pair | longitudinal_pair | corner_quad
enum LoadPathKind: vertical | longitudinal | lateral | structural | crash | restraint
struct MateDefinition: id: str port_a_id: str port_b_id: str kind: PortKind preload_n: f64 sealed: bool serviceable: bool robot_accessible: bool invariant len(id) > 0 invariant len(port_a_id) > 0 invariant len(port_b_id) > 0 invariant port_a_id != port_b_id invariant preload_n >= 0.0
struct JointDefinition: id: str name: str kind: JointKind parent_part_id: str child_part_id: str parent_link: str child_link: str axis_x: f64 axis_y: f64 axis_z: f64 origin_x: f64 origin_y: f64 origin_z: f64 limit_lower: f64 limit_upper: f64 ratio: f64 dof: int idle_dof: int corner: str loop_closing: bool invariant len(id) > 0 invariant len(name) > 0 invariant len(parent_part_id) > 0 invariant len(child_part_id) > 0 invariant len(parent_link) > 0 invariant len(child_link) > 0 invariant parent_link != child_link invariant limit_upper >= limit_lower invariant ratio != 0.0 invariant dof >= 0 and dof <= 6 invariant idle_dof >= 0 and idle_dof <= dof
struct PartPlacement: part_id: str pattern: PlacementPattern pattern_instances: int mass_kg: f64 centre_x: f64 centre_y: f64 centre_z: f64 extent_x: f64 extent_y: f64 extent_z: f64 span_x: f64 span_y: f64 unsprung_fraction: f64 invariant len(part_id) > 0 invariant pattern_instances >= 1 and pattern_instances <= 4 invariant mass_kg >= 0.0 invariant extent_x > 0.0 and extent_y > 0.0 and extent_z > 0.0 invariant span_x >= 0.0 and span_y >= 0.0 invariant unsprung_fraction >= 0.0 and unsprung_fraction <= 1.0
pub struct MassProperties: total_mass_kg: f64 sprung_mass_kg: f64 unsprung_mass_kg: f64 cg_x_m: f64 cg_y_m: f64 cg_z_m: f64 ixx_kg_m2: f64 iyy_kg_m2: f64 izz_kg_m2: f64 front_axle_load_kg: f64 rear_axle_load_kg: f64 front_mass_fraction: f64 wheelbase_m: f64 cg_height_m: f64 part_count: int instance_count: int closure_residual_kg: f64 invariant total_mass_kg > 0.0 invariant sprung_mass_kg >= 0.0 invariant unsprung_mass_kg >= 0.0 invariant ixx_kg_m2 > 0.0 and iyy_kg_m2 > 0.0 and izz_kg_m2 > 0.0 invariant front_axle_load_kg >= 0.0 and rear_axle_load_kg >= 0.0 invariant front_mass_fraction > 0.0 and front_mass_fraction < 1.0 invariant wheelbase_m > 0.0 invariant cg_height_m > 0.0 invariant part_count > 0 invariant instance_count >= part_count
struct LoadPath: id: str name: str kind: LoadPathKind part_ids: list[str] rated_load_n: f64 utilisation: f64 invariant len(id) > 0 invariant len(name) > 0 invariant len(part_ids) >= 2 and len(part_ids) <= 32 invariant rated_load_n > 0.0 invariant utilisation >= 0.0
struct LoadPathReaction: path_id: str element_index: int part_id: str role: str force_n: f64 moment_nm: f64 invariant len(path_id) > 0 invariant element_index >= 0 invariant len(part_id) > 0 invariant len(role) > 0
struct LoadPathResult: path_id: str name: str kind: LoadPathKind applied_n: f64 reaction_n: f64 residual_n: f64 peak_force_n: f64 rated_load_n: f64 utilisation: f64 reactions: list[LoadPathReaction] invariant len(path_id) > 0 invariant rated_load_n > 0.0 invariant peak_force_n >= 0.0 invariant utilisation >= 0.0 invariant len(reactions) >= 2
struct LoadCase: vertical_n: f64 longitudinal_n: f64 lateral_n: f64 results: list[LoadPathResult] equilibrium_residual_n: f64 moment_residual_nm: f64 worst_path_id: str worst_utilisation: f64 invariant len(results) > 0 invariant equilibrium_residual_n >= 0.0 invariant moment_residual_nm >= 0.0 invariant worst_utilisation >= 0.0
pub struct SuspensionState: corner_id: str vertical_load_n: f64 arm_rotation_rad: f64 wheel_travel_m: f64 spring_stroke_m: f64 damper_length_m: f64 spring_force_n: f64 wheel_rate_n_m: f64 motion_ratio: f64 camber_deg: f64 camber_change_deg: f64 toe_deg: f64 toe_change_deg: f64 kingpin_rotation_rad: f64 jounce_limit_m: f64 rebound_limit_m: f64 travel_utilisation: f64 at_limit: bool invariant len(corner_id) > 0 invariant vertical_load_n >= 0.0 invariant damper_length_m > 0.0 invariant motion_ratio > 0.0 invariant jounce_limit_m > 0.0 invariant rebound_limit_m < 0.0 invariant travel_utilisation >= 0.0 and travel_utilisation <= 1.0
struct ThermalNode: id: str part_id: str capacity_kj_k: f64 ambient_coupling_w_k: f64 initial_temp_c: f64 invariant len(id) > 0 invariant len(part_id) > 0 invariant capacity_kj_k > 0.0 invariant ambient_coupling_w_k >= 0.0 invariant initial_temp_c >= -60.0 and initial_temp_c <= 200.0
struct ThermalLink: id: str from_node_id: str to_node_id: str conductance_w_k: f64 medium: str invariant len(id) > 0 invariant len(from_node_id) > 0 invariant len(to_node_id) > 0 invariant from_node_id != to_node_id invariant conductance_w_k > 0.0 invariant len(medium) > 0
struct ThermalNetwork: id: str nodes: list[ThermalNode] links: list[ThermalLink] invariant len(id) > 0 invariant len(nodes) >= 2 invariant len(links) >= 1
struct MobilityReport: corner_id: str link_count: int joint_count: int joint_dof_sum: int gross_mobility: int idle_dof: int effective_mobility: int expected_mobility: int independent_loops: int loop_closing_joint_ids: list[str] ok: bool invariant len(corner_id) > 0 invariant link_count >= 2 invariant joint_count >= 1 invariant joint_dof_sum >= joint_count invariant independent_loops >= 0
CONFIGURATION_VARIANT_DEFAULT = "circuitframe"ASSEMBLY_SCHEMA = "sema.circuitframe-assembly/v1"ASSEMBLY_AUTHORITY = "bounded engineered reconstruction: hard points, mates, joints, and mass properties are declared assumptions derived from the published exterior envelope, not OEM CAD or measured inertia data"GRAVITY_MPS2 = 9.80665WHEELBASE_M = 2.890HALF_TRACK_M = 0.8175FRONT_AXLE_X_M = 1.445REAR_AXLE_X_M = -1.445WHEEL_RADIUS_M = 0.3695RIDE_HEIGHT_M = 0.167ASSEMBLY_MASS_KG = 2108.2
HP_LCA_IN_Y = 0.320HP_LCA_IN_Z = 0.215HP_LCA_BUSH_DX = 0.140HP_UCA_IN_Y = 0.400HP_UCA_IN_Z = 0.470HP_LBJ_Y = 0.735HP_LBJ_Z = 0.185HP_UBJ_Y = 0.700HP_UBJ_Z = 0.545HP_WC_Y = 0.8175HP_WC_Z = 0.3695HP_TIE_DX = -0.125HP_TIE_IN_Y = 0.360HP_TIE_IN_Z = 0.2885HP_TIE_OUT_Y = 0.720HP_TIE_OUT_Z = 0.300HP_KINGPIN_DX = -0.047HP_DAMPER_LO_Y = 0.620HP_DAMPER_LO_Z = 0.19331HP_DAMPER_UP_Y = 0.560HP_DAMPER_UP_Z = 0.760HP_ARB_BAR_DX = -0.230HP_ARB_BAR_Y = 0.480HP_ARB_BAR_Z = 0.250HP_ARB_LINK_Y = 0.660HP_ARB_LINK_Z = 0.205
AIR_SPRING_AREA_M2 = 0.01539AIR_SPRING_VOLUME_M3 = 0.00232AIR_SPRING_POLYTROPIC_N = 1.38ATMOSPHERIC_PRESSURE_PA = 101325.0JOUNCE_LIMIT_M = 0.090REBOUND_LIMIT_M = -0.080STATIC_CAMBER_DEG = -0.5STATIC_TOE_DEG = 0.08TRAVEL_SOLVE_ITERATIONS = 22STEER_SOLVE_ITERATIONS = 6
REFERENCE_BRAKING_N = 9000.0REFERENCE_CORNERING_N = 6000.0GEAR_STAGE_ONE_RATIO = 3.25GEAR_STAGE_TWO_RATIO = 2.8
equation linear_solve_3(matrix, rhs) -> any: """Dense 3x3 static-equilibrium solve; returns the reaction vector.""" return solve(matrix, rhs)
equation grubler_mobility(links, joints, joint_dof_sum) -> any: """Grubler-Kutzbach spatial mobility of a closed kinematic loop.""" return 6 * (links - 1 - joints) + joint_dof_sum
equation polytropic_pressure(reference_pressure, reference_volume, volume, exponent) -> any: """Polytropic gas state of an air spring bellows.""" return reference_pressure * (reference_volume / volume)^exponent
equation box_inertia_term(mass, edge_a, edge_b) -> any: """Second moment of a uniform rectangular prism about a centroidal axis.""" return mass * (edge_a^2 + edge_b^2) / 12.0
equation parallel_axis_term(mass, offset_a, offset_b) -> any: """Parallel-axis transfer of a body inertia to the vehicle centre of mass.""" return mass * (offset_a^2 + offset_b^2)
def interface_specs() -> list[dict[str, any]]: """Declared part-to-part interfaces. Every row expands into two ports and one terminating mate.""" return [ {"mate": "mate-front-subframe-body-rail", "part_a": "front-subframe", "port_a": "front-subframe.mech.body-rail", "part_b": "body-shell", "port_b": "body-shell.mech.front-subframe-rail", "kind": PortKind.mechanical, "preload_n": 42000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-subframe-body-rail", "part_a": "rear-subframe", "port_a": "rear-subframe.mech.body-rail", "part_b": "body-shell", "port_b": "body-shell.mech.rear-subframe-rail", "kind": PortKind.mechanical, "preload_n": 38000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-floor-pan-body-bond", "part_a": "floor-pan", "port_a": "floor-pan.mech.body-bond", "part_b": "body-shell", "port_b": "body-shell.mech.floor-flange", "kind": PortKind.mechanical, "preload_n": 0.0, "sealed": true, "serviceable": false, "robot_accessible": true}, {"mate": "mate-sill-panels-floor-flange", "part_a": "sill-panels", "port_a": "sill-panels.mech.floor-flange", "part_b": "floor-pan", "port_b": "floor-pan.mech.sill-flange", "kind": PortKind.mechanical, "preload_n": 26000.0, "sealed": true, "serviceable": false, "robot_accessible": true}, {"mate": "mate-sill-panels-body-flange", "part_a": "sill-panels", "port_a": "sill-panels.mech.body-flange", "part_b": "body-shell", "port_b": "body-shell.mech.sill-flange", "kind": PortKind.mechanical, "preload_n": 24000.0, "sealed": true, "serviceable": false, "robot_accessible": true}, {"mate": "mate-battery-enclosure-floor-mount", "part_a": "battery-enclosure", "port_a": "battery-enclosure.mech.floor-mount", "part_b": "floor-pan", "port_b": "floor-pan.mech.pack-mount", "kind": PortKind.mechanical, "preload_n": 96000.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-battery-enclosure-sill-mount", "part_a": "battery-enclosure", "port_a": "battery-enclosure.mech.sill-mount", "part_b": "sill-panels", "port_b": "sill-panels.mech.pack-mount", "kind": PortKind.mechanical, "preload_n": 54000.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-battery-modules-enclosure-rail", "part_a": "battery-modules", "port_a": "battery-modules.mech.enclosure-rail", "part_b": "battery-enclosure", "port_b": "battery-enclosure.mech.module-rail", "kind": PortKind.mechanical, "preload_n": 18000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-battery-management-enclosure-bracket", "part_a": "battery-management", "port_a": "battery-management.mech.enclosure-bracket", "part_b": "battery-enclosure", "port_b": "battery-enclosure.mech.bms-bracket", "kind": PortKind.mechanical, "preload_n": 900.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-motor-subframe-mount", "part_a": "front-motor", "port_a": "front-motor.mech.subframe-mount", "part_b": "front-subframe", "port_b": "front-subframe.mech.motor-mount", "kind": PortKind.mechanical, "preload_n": 34000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-motor-subframe-mount", "part_a": "rear-motor", "port_a": "rear-motor.mech.subframe-mount", "part_b": "rear-subframe", "port_b": "rear-subframe.mech.motor-mount", "kind": PortKind.mechanical, "preload_n": 37000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-reduction-gears-front-motor-flange", "part_a": "reduction-gears", "port_a": "reduction-gears.mech.front-motor-flange", "part_b": "front-motor", "port_b": "front-motor.mech.gearbox-flange", "kind": PortKind.mechanical, "preload_n": 21000.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-reduction-gears-rear-motor-flange", "part_a": "reduction-gears", "port_a": "reduction-gears.mech.rear-motor-flange", "part_b": "rear-motor", "port_b": "rear-motor.mech.gearbox-flange", "kind": PortKind.mechanical, "preload_n": 23000.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-inverter-motor-mount", "part_a": "front-inverter", "port_a": "front-inverter.mech.motor-mount", "part_b": "front-motor", "port_b": "front-motor.mech.inverter-pad", "kind": PortKind.mechanical, "preload_n": 5200.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-inverter-motor-mount", "part_a": "rear-inverter", "port_a": "rear-inverter.mech.motor-mount", "part_b": "rear-motor", "port_b": "rear-motor.mech.inverter-pad", "kind": PortKind.mechanical, "preload_n": 5600.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-suspension-front-subframe-pivot", "part_a": "suspension", "port_a": "suspension.mech.front-subframe-pivot", "part_b": "front-subframe", "port_b": "front-subframe.mech.control-arm-pivot", "kind": PortKind.mechanical, "preload_n": 28000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-suspension-rear-subframe-pivot", "part_a": "suspension", "port_a": "suspension.mech.rear-subframe-pivot", "part_b": "rear-subframe", "port_b": "rear-subframe.mech.control-arm-pivot", "kind": PortKind.mechanical, "preload_n": 29000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-suspension-damper-top", "part_a": "suspension", "port_a": "suspension.mech.damper-top", "part_b": "body-shell", "port_b": "body-shell.mech.damper-tower", "kind": PortKind.mechanical, "preload_n": 16000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-brakes-knuckle-mount", "part_a": "brakes", "port_a": "brakes.mech.knuckle-mount", "part_b": "suspension", "port_b": "suspension.mech.caliper-pad", "kind": PortKind.mechanical, "preload_n": 12500.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-wheels-hub-face", "part_a": "wheels", "port_a": "wheels.mech.hub-face", "part_b": "brakes", "port_b": "brakes.mech.hub-face", "kind": PortKind.mechanical, "preload_n": 11800.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-tires-bead-seat", "part_a": "tires", "port_a": "tires.mech.bead-seat", "part_b": "wheels", "port_b": "wheels.mech.rim-flange", "kind": PortKind.mechanical, "preload_n": 0.0, "sealed": true, "serviceable": true, "robot_accessible": false}, {"mate": "mate-steering-subframe-mount", "part_a": "steering", "port_a": "steering.mech.subframe-mount", "part_b": "front-subframe", "port_b": "front-subframe.mech.rack-mount", "kind": PortKind.mechanical, "preload_n": 14000.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-steering-tie-rod-end", "part_a": "steering", "port_a": "steering.mech.tie-rod-end", "part_b": "suspension", "port_b": "suspension.mech.steering-arm", "kind": PortKind.mechanical, "preload_n": 3800.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-closures-hinge-pin", "part_a": "closures", "port_a": "closures.mech.hinge-pin", "part_b": "body-shell", "port_b": "body-shell.mech.hinge-boss", "kind": PortKind.mechanical, "preload_n": 7400.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-glazing-urethane-bead", "part_a": "glazing", "port_a": "glazing.mech.urethane-bead", "part_b": "body-shell", "port_b": "body-shell.mech.glazing-flange", "kind": PortKind.mechanical, "preload_n": 0.0, "sealed": true, "serviceable": false, "robot_accessible": true}, {"mate": "mate-glazing-door-channel", "part_a": "glazing", "port_a": "glazing.mech.door-channel", "part_b": "closures", "port_b": "closures.mech.glazing-channel", "kind": PortKind.mechanical, "preload_n": 0.0, "sealed": true, "serviceable": false, "robot_accessible": false}, {"mate": "mate-seats-floor-rail", "part_a": "seats", "port_a": "seats.mech.floor-rail", "part_b": "floor-pan", "port_b": "floor-pan.mech.seat-rail", "kind": PortKind.mechanical, "preload_n": 9600.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-interior-body-clip", "part_a": "interior", "port_a": "interior.mech.body-clip", "part_b": "body-shell", "port_b": "body-shell.mech.trim-boss", "kind": PortKind.mechanical, "preload_n": 1400.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-restraints-pillar-anchor", "part_a": "restraints", "port_a": "restraints.mech.pillar-anchor", "part_b": "body-shell", "port_b": "body-shell.mech.belt-anchor", "kind": PortKind.mechanical, "preload_n": 13500.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-restraints-seat-buckle", "part_a": "restraints", "port_a": "restraints.mech.seat-buckle", "part_b": "seats", "port_b": "seats.mech.buckle-anchor", "kind": PortKind.mechanical, "preload_n": 13500.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-lamps-body-socket", "part_a": "lamps", "port_a": "lamps.mech.body-socket", "part_b": "body-shell", "port_b": "body-shell.mech.lamp-aperture", "kind": PortKind.mechanical, "preload_n": 900.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-heat-pump-subframe-bracket", "part_a": "heat-pump", "port_a": "heat-pump.mech.subframe-bracket", "part_b": "front-subframe", "port_b": "front-subframe.mech.hvac-bracket", "kind": PortKind.mechanical, "preload_n": 3200.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-body-clip", "part_a": "thermal-loop", "port_a": "thermal-loop.mech.body-clip", "part_b": "body-shell", "port_b": "body-shell.mech.coolant-clip", "kind": PortKind.mechanical, "preload_n": 700.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-charge-port-body-aperture", "part_a": "charge-port", "port_a": "charge-port.mech.body-aperture", "part_b": "body-shell", "port_b": "body-shell.mech.charge-aperture", "kind": PortKind.mechanical, "preload_n": 800.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-onboard-charger-floor-bracket", "part_a": "onboard-charger", "port_a": "onboard-charger.mech.floor-bracket", "part_b": "floor-pan", "port_b": "floor-pan.mech.charger-bracket", "kind": PortKind.mechanical, "preload_n": 2600.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-hv-junction-enclosure-mount", "part_a": "hv-junction", "port_a": "hv-junction.mech.enclosure-mount", "part_b": "battery-enclosure", "port_b": "battery-enclosure.mech.junction-pad", "kind": PortKind.mechanical, "preload_n": 4200.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-dc-dc-body-bracket", "part_a": "dc-dc", "port_a": "dc-dc.mech.body-bracket", "part_b": "body-shell", "port_b": "body-shell.mech.converter-bracket", "kind": PortKind.mechanical, "preload_n": 2100.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-central-compute-body-bracket", "part_a": "central-compute", "port_a": "central-compute.mech.body-bracket", "part_b": "body-shell", "port_b": "body-shell.mech.compute-bracket", "kind": PortKind.mechanical, "preload_n": 1500.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-lv-battery-body-tray", "part_a": "lv-battery", "port_a": "lv-battery.mech.body-tray", "part_b": "body-shell", "port_b": "body-shell.mech.lv-tray", "kind": PortKind.mechanical, "preload_n": 2300.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-zone-controller-body-bracket", "part_a": "front-zone-controller", "port_a": "front-zone-controller.mech.body-bracket", "part_b": "body-shell", "port_b": "body-shell.mech.front-zone-bracket", "kind": PortKind.mechanical, "preload_n": 800.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-zone-controller-body-bracket", "part_a": "rear-zone-controller", "port_a": "rear-zone-controller.mech.body-bracket", "part_b": "body-shell", "port_b": "body-shell.mech.rear-zone-bracket", "kind": PortKind.mechanical, "preload_n": 800.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-cabin-zone-controller-interior-bracket", "part_a": "cabin-zone-controller", "port_a": "cabin-zone-controller.mech.interior-bracket", "part_b": "interior", "port_b": "interior.mech.zone-bracket", "kind": PortKind.mechanical, "preload_n": 600.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-sensors-body-boss", "part_a": "sensors", "port_a": "sensors.mech.body-boss", "part_b": "body-shell", "port_b": "body-shell.mech.sensor-boss", "kind": PortKind.mechanical, "preload_n": 400.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-fasteners-seals-closure-seal", "part_a": "fasteners-seals", "port_a": "fasteners-seals.mech.closure-seal", "part_b": "closures", "port_b": "closures.mech.seal-groove", "kind": PortKind.mechanical, "preload_n": 0.0, "sealed": true, "serviceable": false, "robot_accessible": false}, {"mate": "mate-battery-modules-hv-positive", "part_a": "battery-modules", "port_a": "battery-modules.hv.hv-positive", "part_b": "hv-junction", "port_b": "hv-junction.hv.pack-positive", "kind": PortKind.high_voltage, "preload_n": 320.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-battery-modules-hv-negative", "part_a": "battery-modules", "port_a": "battery-modules.hv.hv-negative", "part_b": "hv-junction", "port_b": "hv-junction.hv.pack-negative", "kind": PortKind.high_voltage, "preload_n": 320.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-hv-junction-front-drive", "part_a": "hv-junction", "port_a": "hv-junction.hv.front-drive", "part_b": "front-inverter", "port_b": "front-inverter.hv.hv-input", "kind": PortKind.high_voltage, "preload_n": 260.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-hv-junction-rear-drive", "part_a": "hv-junction", "port_a": "hv-junction.hv.rear-drive", "part_b": "rear-inverter", "port_b": "rear-inverter.hv.hv-input", "kind": PortKind.high_voltage, "preload_n": 260.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-hv-junction-dcdc-feed", "part_a": "hv-junction", "port_a": "hv-junction.hv.dcdc-feed", "part_b": "dc-dc", "port_b": "dc-dc.hv.hv-input", "kind": PortKind.high_voltage, "preload_n": 180.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-hv-junction-charger-feed", "part_a": "hv-junction", "port_a": "hv-junction.hv.charger-feed", "part_b": "onboard-charger", "port_b": "onboard-charger.hv.hv-output", "kind": PortKind.high_voltage, "preload_n": 180.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-charge-port-dc-inlet", "part_a": "charge-port", "port_a": "charge-port.hv.dc-inlet", "part_b": "hv-junction", "port_b": "hv-junction.hv.dc-fast-input", "kind": PortKind.high_voltage, "preload_n": 240.0, "sealed": true, "serviceable": true, "robot_accessible": false}, {"mate": "mate-hv-junction-thermal-feed", "part_a": "hv-junction", "port_a": "hv-junction.hv.thermal-feed", "part_b": "heat-pump", "port_b": "heat-pump.hv.hv-input", "kind": PortKind.high_voltage, "preload_n": 160.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-inverter-phase-output", "part_a": "front-inverter", "port_a": "front-inverter.hv.phase-output", "part_b": "front-motor", "port_b": "front-motor.hv.phase-terminal", "kind": PortKind.high_voltage, "preload_n": 210.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-inverter-phase-output", "part_a": "rear-inverter", "port_a": "rear-inverter.hv.phase-output", "part_b": "rear-motor", "port_b": "rear-motor.hv.phase-terminal", "kind": PortKind.high_voltage, "preload_n": 210.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-sill-panels-hv-busbar", "part_a": "sill-panels", "port_a": "sill-panels.hv.hv-busbar", "part_b": "hv-junction", "port_b": "hv-junction.hv.sill-busbar", "kind": PortKind.high_voltage, "preload_n": 140.0, "sealed": true, "serviceable": false, "robot_accessible": true}, {"mate": "mate-dc-dc-lv-output", "part_a": "dc-dc", "port_a": "dc-dc.lv.lv-output", "part_b": "lv-battery", "port_b": "lv-battery.lv.lv-terminal", "kind": PortKind.low_voltage, "preload_n": 60.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-dc-dc-front-zone-feed", "part_a": "dc-dc", "port_a": "dc-dc.lv.front-zone-feed", "part_b": "front-zone-controller", "port_b": "front-zone-controller.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 40.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-dc-dc-rear-zone-feed", "part_a": "dc-dc", "port_a": "dc-dc.lv.rear-zone-feed", "part_b": "rear-zone-controller", "port_b": "rear-zone-controller.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 40.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-dc-dc-cabin-zone-feed", "part_a": "dc-dc", "port_a": "dc-dc.lv.cabin-zone-feed", "part_b": "cabin-zone-controller", "port_b": "cabin-zone-controller.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 40.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-dc-dc-compute-feed", "part_a": "dc-dc", "port_a": "dc-dc.lv.compute-feed", "part_b": "central-compute", "port_b": "central-compute.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 40.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-lv-battery-contactor-supply", "part_a": "lv-battery", "port_a": "lv-battery.lv.contactor-supply", "part_b": "hv-junction", "port_b": "hv-junction.lv.contactor-coil", "kind": PortKind.low_voltage, "preload_n": 25.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-zone-controller-lamp-feed", "part_a": "front-zone-controller", "port_a": "front-zone-controller.lv.lamp-feed", "part_b": "lamps", "port_b": "lamps.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 20.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-zone-controller-steering-feed", "part_a": "front-zone-controller", "port_a": "front-zone-controller.lv.steering-feed", "part_b": "steering", "port_b": "steering.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 55.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-zone-controller-brake-feed", "part_a": "rear-zone-controller", "port_a": "rear-zone-controller.lv.brake-feed", "part_b": "brakes", "port_b": "brakes.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 45.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-cabin-zone-controller-seat-feed", "part_a": "cabin-zone-controller", "port_a": "cabin-zone-controller.lv.seat-feed", "part_b": "seats", "port_b": "seats.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 20.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-cabin-zone-controller-squib-feed", "part_a": "cabin-zone-controller", "port_a": "cabin-zone-controller.lv.squib-feed", "part_b": "restraints", "port_b": "restraints.lv.lv-input", "kind": PortKind.low_voltage, "preload_n": 15.0, "sealed": false, "serviceable": false, "robot_accessible": false}, {"mate": "mate-central-compute-front-zone-link", "part_a": "central-compute", "port_a": "central-compute.data.front-zone-link", "part_b": "front-zone-controller", "port_b": "front-zone-controller.data.backbone-a", "kind": PortKind.data, "preload_n": 12.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-central-compute-rear-zone-link", "part_a": "central-compute", "port_a": "central-compute.data.rear-zone-link", "part_b": "rear-zone-controller", "port_b": "rear-zone-controller.data.backbone-a", "kind": PortKind.data, "preload_n": 12.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-central-compute-cabin-zone-link", "part_a": "central-compute", "port_a": "central-compute.data.cabin-zone-link", "part_b": "cabin-zone-controller", "port_b": "cabin-zone-controller.data.backbone-a", "kind": PortKind.data, "preload_n": 12.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-zone-controller-inverter-link", "part_a": "front-zone-controller", "port_a": "front-zone-controller.data.inverter-link", "part_b": "front-inverter", "port_b": "front-inverter.data.control-port", "kind": PortKind.data, "preload_n": 10.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-rear-zone-controller-inverter-link", "part_a": "rear-zone-controller", "port_a": "rear-zone-controller.data.inverter-link", "part_b": "rear-inverter", "port_b": "rear-inverter.data.control-port", "kind": PortKind.data, "preload_n": 10.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-zone-controller-brake-link", "part_a": "front-zone-controller", "port_a": "front-zone-controller.data.brake-link", "part_b": "brakes", "port_b": "brakes.data.control-port", "kind": PortKind.data, "preload_n": 10.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-front-zone-controller-steer-link", "part_a": "front-zone-controller", "port_a": "front-zone-controller.data.steer-link", "part_b": "steering", "port_b": "steering.data.control-port", "kind": PortKind.data, "preload_n": 10.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-battery-management-cell-sense", "part_a": "battery-management", "port_a": "battery-management.data.cell-sense", "part_b": "battery-modules", "port_b": "battery-modules.data.sense-header", "kind": PortKind.data, "preload_n": 8.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-battery-management-thermal-link", "part_a": "battery-management", "port_a": "battery-management.data.thermal-link", "part_b": "cabin-zone-controller", "port_b": "cabin-zone-controller.data.battery-port", "kind": PortKind.data, "preload_n": 8.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-sensors-compute-link", "part_a": "sensors", "port_a": "sensors.data.compute-link", "part_b": "central-compute", "port_b": "central-compute.data.sensor-port", "kind": PortKind.data, "preload_n": 8.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-cabin-zone-controller-loop-link", "part_a": "cabin-zone-controller", "port_a": "cabin-zone-controller.data.loop-link", "part_b": "thermal-loop", "port_b": "thermal-loop.data.sensor-port", "kind": PortKind.data, "preload_n": 8.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-cabin-zone-controller-heatpump-link", "part_a": "cabin-zone-controller", "port_a": "cabin-zone-controller.data.heatpump-link", "part_b": "heat-pump", "port_b": "heat-pump.data.control-port", "kind": PortKind.data, "preload_n": 8.0, "sealed": false, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-pack-supply", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.pack-supply", "part_b": "battery-enclosure", "port_b": "battery-enclosure.fluid.cold-plate-inlet", "kind": PortKind.fluid, "preload_n": 1200.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-front-drive-supply", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.front-drive-supply", "part_b": "front-motor", "port_b": "front-motor.fluid.jacket-inlet", "kind": PortKind.fluid, "preload_n": 900.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-rear-drive-supply", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.rear-drive-supply", "part_b": "rear-motor", "port_b": "rear-motor.fluid.jacket-inlet", "kind": PortKind.fluid, "preload_n": 900.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-front-inverter-supply", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.front-inverter-supply", "part_b": "front-inverter", "port_b": "front-inverter.fluid.cold-plate-inlet", "kind": PortKind.fluid, "preload_n": 700.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-rear-inverter-supply", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.rear-inverter-supply", "part_b": "rear-inverter", "port_b": "rear-inverter.fluid.cold-plate-inlet", "kind": PortKind.fluid, "preload_n": 700.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-chiller-port", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.chiller-port", "part_b": "heat-pump", "port_b": "heat-pump.fluid.chiller-inlet", "kind": PortKind.fluid, "preload_n": 1000.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-thermal-loop-charger-supply", "part_a": "thermal-loop", "port_a": "thermal-loop.fluid.charger-supply", "part_b": "onboard-charger", "port_b": "onboard-charger.fluid.cold-plate-inlet", "kind": PortKind.fluid, "preload_n": 600.0, "sealed": true, "serviceable": true, "robot_accessible": true}, {"mate": "mate-heat-pump-cabin-duct", "part_a": "heat-pump", "port_a": "heat-pump.therm.cabin-duct", "part_b": "interior", "port_b": "interior.therm.hvac-duct", "kind": PortKind.thermal, "preload_n": 0.0, "sealed": false, "serviceable": false, "robot_accessible": false}, {"mate": "mate-battery-modules-pad-interface", "part_a": "battery-modules", "port_a": "battery-modules.therm.pad-interface", "part_b": "battery-enclosure", "port_b": "battery-enclosure.therm.thermal-pad", "kind": PortKind.thermal, "preload_n": 0.0, "sealed": false, "serviceable": false, "robot_accessible": true}, {"mate": "mate-front-inverter-housing-interface", "part_a": "front-inverter", "port_a": "front-inverter.therm.housing-interface", "part_b": "front-motor", "port_b": "front-motor.therm.housing-pad", "kind": PortKind.thermal, "preload_n": 0.0, "sealed": false, "serviceable": false, "robot_accessible": true}, {"mate": "mate-rear-inverter-housing-interface", "part_a": "rear-inverter", "port_a": "rear-inverter.therm.housing-interface", "part_b": "rear-motor", "port_b": "rear-motor.therm.housing-pad", "kind": PortKind.thermal, "preload_n": 0.0, "sealed": false, "serviceable": false, "robot_accessible": true}, {"mate": "mate-service-links-hv-disconnect", "part_a": "service-links", "port_a": "service-links.svc.hv-disconnect", "part_b": "hv-junction", "port_b": "hv-junction.svc.service-disconnect", "kind": PortKind.service, "preload_n": 300.0, "sealed": true, "serviceable": true, "robot_accessible": false}, {"mate": "mate-service-links-pack-service", "part_a": "service-links", "port_a": "service-links.svc.pack-service", "part_b": "battery-enclosure", "port_b": "battery-enclosure.svc.service-port", "kind": PortKind.service, "preload_n": 250.0, "sealed": true, "serviceable": true, "robot_accessible": false}, {"mate": "mate-service-links-front-zone-service", "part_a": "service-links", "port_a": "service-links.svc.front-zone-service", "part_b": "front-zone-controller", "port_b": "front-zone-controller.svc.service-port", "kind": PortKind.service, "preload_n": 60.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-service-links-rear-zone-service", "part_a": "service-links", "port_a": "service-links.svc.rear-zone-service", "part_b": "rear-zone-controller", "port_b": "rear-zone-controller.svc.service-port", "kind": PortKind.service, "preload_n": 60.0, "sealed": false, "serviceable": true, "robot_accessible": false}, {"mate": "mate-service-links-charge-service", "part_a": "service-links", "port_a": "service-links.svc.charge-service", "part_b": "charge-port", "port_b": "charge-port.svc.service-port", "kind": PortKind.service, "preload_n": 80.0, "sealed": false, "serviceable": true, "robot_accessible": false}, ]
def open_port_specs() -> list[dict[str, any]]: """Declared but intentionally unmated interfaces. None of them is a required port.""" return [ {"part": "body-shell", "port": "body-shell.mech.tow-eye-front", "kind": PortKind.mechanical}, {"part": "body-shell", "port": "body-shell.mech.tow-eye-rear", "kind": PortKind.mechanical}, {"part": "body-shell", "port": "body-shell.mech.roof-rail-left", "kind": PortKind.mechanical}, {"part": "body-shell", "port": "body-shell.mech.roof-rail-right", "kind": PortKind.mechanical}, {"part": "central-compute", "port": "central-compute.svc.obd-diagnostic", "kind": PortKind.service}, {"part": "central-compute", "port": "central-compute.data.telemetry-expansion", "kind": PortKind.data}, {"part": "hv-junction", "port": "hv-junction.hv.v2l-outlet", "kind": PortKind.high_voltage}, {"part": "thermal-loop", "port": "thermal-loop.fluid.auxiliary-heater-port", "kind": PortKind.fluid}, ]
def vehicle_ports(): """Full port inventory: two terminated ports per declared interface plus the open optional ports.""" mut ports = [] for spec in interface_specs(): ports.append(PortDefinition(id=spec["port_a"], owner_part_id=spec["part_a"], kind=spec["kind"], mate_port_id=spec["port_b"], required=true, terminated=true)) ports.append(PortDefinition(id=spec["port_b"], owner_part_id=spec["part_b"], kind=spec["kind"], mate_port_id=spec["port_a"], required=true, terminated=true)) for spec in open_port_specs(): ports.append(PortDefinition(id=spec["port"], owner_part_id=spec["part"], kind=spec["kind"], mate_port_id="", required=false, terminated=false)) return ports
def vehicle_mates(): """One mate per declared interface, carrying its preload, sealing, and service classification.""" mut mates = [] for spec in interface_specs(): mates.append(MateDefinition(id=spec["mate"], port_a_id=spec["port_a"], port_b_id=spec["port_b"], kind=spec["kind"], preload_n=spec["preload_n"], sealed=spec["sealed"], serviceable=spec["serviceable"], robot_accessible=spec["robot_accessible"])) return mates
def port_termination_report(): """Counts required, terminated, open, and multiply-mated ports by scanning the inventory.""" ports = vehicle_ports() mates = vehicle_mates() mut required_open = 0 mut open_optional = 0 mut mismated = 0 for port in ports: if port.required and not port.terminated: required_open = required_open + 1 if not port.terminated: open_optional = open_optional + 1 if port.terminated: mut hits = 0 for mate in mates: if mate.port_a_id == port.id or mate.port_b_id == port.id: hits = hits + 1 if hits != 1: mismated = mismated + 1 return { "ports": len(ports), "mates": len(mates), "unterminated_required_ports": required_open, "open_optional_ports": open_optional, "ports_mated_exactly_once": len(ports) - open_optional - mismated, "mismated_ports": mismated, }
def part_placements(): """Placement of every declared part in vehicle coordinates, in the same order as vehicle_parts().""" return [ PartPlacement(part_id="body-shell", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=236.0, centre_x=0.0, centre_y=0.0, centre_z=0.98, extent_x=4.4, extent_y=1.86, extent_z=1.18, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="floor-pan", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=82.0, centre_x=-0.05, centre_y=0.0, centre_z=0.235, extent_x=3.15, extent_y=1.52, extent_z=0.13, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="front-subframe", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=31.0, centre_x=1.445, centre_y=0.0, centre_z=0.3, extent_x=0.64, extent_y=1.4, extent_z=0.23, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="rear-subframe", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=34.0, centre_x=-1.48, centre_y=0.0, centre_z=0.345, extent_x=0.7, extent_y=1.42, extent_z=0.26, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="sill-panels", pattern=PlacementPattern.lateral_pair, pattern_instances=2, mass_kg=26.0, centre_x=0.0, centre_y=0.0, centre_z=0.29, extent_x=2.9, extent_y=0.135, extent_z=0.24, span_x=0.0, span_y=1.545, unsprung_fraction=0.0), PartPlacement(part_id="battery-enclosure", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=118.0, centre_x=-0.02, centre_y=0.0, centre_z=0.23, extent_x=2.96, extent_y=1.56, extent_z=0.17, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="battery-modules", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=438.0, centre_x=-0.02, centre_y=0.0, centre_z=0.25, extent_x=2.82, extent_y=1.44, extent_z=0.115, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="battery-management", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=6.0, centre_x=-0.62, centre_y=0.3, centre_z=0.31, extent_x=0.24, extent_y=0.18, extent_z=0.055, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="hv-junction", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=13.0, centre_x=-0.98, centre_y=0.42, centre_z=0.36, extent_x=0.32, extent_y=0.24, extent_z=0.15, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="charge-port", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=4.0, centre_x=-2.18, centre_y=0.76, centre_z=0.76, extent_x=0.19, extent_y=0.13, extent_z=0.17, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="onboard-charger", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=14.0, centre_x=-1.7, centre_y=-0.43, centre_z=0.42, extent_x=0.34, extent_y=0.26, extent_z=0.12, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="dc-dc", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=8.0, centre_x=1.32, centre_y=-0.4, centre_z=0.52, extent_x=0.26, extent_y=0.21, extent_z=0.11, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="front-inverter", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=11.0, centre_x=1.64, centre_y=0.18, centre_z=0.52, extent_x=0.28, extent_y=0.26, extent_z=0.18, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="rear-inverter", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=12.0, centre_x=-1.64, centre_y=-0.18, centre_z=0.54, extent_x=0.29, extent_y=0.27, extent_z=0.19, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="front-motor", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=58.0, centre_x=1.56, centre_y=0.0, centre_z=0.33, extent_x=0.4, extent_y=0.56, extent_z=0.34, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="rear-motor", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=71.0, centre_x=-1.56, centre_y=0.0, centre_z=0.345, extent_x=0.43, extent_y=0.6, extent_z=0.36, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="reduction-gears", pattern=PlacementPattern.longitudinal_pair, pattern_instances=2, mass_kg=42.0, centre_x=0.0, centre_y=0.0, centre_z=0.335, extent_x=0.24, extent_y=0.42, extent_z=0.33, span_x=3.06, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="wheels", pattern=PlacementPattern.corner_quad, pattern_instances=4, mass_kg=52.0, centre_x=0.0, centre_y=0.0, centre_z=0.3695, extent_x=0.235, extent_y=0.235, extent_z=0.739, span_x=2.89, span_y=1.635, unsprung_fraction=1.0), PartPlacement(part_id="tires", pattern=PlacementPattern.corner_quad, pattern_instances=4, mass_kg=48.0, centre_x=0.0, centre_y=0.0, centre_z=0.3695, extent_x=0.285, extent_y=0.285, extent_z=0.739, span_x=2.89, span_y=1.635, unsprung_fraction=1.0), PartPlacement(part_id="suspension", pattern=PlacementPattern.corner_quad, pattern_instances=4, mass_kg=104.0, centre_x=0.0, centre_y=0.0, centre_z=0.38, extent_x=0.48, extent_y=0.52, extent_z=0.56, span_x=2.89, span_y=1.3, unsprung_fraction=0.45), PartPlacement(part_id="steering", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=23.0, centre_x=1.3, centre_y=0.0, centre_z=0.33, extent_x=0.16, extent_y=1.18, extent_z=0.17, span_x=0.0, span_y=0.0, unsprung_fraction=0.1), PartPlacement(part_id="brakes", pattern=PlacementPattern.corner_quad, pattern_instances=4, mass_kg=76.0, centre_x=0.0, centre_y=0.0, centre_z=0.3695, extent_x=0.19, extent_y=0.23, extent_z=0.4, span_x=2.89, span_y=1.5, unsprung_fraction=0.85), PartPlacement(part_id="thermal-loop", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=31.0, centre_x=0.15, centre_y=0.0, centre_z=0.43, extent_x=3.9, extent_y=1.32, extent_z=0.42, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="heat-pump", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=24.0, centre_x=1.82, centre_y=0.24, centre_z=0.56, extent_x=0.42, extent_y=0.38, extent_z=0.33, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="front-zone-controller", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=2.2, centre_x=1.4, centre_y=0.56, centre_z=0.64, extent_x=0.2, extent_y=0.16, extent_z=0.06, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="rear-zone-controller", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=2.2, centre_x=-1.52, centre_y=-0.52, centre_z=0.62, extent_x=0.2, extent_y=0.16, extent_z=0.06, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="cabin-zone-controller", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=1.8, centre_x=0.56, centre_y=0.38, centre_z=0.62, extent_x=0.18, extent_y=0.15, extent_z=0.055, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="central-compute", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=5.0, centre_x=0.88, centre_y=0.0, centre_z=0.64, extent_x=0.3, extent_y=0.24, extent_z=0.09, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="lv-battery", pattern=PlacementPattern.single, pattern_instances=1, mass_kg=12.0, centre_x=1.68, centre_y=-0.56, centre_z=0.44, extent_x=0.26, extent_y=0.18, extent_z=0.19, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="sensors", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=18.0, centre_x=0.2, centre_y=0.0, centre_z=0.98, extent_x=4.5, extent_y=1.86, extent_z=1.24, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="lamps", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=16.0, centre_x=0.05, centre_y=0.0, centre_z=0.83, extent_x=4.56, extent_y=1.84, extent_z=0.56, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="glazing", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=47.0, centre_x=-0.15, centre_y=0.0, centre_z=1.19, extent_x=3.5, extent_y=1.7, extent_z=0.98, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="closures", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=112.0, centre_x=-0.06, centre_y=0.0, centre_z=0.93, extent_x=3.3, extent_y=1.88, extent_z=1.06, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="seats", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=96.0, centre_x=-0.23, centre_y=0.0, centre_z=0.76, extent_x=1.9, extent_y=1.44, extent_z=1.02, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="interior", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=118.0, centre_x=-0.12, centre_y=0.0, centre_z=0.79, extent_x=2.7, extent_y=1.68, extent_z=1.06, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="restraints", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=34.0, centre_x=0.15, centre_y=0.0, centre_z=1.02, extent_x=2.9, extent_y=1.76, extent_z=0.9, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="fasteners-seals", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=63.0, centre_x=0.0, centre_y=0.0, centre_z=0.82, extent_x=4.5, extent_y=1.88, extent_z=1.4, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), PartPlacement(part_id="service-links", pattern=PlacementPattern.distributed, pattern_instances=1, mass_kg=19.0, centre_x=0.05, centre_y=0.0, centre_z=0.56, extent_x=4.2, extent_y=1.74, extent_z=0.9, span_x=0.0, span_y=0.0, unsprung_fraction=0.0), ]
def placement_instances(placement: PartPlacement): """Expands a placement pattern into its instance centres. Each row is [mass, x, y, z].""" share = placement.mass_kg / float(placement.pattern_instances) half_x = placement.span_x * 0.5 half_y = placement.span_y * 0.5 if placement.pattern == PlacementPattern.corner_quad: return [ [share, placement.centre_x + half_x, placement.centre_y + half_y, placement.centre_z], [share, placement.centre_x + half_x, placement.centre_y - half_y, placement.centre_z], [share, placement.centre_x - half_x, placement.centre_y + half_y, placement.centre_z], [share, placement.centre_x - half_x, placement.centre_y - half_y, placement.centre_z], ] if placement.pattern == PlacementPattern.lateral_pair: return [ [share, placement.centre_x, placement.centre_y + half_y, placement.centre_z], [share, placement.centre_x, placement.centre_y - half_y, placement.centre_z], ] if placement.pattern == PlacementPattern.longitudinal_pair: return [ [share, placement.centre_x + half_x, placement.centre_y, placement.centre_z], [share, placement.centre_x - half_x, placement.centre_y, placement.centre_z], ] return [[share, placement.centre_x, placement.centre_y, placement.centre_z]]
pub def mass_properties() -> MassProperties !{}: """Total mass, centre of gravity, and inertia tensor summed from the part placements.
Every part is treated as a uniform rectangular prism of its declared extent, transferred to the vehicle centre of mass by the parallel-axis theorem. The closure residual compares the declared part masses against the placement masses and must be exactly zero. """ parts = vehicle_parts() placements = part_placements() mut aligned = len(parts) == len(placements) mut declared_mass = 0.0 mut placed_mass = 0.0 mut instances = 0 mut unsprung = 0.0 mut moment_x = 0.0 mut moment_y = 0.0 mut moment_z = 0.0 for index in range(0, len(parts)): part = parts[index] placement = placements[index] if part.id != placement.part_id: aligned = false declared_mass = declared_mass + part.mass_kg placed_mass = placed_mass + placement.mass_kg instances = instances + part.count unsprung = unsprung + placement.mass_kg * placement.unsprung_fraction for row in placement_instances(placement): moment_x = moment_x + row[0] * row[1] moment_y = moment_y + row[0] * row[2] moment_z = moment_z + row[0] * row[3] ensure aligned cg_x = moment_x / placed_mass cg_y = moment_y / placed_mass cg_z = moment_z / placed_mass mut ixx = 0.0 mut iyy = 0.0 mut izz = 0.0 for placement in placements: for row in placement_instances(placement): ixx = ixx + box_inertia_term(row[0], placement.extent_y, placement.extent_z) + parallel_axis_term(row[0], row[2] - cg_y, row[3] - cg_z) iyy = iyy + box_inertia_term(row[0], placement.extent_x, placement.extent_z) + parallel_axis_term(row[0], row[1] - cg_x, row[3] - cg_z) izz = izz + box_inertia_term(row[0], placement.extent_x, placement.extent_y) + parallel_axis_term(row[0], row[1] - cg_x, row[2] - cg_y) front_fraction = (cg_x - REAR_AXLE_X_M) / WHEELBASE_M return MassProperties( total_mass_kg=placed_mass, sprung_mass_kg=placed_mass - unsprung, unsprung_mass_kg=unsprung, cg_x_m=cg_x, cg_y_m=cg_y, cg_z_m=cg_z, ixx_kg_m2=ixx, iyy_kg_m2=iyy, izz_kg_m2=izz, front_axle_load_kg=placed_mass * front_fraction, rear_axle_load_kg=placed_mass * (1.0 - front_fraction), front_mass_fraction=front_fraction, wheelbase_m=WHEELBASE_M, cg_height_m=cg_z, part_count=len(parts), instance_count=instances, closure_residual_kg=declared_mass - placed_mass, )
def corner_table(): """The four suspension corners: mirror signs, steering capability, and the subframe they hang from.""" return [ {"corner": "front-left", "prefix": "fl", "axle": "front", "sign_x": 1.0, "sign_y": 1.0, "steerable": true, "subframe": "front-subframe"}, {"corner": "front-right", "prefix": "fr", "axle": "front", "sign_x": 1.0, "sign_y": -1.0, "steerable": true, "subframe": "front-subframe"}, {"corner": "rear-left", "prefix": "rl", "axle": "rear", "sign_x": -1.0, "sign_y": 1.0, "steerable": false, "subframe": "rear-subframe"}, {"corner": "rear-right", "prefix": "rr", "axle": "rear", "sign_x": -1.0, "sign_y": -1.0, "steerable": false, "subframe": "rear-subframe"}, ]
def joint(id: str, name: str, kind: JointKind, parent_part_id: str, child_part_id: str, parent_link: str, child_link: str, axis: list[f64], origin: list[f64], limits: list[f64], ratio: f64, dof: int, idle_dof: int, corner: str, loop_closing: bool): """Constructs a joint from packed axis, origin, and limit triples.""" return JointDefinition( id=id, name=name, kind=kind, parent_part_id=parent_part_id, child_part_id=child_part_id, parent_link=parent_link, child_link=child_link, axis_x=axis[0], axis_y=axis[1], axis_z=axis[2], origin_x=origin[0], origin_y=origin[1], origin_z=origin[2], limit_lower=limits[0], limit_upper=limits[1], ratio=ratio, dof=dof, idle_dof=idle_dof, corner=corner, loop_closing=loop_closing, )
def corner_joints(entry: dict[str, any]): """Every joint of one suspension corner, mirrored from the reference hard points.""" prefix = entry["prefix"] corner = entry["corner"] axle = entry["axle"] subframe = entry["subframe"] sign_x = entry["sign_x"] sign_y = entry["sign_y"] axle_x = sign_x * FRONT_AXLE_X_M knuckle = prefix + "-knuckle" lower_arm = prefix + "-lower-arm" upper_arm = prefix + "-upper-arm" hub = prefix + "-hub" damper_dy = sign_y * (HP_DAMPER_UP_Y - HP_DAMPER_LO_Y) damper_dz = HP_DAMPER_UP_Z - HP_DAMPER_LO_Z damper_axis_length = math.sqrt(damper_dy * damper_dy + damper_dz * damper_dz) steer_x = axle_x + sign_x * HP_TIE_DX steer_link = prefix + "-tie-rod" if entry["steerable"] else prefix + "-toe-link" steer_root = "steering-rack" if entry["steerable"] else "chassis" steer_root_part = "steering" if entry["steerable"] else subframe steer_name = "tie rod" if entry["steerable"] else "toe link" steer_slug = "tie-rod" if entry["steerable"] else "toe-link" return [ joint(prefix + "-lca-inboard", corner + " lower control arm inboard pivot", JointKind.revolute, subframe, "suspension", "chassis", lower_arm, [1.0, 0.0, 0.0], [axle_x, sign_y * HP_LCA_IN_Y, HP_LCA_IN_Z], [-0.35, 0.35], 1.0, 1, 0, corner, false), joint(prefix + "-uca-inboard", corner + " upper control arm inboard pivot", JointKind.revolute, subframe, "suspension", "chassis", upper_arm, [1.0, 0.0, 0.0], [axle_x, sign_y * HP_UCA_IN_Y, HP_UCA_IN_Z], [-0.35, 0.35], 1.0, 1, 0, corner, false), joint(prefix + "-lca-outboard", corner + " lower ball joint", JointKind.spherical, "suspension", "suspension", lower_arm, knuckle, [0.0, 0.0, 1.0], [axle_x, sign_y * HP_LBJ_Y, HP_LBJ_Z], [-0.45, 0.45], 1.0, 3, 0, corner, false), joint(prefix + "-uca-outboard", corner + " upper ball joint", JointKind.spherical, "suspension", "suspension", upper_arm, knuckle, [0.0, 0.0, 1.0], [axle_x, sign_y * HP_UBJ_Y, HP_UBJ_Z], [-0.45, 0.45], 1.0, 3, 0, corner, true), joint(prefix + "-damper-top", corner + " damper upper mount", JointKind.spherical, "body-shell", "suspension", "chassis", prefix + "-damper-body", [0.0, damper_dy / damper_axis_length, damper_dz / damper_axis_length], [axle_x, sign_y * HP_DAMPER_UP_Y, HP_DAMPER_UP_Z], [-0.20, 0.20], 1.0, 3, 0, corner, false), joint(prefix + "-damper-slide", corner + " air spring and damper stroke", JointKind.prismatic, "suspension", "suspension", prefix + "-damper-body", prefix + "-damper-rod", [0.0, damper_dy / damper_axis_length, damper_dz / damper_axis_length], [axle_x, sign_y * (HP_DAMPER_LO_Y + HP_DAMPER_UP_Y) * 0.5, (HP_DAMPER_LO_Z + HP_DAMPER_UP_Z) * 0.5], [-0.05296641106708366, 0.0603908906802505], 1.0, 1, 1, corner, false), joint(prefix + "-damper-lower", corner + " damper lower mount", JointKind.spherical, "suspension", "suspension", prefix + "-damper-rod", lower_arm, [0.0, 0.0, 1.0], [axle_x, sign_y * HP_DAMPER_LO_Y, HP_DAMPER_LO_Z], [-0.20, 0.20], 1.0, 3, 0, corner, true), joint(prefix + "-" + steer_slug + "-inner", corner + " " + steer_name + " inner joint", JointKind.spherical, steer_root_part, "suspension", steer_root, steer_link, [0.0, 0.0, 1.0], [steer_x, sign_y * HP_TIE_IN_Y, HP_TIE_IN_Z], [-0.60, 0.60], 1.0, 3, 1, corner, false), joint(prefix + "-" + steer_slug + "-outer", corner + " " + steer_name + " outer joint", JointKind.spherical, "suspension", "suspension", steer_link, knuckle, [0.0, 0.0, 1.0], [steer_x, sign_y * HP_TIE_OUT_Y, HP_TIE_OUT_Z], [-0.60, 0.60], 1.0, 3, 0, corner, true), joint(prefix + "-arb-bearing", corner + " anti-roll bar bearing", JointKind.cylindrical, subframe, "suspension", "chassis", axle + "-anti-roll-bar", [0.0, 1.0, 0.0], [axle_x + sign_x * HP_ARB_BAR_DX, sign_y * HP_ARB_BAR_Y, HP_ARB_BAR_Z], [-0.30, 0.30], 1.0, 2, 0, corner, false), joint(prefix + "-arb-link", corner + " anti-roll bar drop link", JointKind.revolute, "suspension", "suspension", axle + "-anti-roll-bar", lower_arm, [1.0, 0.0, 0.0], [axle_x + sign_x * HP_ARB_BAR_DX, sign_y * HP_ARB_LINK_Y, HP_ARB_LINK_Z], [-0.25, 0.25], 1.0, 1, 0, corner, true), joint(prefix + "-halfshaft-inboard", corner + " inboard constant-velocity joint", JointKind.spherical, "reduction-gears", "suspension", axle + "-final-drive", prefix + "-halfshaft", [0.0, sign_y, 0.0], [axle_x, sign_y * 0.190, HP_WC_Z], [-0.40, 0.40], 1.0, 3, 1, corner, false), joint(prefix + "-halfshaft-outboard", corner + " outboard constant-velocity joint", JointKind.spherical, "suspension", "suspension", prefix + "-halfshaft", hub, [0.0, sign_y, 0.0], [axle_x, sign_y * 0.700, HP_WC_Z], [-0.85, 0.85], 1.0, 3, 0, corner, false), joint(prefix + "-hub", corner + " wheel bearing", JointKind.revolute, "suspension", "suspension", knuckle, hub, [0.0, sign_y, 0.0], [axle_x, sign_y * HP_WC_Y, HP_WC_Z], [-6.283185307179586, 6.283185307179586], 1.0, 1, 0, corner, false), joint(prefix + "-brake-disc", corner + " brake disc mount", JointKind.fixed, "suspension", "brakes", hub, prefix + "-brake-disc", [0.0, sign_y, 0.0], [axle_x, sign_y * 0.760, HP_WC_Z], [0.0, 0.0], 1.0, 0, 0, corner, false), joint(prefix + "-brake-caliper", corner + " brake caliper mount", JointKind.fixed, "suspension", "brakes", knuckle, prefix + "-brake-caliper", [0.0, sign_y, 0.0], [axle_x + sign_x * 0.075, sign_y * 0.735, HP_WC_Z + 0.055], [0.0, 0.0], 1.0, 0, 0, corner, false), joint(prefix + "-wheel-mount", corner + " wheel bolt circle", JointKind.fixed, "suspension", "wheels", hub, prefix + "-wheel", [0.0, sign_y, 0.0], [axle_x, sign_y * 0.790, HP_WC_Z], [0.0, 0.0], 1.0, 0, 0, corner, false), joint(prefix + "-tire-bead", corner + " tire bead seat", JointKind.fixed, "wheels", "tires", prefix + "-wheel", prefix + "-tire", [0.0, sign_y, 0.0], [axle_x, sign_y * HP_WC_Y, HP_WC_Z], [0.0, 0.0], 1.0, 0, 0, corner, false), ]
def variant_joint_kind(configuration_variant: str): """Subframe and pack mounts are elastomeric bolted joints conventionally and bonded structural joints in the CircuitFrame variant.""" require configuration_variant == "conventional" or configuration_variant == "circuitframe" return JointKind.fixed if configuration_variant == "conventional" else JointKind.rigid_bond
def structural_joints(configuration_variant: str): """Driveline, body, pack, and equipment joints outside the four suspension corners.""" mount_kind = variant_joint_kind(configuration_variant) return [ joint("steering-rack-slide", "Steering rack translation", JointKind.prismatic, "steering", "steering", "chassis", "steering-rack", [0.0, 1.0, 0.0], [1.32, 0.0, 0.29], [-0.078, 0.078], 1.0, 1, 0, "vehicle", false), joint("front-rotor", "Front motor rotor bearing", JointKind.revolute, "front-motor", "front-motor", "front-motor-housing", "front-motor-rotor", [0.0, 1.0, 0.0], [1.56, 0.0, 0.33], [-6.283185307179586, 6.283185307179586], 1.0, 1, 0, "vehicle", false), joint("rear-rotor", "Rear motor rotor bearing", JointKind.revolute, "rear-motor", "rear-motor", "rear-motor-housing", "rear-motor-rotor", [0.0, 1.0, 0.0], [-1.56, 0.0, 0.345], [-6.283185307179586, 6.283185307179586], 1.0, 1, 0, "vehicle", false), joint("front-gear-stage-1", "Front reduction first stage", JointKind.gear, "front-motor", "reduction-gears", "front-motor-rotor", "front-intermediate-shaft", [0.0, 1.0, 0.0], [1.5, 0.14, 0.33], [-6.283185307179586, 6.283185307179586], 3.25, 1, 0, "vehicle", false), joint("front-gear-stage-2", "Front reduction second stage", JointKind.gear, "reduction-gears", "reduction-gears", "front-intermediate-shaft", "front-final-drive", [0.0, 1.0, 0.0], [1.47, 0.07, 0.335], [-6.283185307179586, 6.283185307179586], 2.8, 1, 0, "vehicle", false), joint("rear-gear-stage-1", "Rear reduction first stage", JointKind.gear, "rear-motor", "reduction-gears", "rear-motor-rotor", "rear-intermediate-shaft", [0.0, 1.0, 0.0], [-1.5, 0.14, 0.345], [-6.283185307179586, 6.283185307179586], 3.25, 1, 0, "vehicle", false), joint("rear-gear-stage-2", "Rear reduction second stage", JointKind.gear, "reduction-gears", "reduction-gears", "rear-intermediate-shaft", "rear-final-drive", [0.0, 1.0, 0.0], [-1.47, 0.07, 0.34], [-6.283185307179586, 6.283185307179586], 2.8, 1, 0, "vehicle", false), joint("front-subframe-mount", "Front subframe to body rails", mount_kind, "body-shell", "front-subframe", "body-structure", "front-subframe", [0.0, 0.0, 1.0], [1.445, 0.0, 0.3], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("rear-subframe-mount", "Rear subframe to body rails", mount_kind, "body-shell", "rear-subframe", "body-structure", "rear-subframe", [0.0, 0.0, 1.0], [-1.48, 0.0, 0.345], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("floor-pan-bond", "Structural floor pan to body", JointKind.rigid_bond, "body-shell", "floor-pan", "body-structure", "floor-pan", [0.0, 0.0, 1.0], [-0.05, 0.0, 0.235], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("sill-left-bond", "Left structural sill to floor and body", JointKind.rigid_bond, "floor-pan", "sill-panels", "floor-pan", "sill-left", [0.0, 0.0, 1.0], [0.0, 0.7725, 0.29], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("sill-right-bond", "Right structural sill to floor and body", JointKind.rigid_bond, "floor-pan", "sill-panels", "floor-pan", "sill-right", [0.0, 0.0, 1.0], [0.0, -0.7725, 0.29], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("pack-enclosure-mount", "Battery enclosure to floor and sills", mount_kind, "floor-pan", "battery-enclosure", "floor-pan", "battery-enclosure", [0.0, 0.0, 1.0], [-0.02, 0.0, 0.23], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("pack-module-mount", "Battery modules to enclosure rails", JointKind.fixed, "battery-enclosure", "battery-modules", "battery-enclosure", "battery-modules", [0.0, 0.0, 1.0], [-0.02, 0.0, 0.25], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("bms-mount", "Battery management to enclosure", JointKind.fixed, "battery-enclosure", "battery-management", "battery-enclosure", "battery-management", [0.0, 0.0, 1.0], [-0.62, 0.3, 0.31], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("front-motor-mount", "Front drive unit to subframe", JointKind.fixed, "front-subframe", "front-motor", "front-subframe", "front-motor-housing", [0.0, 0.0, 1.0], [1.56, 0.0, 0.33], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("rear-motor-mount", "Rear drive unit to subframe", JointKind.fixed, "rear-subframe", "rear-motor", "rear-subframe", "rear-motor-housing", [0.0, 0.0, 1.0], [-1.56, 0.0, 0.345], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("front-gearbox-mount", "Front gearbox housing to motor", JointKind.fixed, "front-motor", "reduction-gears", "front-motor-housing", "front-gearbox-housing", [0.0, 0.0, 1.0], [1.48, 0.13, 0.33], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("rear-gearbox-mount", "Rear gearbox housing to motor", JointKind.fixed, "rear-motor", "reduction-gears", "rear-motor-housing", "rear-gearbox-housing", [0.0, 0.0, 1.0], [-1.48, 0.13, 0.345], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("steering-rack-mount", "Steering rack housing to subframe", JointKind.fixed, "front-subframe", "steering", "front-subframe", "steering-rack-housing", [0.0, 0.0, 1.0], [1.3, 0.0, 0.33], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("front-inverter-mount", "Front inverter to drive unit", JointKind.fixed, "front-motor", "front-inverter", "front-motor-housing", "front-inverter", [0.0, 0.0, 1.0], [1.64, 0.18, 0.52], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("rear-inverter-mount", "Rear inverter to drive unit", JointKind.fixed, "rear-motor", "rear-inverter", "rear-motor-housing", "rear-inverter", [0.0, 0.0, 1.0], [-1.64, -0.18, 0.54], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("hood-hinge", "Hood hinge", JointKind.revolute, "body-shell", "closures", "body-structure", "hood", [0.0, 1.0, 0.0], [1.02, 0.0, 1.06], [0.0, 1.2], 1.0, 1, 0, "vehicle", false), joint("liftgate-hinge", "Liftgate hinge", JointKind.revolute, "body-shell", "closures", "body-structure", "liftgate", [0.0, 1.0, 0.0], [-1.93, 0.0, 1.56], [0.0, 1.45], 1.0, 1, 0, "vehicle", false), joint("door-hinge-front-left", "Front left door hinge", JointKind.revolute, "body-shell", "closures", "body-structure", "door-front-left", [0.0, 0.0, 1.0], [0.76, 0.86, 0.9], [0.0, 1.28], 1.0, 1, 0, "vehicle", false), joint("door-hinge-front-right", "Front right door hinge", JointKind.revolute, "body-shell", "closures", "body-structure", "door-front-right", [0.0, 0.0, 1.0], [0.76, -0.86, 0.9], [0.0, 1.28], 1.0, 1, 0, "vehicle", false), joint("door-hinge-rear-left", "Rear left door hinge", JointKind.revolute, "body-shell", "closures", "body-structure", "door-rear-left", [0.0, 0.0, 1.0], [-0.42, 0.86, 0.9], [0.0, 1.22], 1.0, 1, 0, "vehicle", false), joint("door-hinge-rear-right", "Rear right door hinge", JointKind.revolute, "body-shell", "closures", "body-structure", "door-rear-right", [0.0, 0.0, 1.0], [-0.42, -0.86, 0.9], [0.0, 1.22], 1.0, 1, 0, "vehicle", false), joint("seat-rail-slide", "Front seat longitudinal rail", JointKind.prismatic, "floor-pan", "seats", "floor-pan", "front-seat-frame", [1.0, 0.0, 0.0], [0.18, 0.38, 0.42], [-0.12, 0.12], 1.0, 1, 0, "vehicle", false), joint("glazing-bond", "Laminated glazing urethane bond", JointKind.rigid_bond, "body-shell", "glazing", "body-structure", "glazing", [0.0, 0.0, 1.0], [-0.15, 0.0, 1.19], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("interior-mount", "Interior and trim to body", JointKind.fixed, "body-shell", "interior", "body-structure", "interior", [0.0, 0.0, 1.0], [-0.12, 0.0, 0.79], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("restraints-mount", "Restraint anchorages to body", JointKind.fixed, "body-shell", "restraints", "body-structure", "restraints", [0.0, 0.0, 1.0], [0.15, 0.0, 1.02], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("lamps-mount", "Exterior lighting to body apertures", JointKind.fixed, "body-shell", "lamps", "body-structure", "lamps", [0.0, 0.0, 1.0], [0.05, 0.0, 0.83], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("sensors-mount", "Sensing suite to body bosses", JointKind.fixed, "body-shell", "sensors", "body-structure", "sensors", [0.0, 0.0, 1.0], [0.2, 0.0, 0.98], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("hv-junction-mount", "HV junction unit to pack", JointKind.fixed, "battery-enclosure", "hv-junction", "battery-enclosure", "hv-junction", [0.0, 0.0, 1.0], [-0.98, 0.42, 0.36], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("charge-port-mount", "Charge port to body aperture", JointKind.fixed, "body-shell", "charge-port", "body-structure", "charge-port", [0.0, 0.0, 1.0], [-2.18, 0.76, 0.76], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("onboard-charger-mount", "On-board charger to floor", JointKind.fixed, "floor-pan", "onboard-charger", "floor-pan", "onboard-charger", [0.0, 0.0, 1.0], [-1.7, -0.43, 0.42], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("dc-dc-mount", "DC/DC converter to body bracket", JointKind.fixed, "body-shell", "dc-dc", "body-structure", "dc-dc", [0.0, 0.0, 1.0], [1.32, -0.4, 0.52], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("lv-battery-mount", "Low-voltage battery tray", JointKind.fixed, "body-shell", "lv-battery", "body-structure", "lv-battery", [0.0, 0.0, 1.0], [1.68, -0.56, 0.44], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("central-compute-mount", "Central compute to body bracket", JointKind.fixed, "body-shell", "central-compute", "body-structure", "central-compute", [0.0, 0.0, 1.0], [0.88, 0.0, 0.64], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("front-zone-controller-mount", "Front zone controller bracket", JointKind.fixed, "body-shell", "front-zone-controller", "body-structure", "front-zone-controller", [0.0, 0.0, 1.0], [1.4, 0.56, 0.64], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("rear-zone-controller-mount", "Rear zone controller bracket", JointKind.fixed, "body-shell", "rear-zone-controller", "body-structure", "rear-zone-controller", [0.0, 0.0, 1.0], [-1.52, -0.52, 0.62], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("cabin-zone-controller-mount", "Cabin zone controller bracket", JointKind.fixed, "interior", "cabin-zone-controller", "interior", "cabin-zone-controller", [0.0, 0.0, 1.0], [0.56, 0.38, 0.62], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("heat-pump-mount", "Heat pump to front subframe", JointKind.fixed, "front-subframe", "heat-pump", "front-subframe", "heat-pump", [0.0, 0.0, 1.0], [1.82, 0.24, 0.56], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("thermal-loop-mount", "Coolant loop routing to body", JointKind.fixed, "body-shell", "thermal-loop", "body-structure", "thermal-loop", [0.0, 0.0, 1.0], [0.15, 0.0, 0.43], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("service-links-mount", "Serviceable harness set to body", JointKind.fixed, "body-shell", "service-links", "body-structure", "service-links", [0.0, 0.0, 1.0], [0.05, 0.0, 0.56], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), joint("fasteners-seals-bond", "Closure seal and joining family", JointKind.rigid_bond, "closures", "fasteners-seals", "door-front-left", "closure-seal-set", [0.0, 0.0, 1.0], [0.76, 0.86, 0.9], [0.0, 0.0], 1.0, 0, 0, "vehicle", false), ]
def vehicle_joints_for(configuration_variant: str): """Complete kinematic joint set for one configuration variant.""" require configuration_variant == "conventional" or configuration_variant == "circuitframe" mut joints = [] for entry in corner_table(): for corner_joint in corner_joints(entry): joints.append(corner_joint) for structural in structural_joints(configuration_variant): joints.append(structural) return joints
def vehicle_joints(): """Complete kinematic joint set for the default CircuitFrame configuration.""" return vehicle_joints_for(CONFIGURATION_VARIANT_DEFAULT)
def corner_loop_joint_ids(entry: dict[str, any]): """The joints that form the closed kinematic loop of one corner.
Wheel bearing, brake, wheel, tire, halfshaft, and anti-roll-bar joints are excluded: they extend the corner as an open chain or couple the two corners of an axle, and are analysed separately. """ prefix = entry["prefix"] steer_slug = "tie-rod" if entry["steerable"] else "toe-link" mut ids = [ prefix + "-lca-inboard", prefix + "-uca-inboard", prefix + "-lca-outboard", prefix + "-uca-outboard", prefix + "-damper-top", prefix + "-damper-slide", prefix + "-damper-lower", prefix + "-" + steer_slug + "-inner", prefix + "-" + steer_slug + "-outer", ] if entry["steerable"]: ids = ids + ["steering-rack-slide"] return ids
def corner_mobility(entry: dict[str, any]) !{}: """Grubler-Kutzbach mobility of one suspension corner loop.
Gross mobility counts every joint freedom; the idle freedoms are the spins of the spherical-spherical steering link and of the spherical-prismatic-spherical damper unit, which move no wheel state. A steered corner must retain one wheel-travel and one steer freedom; a rear corner must retain wheel travel only. """ loop_ids = corner_loop_joint_ids(entry) joints = vehicle_joints() mut links = [] mut dof_sum = 0 mut idle_sum = 0 mut loop_closing = [] mut found = 0 for candidate in joints: mut selected = false for loop_id in loop_ids: if candidate.id == loop_id: selected = true if selected: found = found + 1 dof_sum = dof_sum + candidate.dof idle_sum = idle_sum + candidate.idle_dof if candidate.loop_closing: loop_closing = loop_closing + [candidate.id] for link in [candidate.parent_link, candidate.child_link]: mut seen = false for known in links: if known == link: seen = true if not seen: links = links + [link] gross = grubler_mobility(len(links), found, dof_sum) expected = 2 if entry["steerable"] else 1 effective = gross - idle_sum return MobilityReport( corner_id=entry["corner"], link_count=len(links), joint_count=found, joint_dof_sum=dof_sum, gross_mobility=gross, idle_dof=idle_sum, effective_mobility=effective, expected_mobility=expected, independent_loops=found - len(links) + 1, loop_closing_joint_ids=loop_closing, ok=effective == expected and found == len(loop_ids), )
def mobility_reports() !{}: """Mobility of all four suspension corner loops.""" mut reports = [] for entry in corner_table(): reports.append(corner_mobility(entry)) return reports
pub def arm_rotation_limits() -> list[f64] !{}: """Lower control arm rotation at the rebound and jounce stops, from the declared wheel travel.""" return [-0.18232180232086856, 0.2017847836373893]
pub def corner_pose(arm_rotation_rad: f64) -> list[f64] !{}: """Closes the planar double-wishbone four-bar for a given lower-arm rotation.
Returns [lbj_y, lbj_z, ubj_y, ubj_z, knuckle_rotation_rad, wheel_y, wheel_z, wheel_travel_m]. The upper ball joint is the circle-circle intersection of the upper arm sweep and the rigid knuckle, so wheel travel and camber are consequences of the joint set, not of a fitted curve. """ lower_dy = HP_LBJ_Y - HP_LCA_IN_Y lower_dz = HP_LBJ_Z - HP_LCA_IN_Z upper_dy = HP_UBJ_Y - HP_UCA_IN_Y upper_dz = HP_UBJ_Z - HP_UCA_IN_Z knuckle_dy = HP_UBJ_Y - HP_LBJ_Y knuckle_dz = HP_UBJ_Z - HP_LBJ_Z lower_length = math.sqrt(lower_dy * lower_dy + lower_dz * lower_dz) upper_length = math.sqrt(upper_dy * upper_dy + upper_dz * upper_dz) knuckle_span = math.sqrt(knuckle_dy * knuckle_dy + knuckle_dz * knuckle_dz) angle = math.atan2(lower_dz, lower_dy) + arm_rotation_rad lbj_y = HP_LCA_IN_Y + lower_length * math.cos(angle) lbj_z = HP_LCA_IN_Z + lower_length * math.sin(angle) span_y = lbj_y - HP_UCA_IN_Y span_z = lbj_z - HP_UCA_IN_Z span = math.sqrt(span_y * span_y + span_z * span_z) projection = (upper_length * upper_length - knuckle_span * knuckle_span + span * span) / (2.0 * span) offset = math.sqrt(max(0.0, upper_length * upper_length - projection * projection)) mid_y = HP_UCA_IN_Y + projection * span_y / span mid_z = HP_UCA_IN_Z + projection * span_z / span first_y = mid_y - offset * span_z / span first_z = mid_z + offset * span_y / span second_y = mid_y + offset * span_z / span second_z = mid_z - offset * span_y / span first_error = (first_y - HP_UBJ_Y) * (first_y - HP_UBJ_Y) + (first_z - HP_UBJ_Z) * (first_z - HP_UBJ_Z) second_error = (second_y - HP_UBJ_Y) * (second_y - HP_UBJ_Y) + (second_z - HP_UBJ_Z) * (second_z - HP_UBJ_Z) ubj_y = first_y if first_error <= second_error else second_y ubj_z = first_z if first_error <= second_error else second_z knuckle_rotation = math.atan2(ubj_z - lbj_z, ubj_y - lbj_y) - math.atan2(knuckle_dz, knuckle_dy) arm_y = HP_WC_Y - HP_LBJ_Y arm_z = HP_WC_Z - HP_LBJ_Z cosine = math.cos(knuckle_rotation) sine = math.sin(knuckle_rotation) wheel_y = lbj_y + cosine * arm_y - sine * arm_z wheel_z = lbj_z + sine * arm_y + cosine * arm_z return [lbj_y, lbj_z, ubj_y, ubj_z, knuckle_rotation, wheel_y, wheel_z, wheel_z - HP_WC_Z]
def damper_length(arm_rotation_rad: f64): """Installed length of the air spring and damper unit for a given lower-arm rotation.""" mount_dy = HP_DAMPER_LO_Y - HP_LCA_IN_Y mount_dz = HP_DAMPER_LO_Z - HP_LCA_IN_Z radius = math.sqrt(mount_dy * mount_dy + mount_dz * mount_dz) angle = math.atan2(mount_dz, mount_dy) + arm_rotation_rad mount_y = HP_LCA_IN_Y + radius * math.cos(angle) mount_z = HP_LCA_IN_Z + radius * math.sin(angle) return math.sqrt((HP_DAMPER_UP_Y - mount_y) * (HP_DAMPER_UP_Y - mount_y) + (HP_DAMPER_UP_Z - mount_z) * (HP_DAMPER_UP_Z - mount_z))
def motion_ratio(arm_rotation_rad: f64): """Spring shortening per unit wheel rise, differentiated through the linkage.""" step = 0.00001 length_change = damper_length(arm_rotation_rad + step) - damper_length(arm_rotation_rad - step) travel_change = corner_pose(arm_rotation_rad + step)[7] - corner_pose(arm_rotation_rad - step)[7] return -length_change / travel_change
def static_corner_load_n(): """Static vertical load at one wheel with the vehicle at its declared kerb mass.""" return ASSEMBLY_MASS_KG * GRAVITY_MPS2 / 4.0
pub def air_spring_reference_pressure() -> f64 !{}: """Absolute bellows pressure that balances the static corner load at design height.""" return static_corner_load_n() / (motion_ratio(0.0) * AIR_SPRING_AREA_M2) + ATMOSPHERIC_PRESSURE_PA
def air_spring_force(arm_rotation_rad: f64, reference_pressure: f64) !{}: """Axial air spring force from the polytropic gas state of the bellows.""" stroke = damper_length(0.0) - damper_length(arm_rotation_rad) volume = AIR_SPRING_VOLUME_M3 - AIR_SPRING_AREA_M2 * stroke pressure = polytropic_pressure(reference_pressure, AIR_SPRING_VOLUME_M3, volume, AIR_SPRING_POLYTROPIC_N) return AIR_SPRING_AREA_M2 * (pressure - ATMOSPHERIC_PRESSURE_PA)
pub def wheel_force(arm_rotation_rad: f64, reference_pressure: f64) -> f64 !{}: """Vertical wheel force supported by the corner, by virtual work through the motion ratio.""" return air_spring_force(arm_rotation_rad, reference_pressure) * motion_ratio(arm_rotation_rad)
def vector_rotate(vector: list[f64], axis: list[f64], angle: f64): """Rodrigues rotation of a vector about a unit axis.""" cosine = math.cos(angle) sine = math.sin(angle) projection = axis[0] * vector[0] + axis[1] * vector[1] + axis[2] * vector[2] return [ vector[0] * cosine + (axis[1] * vector[2] - axis[2] * vector[1]) * sine + axis[0] * projection * (1.0 - cosine), vector[1] * cosine + (axis[2] * vector[0] - axis[0] * vector[2]) * sine + axis[1] * projection * (1.0 - cosine), vector[2] * cosine + (axis[0] * vector[1] - axis[1] * vector[0]) * sine + axis[2] * projection * (1.0 - cosine), ]
def tie_rod_residual(pose: list[f64], steer_rotation_rad: f64): """Length error of the steering link when the knuckle is rotated about its kingpin axis.""" kingpin_y = pose[2] - pose[0] kingpin_z = pose[3] - pose[1] kingpin_length = math.sqrt(HP_KINGPIN_DX * HP_KINGPIN_DX + kingpin_y * kingpin_y + kingpin_z * kingpin_z) axis = [HP_KINGPIN_DX / kingpin_length, kingpin_y / kingpin_length, kingpin_z / kingpin_length] cosine = math.cos(pose[4]) sine = math.sin(pose[4]) offset_y = HP_TIE_OUT_Y - HP_LBJ_Y offset_z = HP_TIE_OUT_Z - HP_LBJ_Z carried = [HP_TIE_DX, cosine * offset_y - sine * offset_z, sine * offset_y + cosine * offset_z] turned = vector_rotate(carried, axis, steer_rotation_rad) delta_x = turned[0] - HP_TIE_DX delta_y = pose[0] + turned[1] - HP_TIE_IN_Y delta_z = pose[1] + turned[2] - HP_TIE_IN_Z reference_y = HP_TIE_OUT_Y - HP_TIE_IN_Y reference_z = HP_TIE_OUT_Z - HP_TIE_IN_Z return math.sqrt(delta_x * delta_x + delta_y * delta_y + delta_z * delta_z) - math.sqrt(reference_y * reference_y + reference_z * reference_z)
def bump_steer(pose: list[f64]): """Kingpin rotation forced by the fixed-length steering link, and the toe change it produces.
Returns [kingpin_rotation_rad, toe_change_deg]. Solved with a damped Newton iteration seeded at the design position, so it tracks the physically near root of the length constraint. """ kingpin_y = pose[2] - pose[0] kingpin_z = pose[3] - pose[1] kingpin_length = math.sqrt(HP_KINGPIN_DX * HP_KINGPIN_DX + kingpin_y * kingpin_y + kingpin_z * kingpin_z) vertical_component = kingpin_z / kingpin_length mut rotation = 0.0 mut index = 0 while index < STEER_SOLVE_ITERATIONS: step = 0.000001 slope = (tie_rod_residual(pose, rotation + step) - tie_rod_residual(pose, rotation - step)) / (2.0 * step) if abs(slope) < 0.000000000001: index = STEER_SOLVE_ITERATIONS else: correction = tie_rod_residual(pose, rotation) / slope rotation = rotation - max(-0.05, min(0.05, correction)) index = index + 1 return [rotation, rotation * vertical_component * 180.0 / math.pi]
pub def suspension_travel(vertical_load_n: f64) -> SuspensionState !{}: """Solves the corner for a vertical wheel load and reports the resulting wheel motion.
The load is balanced against the polytropic air spring through the linkage motion ratio; the lower-arm rotation that satisfies it is bisected between the rebound and jounce stops. Camber and toe are then read out of the four-bar closure and the steering-link length constraint, so the wheel pose is a consequence of the declared joints. """ require vertical_load_n >= 0.0 and vertical_load_n <= 40000.0 limits = arm_rotation_limits() reference_pressure = air_spring_reference_pressure() mut low = limits[0] mut high = limits[1] if vertical_load_n <= wheel_force(low, reference_pressure): high = low elif vertical_load_n >= wheel_force(high, reference_pressure): low = high else: mut index = 0 while index < TRAVEL_SOLVE_ITERATIONS: middle = (low + high) * 0.5 if wheel_force(middle, reference_pressure) < vertical_load_n: low = middle else: high = middle index = index + 1 rotation = (low + high) * 0.5 pose = corner_pose(rotation) steer = bump_steer(pose) travel = pose[7] step = 0.00001 rate = (wheel_force(rotation + step, reference_pressure) - wheel_force(rotation - step, reference_pressure)) / (corner_pose(rotation + step)[7] - corner_pose(rotation - step)[7]) camber_change = -pose[4] * 180.0 / math.pi utilisation = travel / JOUNCE_LIMIT_M if travel >= 0.0 else travel / REBOUND_LIMIT_M return SuspensionState( corner_id="front-left", vertical_load_n=vertical_load_n, arm_rotation_rad=rotation, wheel_travel_m=travel, spring_stroke_m=damper_length(0.0) - damper_length(rotation), damper_length_m=damper_length(rotation), spring_force_n=air_spring_force(rotation, reference_pressure), wheel_rate_n_m=rate, motion_ratio=motion_ratio(rotation), camber_deg=STATIC_CAMBER_DEG + camber_change, camber_change_deg=camber_change, toe_deg=STATIC_TOE_DEG + steer[1], toe_change_deg=steer[1], kingpin_rotation_rad=steer[0], jounce_limit_m=JOUNCE_LIMIT_M, rebound_limit_m=REBOUND_LIMIT_M, travel_utilisation=min(1.0, max(0.0, utilisation)), at_limit=travel >= JOUNCE_LIMIT_M or travel <= REBOUND_LIMIT_M, )
def load_path_declarations(): """Declared load paths from load introduction to structural reaction, in transmission order.
Utilisation is left at zero here; load_paths() fills it from the reference static load case. """ return [ LoadPath(id="lp-vertical-corner", name="Vertical wheel load to body and pack rails", kind=LoadPathKind.vertical, part_ids=["tires", "wheels", "brakes", "suspension", "front-subframe", "body-shell", "battery-enclosure"], rated_load_n=16000.0, utilisation=0.0), LoadPath(id="lp-braking-longitudinal", name="Braking reaction to subframe and floor", kind=LoadPathKind.longitudinal, part_ids=["tires", "wheels", "brakes", "suspension", "front-subframe", "floor-pan", "body-shell"], rated_load_n=26000.0, utilisation=0.0), LoadPath(id="lp-cornering-lateral", name="Cornering side force to sills and pack", kind=LoadPathKind.lateral, part_ids=["tires", "wheels", "suspension", "front-subframe", "sill-panels", "battery-enclosure", "body-shell"], rated_load_n=16000.0, utilisation=0.0), LoadPath(id="lp-pack-stressed-member", name="Battery pack as a stressed structural member", kind=LoadPathKind.structural, part_ids=["sill-panels", "battery-enclosure", "battery-modules", "floor-pan", "body-shell"], rated_load_n=90000.0, utilisation=0.0), LoadPath(id="lp-front-crash-rail", name="Front crash rail crush path", kind=LoadPathKind.crash, part_ids=["body-shell", "front-subframe", "sill-panels", "floor-pan", "battery-enclosure"], rated_load_n=420000.0, utilisation=0.0), LoadPath(id="lp-rear-crash-rail", name="Rear crash rail crush path", kind=LoadPathKind.crash, part_ids=["body-shell", "rear-subframe", "sill-panels", "floor-pan"], rated_load_n=300000.0, utilisation=0.0), LoadPath(id="lp-belt-anchorage", name="Seat belt anchorage reaction", kind=LoadPathKind.restraint, part_ids=["restraints", "seats", "floor-pan", "sill-panels", "body-shell"], rated_load_n=13500.0, utilisation=0.0), ]
def corner_statics(lateral_n: f64, vertical_n: f64) !{}: """Static equilibrium of one corner at the design position.
Two 3x3 solves: the knuckle against the lower ball joint reaction and the axial upper-arm force, then the lower arm against the damper force and its inboard pivot reaction. Returns [lower_ball_y, lower_ball_z, upper_arm_axial, damper_axial, pivot_y, pivot_z, chassis_y, chassis_z, force_residual, moment_residual]. """ upper_dy = HP_UBJ_Y - HP_UCA_IN_Y upper_dz = HP_UBJ_Z - HP_UCA_IN_Z upper_length = math.sqrt(upper_dy * upper_dy + upper_dz * upper_dz) upper_unit_y = upper_dy / upper_length upper_unit_z = upper_dz / upper_length damper_dy = HP_DAMPER_UP_Y - HP_DAMPER_LO_Y damper_dz = HP_DAMPER_UP_Z - HP_DAMPER_LO_Z damper_length_static = math.sqrt(damper_dy * damper_dy + damper_dz * damper_dz) damper_unit_y = damper_dy / damper_length_static damper_unit_z = damper_dz / damper_length_static knuckle_matrix = [ [1.0, 0.0, upper_unit_y], [0.0, 1.0, upper_unit_z], [0.0, 0.0, (HP_UBJ_Y - HP_LBJ_Y) * upper_unit_z - (HP_UBJ_Z - HP_LBJ_Z) * upper_unit_y], ] knuckle_rhs = [ -lateral_n, -vertical_n, -((HP_WC_Y - HP_LBJ_Y) * vertical_n - (0.0 - HP_LBJ_Z) * lateral_n), ] knuckle = linear_solve_3(knuckle_matrix, knuckle_rhs) lower_ball_y = knuckle[0] lower_ball_z = knuckle[1] upper_axial = knuckle[2] arm_matrix = [ [damper_unit_y, 1.0, 0.0], [damper_unit_z, 0.0, 1.0], [(HP_DAMPER_LO_Y - HP_LCA_IN_Y) * damper_unit_z - (HP_DAMPER_LO_Z - HP_LCA_IN_Z) * damper_unit_y, 0.0, 0.0], ] arm_rhs = [ lower_ball_y, lower_ball_z, (HP_LBJ_Y - HP_LCA_IN_Y) * lower_ball_z - (HP_LBJ_Z - HP_LCA_IN_Z) * lower_ball_y, ] arm = linear_solve_3(arm_matrix, arm_rhs) damper_axial = arm[0] pivot_y = arm[1] pivot_z = arm[2] chassis_y = -pivot_y - upper_axial * upper_unit_y - damper_axial * damper_unit_y chassis_z = -pivot_z - upper_axial * upper_unit_z - damper_axial * damper_unit_z applied_moment = (HP_WC_Y - HP_LCA_IN_Y) * vertical_n - (0.0 - HP_LCA_IN_Z) * lateral_n reaction_moment = (HP_UCA_IN_Y - HP_LCA_IN_Y) * (-upper_axial * upper_unit_z) - (HP_UCA_IN_Z - HP_LCA_IN_Z) * (-upper_axial * upper_unit_y) + (HP_DAMPER_UP_Y - HP_LCA_IN_Y) * (-damper_axial * damper_unit_z) - (HP_DAMPER_UP_Z - HP_LCA_IN_Z) * (-damper_axial * damper_unit_y) return [ lower_ball_y, lower_ball_z, upper_axial, damper_axial, pivot_y, pivot_z, chassis_y, chassis_z, math.sqrt((chassis_y - lateral_n) * (chassis_y - lateral_n) + (chassis_z - vertical_n) * (chassis_z - vertical_n)), abs(applied_moment - reaction_moment), ]
def load_paths() !{}: """Declared load paths with the utilisation each reaches under the reference static load case.""" evaluated = load_path_reactions(static_corner_load_n(), REFERENCE_BRAKING_N, REFERENCE_CORNERING_N) declarations = load_path_declarations() mut paths = [] for index in range(0, len(declarations)): declaration = declarations[index] paths.append(LoadPath(id=declaration.id, name=declaration.name, kind=declaration.kind, part_ids=declaration.part_ids, rated_load_n=declaration.rated_load_n, utilisation=evaluated.results[index].utilisation)) return paths
def reaction(path_id: str, index: int, part_id: str, role: str, force_n: f64, moment_nm: f64): """One element of a load path chain.""" return LoadPathReaction(path_id=path_id, element_index=index, part_id=part_id, role=role, force_n=force_n, moment_nm=moment_nm)
def shared_path_result(path: LoadPath, applied_n: f64, shares: list[f64], roles: list[str]): """Distributes an applied load over a branching path whose shares sum to one.""" require len(shares) == len(path.part_ids) require len(roles) == len(path.part_ids) mut reactions = [] mut total = 0.0 mut peak = 0.0 for index in range(0, len(shares)): element = applied_n * shares[index] total = total + element peak = max(peak, abs(element)) reactions.append(reaction(path.id, index, path.part_ids[index], roles[index], element, 0.0)) return LoadPathResult( path_id=path.id, name=path.name, kind=path.kind, applied_n=applied_n, reaction_n=total, residual_n=abs(applied_n - total), peak_force_n=peak, rated_load_n=path.rated_load_n, utilisation=peak / path.rated_load_n, reactions=reactions, )
def load_path_reactions(vertical_n: f64, longitudinal_n: f64, lateral_n: f64) !{}: """Reactions along every declared load path for one static load case.
The vertical and lateral corner paths are closed by the two 3x3 equilibrium solves in corner_statics; the remaining paths distribute their input over branch shares that sum to one. Every path reports the residual between the applied load and the summed reaction. """ require vertical_n >= 0.0 and vertical_n <= 40000.0 require longitudinal_n >= -60000.0 and longitudinal_n <= 60000.0 require lateral_n >= -60000.0 and lateral_n <= 60000.0 paths = load_path_declarations() braking = abs(longitudinal_n) cornering = abs(lateral_n) vertical_solve = corner_statics(0.0, vertical_n) lateral_solve = corner_statics(lateral_n, vertical_n) lower_ball = math.sqrt(vertical_solve[0] * vertical_solve[0] + vertical_solve[1] * vertical_solve[1]) pivot = math.sqrt(vertical_solve[4] * vertical_solve[4] + vertical_solve[5] * vertical_solve[5]) chassis = math.sqrt(vertical_solve[6] * vertical_solve[6] + vertical_solve[7] * vertical_solve[7]) lateral_ball = math.sqrt(lateral_solve[0] * lateral_solve[0] + lateral_solve[1] * lateral_solve[1]) lateral_pivot = math.sqrt(lateral_solve[4] * lateral_solve[4] + lateral_solve[5] * lateral_solve[5]) vertical_path = paths[0] vertical_reactions = [ reaction(vertical_path.id, 0, "tires", "contact patch", vertical_n, 0.0), reaction(vertical_path.id, 1, "wheels", "rim to hub face", vertical_n, 0.0), reaction(vertical_path.id, 2, "brakes", "hub bearing", vertical_n, 0.0), reaction(vertical_path.id, 3, "suspension", "lower ball joint and upper arm", max(lower_ball, abs(vertical_solve[2])), 0.0), reaction(vertical_path.id, 4, "front-subframe", "control arm pivot", pivot, 0.0), reaction(vertical_path.id, 5, "body-shell", "damper tower", abs(vertical_solve[3]), 0.0), reaction(vertical_path.id, 6, "battery-enclosure", "pack rail reaction", chassis, 0.0), ] mut vertical_peak = 0.0 for entry in vertical_reactions: vertical_peak = max(vertical_peak, abs(entry.force_n)) vertical_result = LoadPathResult(path_id=vertical_path.id, name=vertical_path.name, kind=vertical_path.kind, applied_n=vertical_n, reaction_n=chassis, residual_n=vertical_solve[8], peak_force_n=vertical_peak, rated_load_n=vertical_path.rated_load_n, utilisation=vertical_peak / vertical_path.rated_load_n, reactions=vertical_reactions) braking_path = paths[1] anti_dive_couple = braking * WHEEL_RADIUS_M / (HP_UBJ_Z - HP_LBJ_Z) braking_reactions = [ reaction(braking_path.id, 0, "tires", "contact patch", braking, 0.0), reaction(braking_path.id, 1, "wheels", "rim to hub face", braking, 0.0), reaction(braking_path.id, 2, "brakes", "caliper torque reaction", braking * WHEEL_RADIUS_M / 0.150, braking * WHEEL_RADIUS_M), reaction(braking_path.id, 3, "suspension", "arm couple against wheel torque", anti_dive_couple, braking * WHEEL_RADIUS_M), reaction(braking_path.id, 4, "front-subframe", "fore-aft bush reaction", braking, 0.0), reaction(braking_path.id, 5, "floor-pan", "front rail shear", braking * 0.5, 0.0), reaction(braking_path.id, 6, "body-shell", "front rail shear", braking * 0.5, 0.0), ] mut braking_peak = 0.0 for entry in braking_reactions: braking_peak = max(braking_peak, abs(entry.force_n)) braking_result = LoadPathResult(path_id=braking_path.id, name=braking_path.name, kind=braking_path.kind, applied_n=braking, reaction_n=braking * 0.5 + braking * 0.5, residual_n=abs(braking - (braking * 0.5 + braking * 0.5)), peak_force_n=braking_peak, rated_load_n=braking_path.rated_load_n, utilisation=braking_peak / braking_path.rated_load_n, reactions=braking_reactions) lateral_path = paths[2] lateral_reactions = [ reaction(lateral_path.id, 0, "tires", "contact patch side force", cornering, 0.0), reaction(lateral_path.id, 1, "wheels", "bead seat shear", cornering, 0.0), reaction(lateral_path.id, 2, "suspension", "ball joint resultant", lateral_ball, 0.0), reaction(lateral_path.id, 3, "front-subframe", "control arm pivot", lateral_pivot, 0.0), reaction(lateral_path.id, 4, "sill-panels", "sill shear panel", cornering * 0.5, 0.0), reaction(lateral_path.id, 5, "battery-enclosure", "pack cross member", cornering * 0.25, 0.0), reaction(lateral_path.id, 6, "body-shell", "floor cross member", cornering * 0.25, 0.0), ] mut lateral_peak = 0.0 for entry in lateral_reactions: lateral_peak = max(lateral_peak, abs(entry.force_n)) lateral_total = cornering * 0.5 + cornering * 0.25 + cornering * 0.25 lateral_result = LoadPathResult(path_id=lateral_path.id, name=lateral_path.name, kind=lateral_path.kind, applied_n=cornering, reaction_n=lateral_total, residual_n=abs(cornering - lateral_total), peak_force_n=lateral_peak, rated_load_n=lateral_path.rated_load_n, utilisation=lateral_peak / lateral_path.rated_load_n, reactions=lateral_reactions) pack_result = shared_path_result(paths[3], vertical_n * 4.0 * 0.5, [0.5, 0.25, 0.125, 0.0625, 0.0625], ["sill shear panel", "enclosure top and bottom skin", "module stack shear", "floor tunnel", "body ring"]) front_crash_result = shared_path_result(paths[4], braking, [0.375, 0.1875, 0.25, 0.125, 0.0625], ["upper rail", "subframe crush can", "sill front horn", "floor front rail", "pack front bumper beam"]) rear_crash_result = shared_path_result(paths[5], braking, [0.4375, 0.1875, 0.25, 0.125], ["upper rail", "subframe crush can", "sill rear horn", "floor rear rail"]) belt_result = shared_path_result(paths[6], paths[6].rated_load_n, [0.5, 0.25, 0.125, 0.0625, 0.0625], ["upper anchorage", "buckle anchorage", "floor anchor plate", "sill reinforcement", "pillar ring"]) results = [vertical_result, braking_result, lateral_result, pack_result, front_crash_result, rear_crash_result, belt_result] mut residual = 0.0 mut worst_id = results[0].path_id mut worst = 0.0 for path_result in results: residual = max(residual, path_result.residual_n) if path_result.utilisation > worst: worst = path_result.utilisation worst_id = path_result.path_id return LoadCase( vertical_n=vertical_n, longitudinal_n=longitudinal_n, lateral_n=lateral_n, results=results, equilibrium_residual_n=residual, moment_residual_nm=max(vertical_solve[9], lateral_solve[9]), worst_path_id=worst_id, worst_utilisation=worst, )
def thermal_network(): """Coupled lumped thermal network: the coolant loop is a shared node, not a fixed sink.
Heat leaves a component into the glycol loop, travels to the chiller and the cabin through the heat pump, and only then reaches ambient through the radiator and the body skin. """ return ThermalNetwork( id="circuitframe-thermal-v1", nodes=[ ThermalNode(id="tn-battery", part_id="battery-modules", capacity_kj_k=540.0, ambient_coupling_w_k=6.0, initial_temp_c=24.0), ThermalNode(id="tn-front-motor", part_id="front-motor", capacity_kj_k=27.0, ambient_coupling_w_k=3.5, initial_temp_c=26.0), ThermalNode(id="tn-rear-motor", part_id="rear-motor", capacity_kj_k=33.0, ambient_coupling_w_k=3.8, initial_temp_c=26.0), ThermalNode(id="tn-front-inverter", part_id="front-inverter", capacity_kj_k=7.7, ambient_coupling_w_k=1.6, initial_temp_c=25.0), ThermalNode(id="tn-rear-inverter", part_id="rear-inverter", capacity_kj_k=8.4, ambient_coupling_w_k=1.7, initial_temp_c=25.0), ThermalNode(id="tn-cabin", part_id="interior", capacity_kj_k=160.0, ambient_coupling_w_k=42.0, initial_temp_c=22.0), ThermalNode(id="tn-coolant", part_id="thermal-loop", capacity_kj_k=60.0, ambient_coupling_w_k=260.0, initial_temp_c=24.0), ThermalNode(id="tn-heat-pump", part_id="heat-pump", capacity_kj_k=22.0, ambient_coupling_w_k=4.0, initial_temp_c=24.0), ], links=[ ThermalLink(id="tl-battery-coolant", from_node_id="tn-battery", to_node_id="tn-coolant", conductance_w_k=900.0, medium="glycol cold plate"), ThermalLink(id="tl-front-motor-coolant", from_node_id="tn-front-motor", to_node_id="tn-coolant", conductance_w_k=420.0, medium="stator jacket"), ThermalLink(id="tl-rear-motor-coolant", from_node_id="tn-rear-motor", to_node_id="tn-coolant", conductance_w_k=480.0, medium="stator jacket"), ThermalLink(id="tl-front-inverter-coolant", from_node_id="tn-front-inverter", to_node_id="tn-coolant", conductance_w_k=260.0, medium="power module cold plate"), ThermalLink(id="tl-rear-inverter-coolant", from_node_id="tn-rear-inverter", to_node_id="tn-coolant", conductance_w_k=280.0, medium="power module cold plate"), ThermalLink(id="tl-coolant-heat-pump", from_node_id="tn-coolant", to_node_id="tn-heat-pump", conductance_w_k=700.0, medium="chiller plate heat exchanger"), ThermalLink(id="tl-heat-pump-cabin", from_node_id="tn-heat-pump", to_node_id="tn-cabin", conductance_w_k=520.0, medium="cabin condenser and evaporator"), ThermalLink(id="tl-cabin-battery", from_node_id="tn-cabin", to_node_id="tn-battery", conductance_w_k=55.0, medium="floor conduction"), ThermalLink(id="tl-front-inverter-motor", from_node_id="tn-front-inverter", to_node_id="tn-front-motor", conductance_w_k=34.0, medium="shared drive unit housing"), ThermalLink(id="tl-rear-inverter-motor", from_node_id="tn-rear-inverter", to_node_id="tn-rear-motor", conductance_w_k=36.0, medium="shared drive unit housing"), ], )
def isolated_thermal_network(): """The same graph with every ambient coupling removed, for closed-system energy checks.""" source = thermal_network() mut nodes = [] for node in source.nodes: nodes.append(ThermalNode(id=node.id, part_id=node.part_id, capacity_kj_k=node.capacity_kj_k, ambient_coupling_w_k=0.0, initial_temp_c=node.initial_temp_c)) return ThermalNetwork(id=source.id + "-isolated", nodes=nodes, links=source.links)
def node_index(network: ThermalNetwork, node_id: str): """Position of a node in the network ordering, or -1 when it is absent.""" for index in range(0, len(network.nodes)): if network.nodes[index].id == node_id: return index return -1
def thermal_step_network(network: ThermalNetwork, temps: list[f64], heat_w: list[f64], ambient_c: f64, dt_s: f64): """Advances an explicit lumped-capacitance network one step.
C_i dT_i/dt = Q_i + sum_j k_ij (T_j - T_i) - h_i (T_i - T_amb). The link term is antisymmetric, so with the ambient couplings removed the total stored energy changes by exactly the injected heat. """ require len(temps) == len(network.nodes) require len(heat_w) == len(network.nodes) require dt_s > 0.0 and dt_s <= 5.0 mut flux = [] for index in range(0, len(temps)): flux = flux + [heat_w[index] - network.nodes[index].ambient_coupling_w_k * (temps[index] - ambient_c)] for link in network.links: source = node_index(network, link.from_node_id) target = node_index(network, link.to_node_id) transfer = link.conductance_w_k * (temps[source] - temps[target]) flux[source] = flux[source] - transfer flux[target] = flux[target] + transfer mut next_temps = [] for index in range(0, len(temps)): next_temps = next_temps + [temps[index] + flux[index] * dt_s / (network.nodes[index].capacity_kj_k * 1000.0)] return next_temps
pub def thermal_step(temps: list[f64], heat_w: list[f64], ambient_c: f64, dt_s: f64) -> list[f64] !{}: """Advances the vehicle thermal network one step.""" return thermal_step_network(thermal_network(), temps, heat_w, ambient_c, dt_s)
pub def thermal_initial_temps() -> list[f64] !{}: """Declared soak temperatures of the network, in node order.""" mut temps = [] for node in thermal_network().nodes: temps = temps + [node.initial_temp_c] return temps
def thermal_stability_limit_s(): """Largest explicit step the network tolerates: min over nodes of 2 C / (sum k + h).""" network = thermal_network() mut limit = 1000000.0 for index in range(0, len(network.nodes)): mut conductance = network.nodes[index].ambient_coupling_w_k for link in network.links: if link.from_node_id == network.nodes[index].id or link.to_node_id == network.nodes[index].id: conductance = conductance + link.conductance_w_k limit = min(limit, 2.0 * network.nodes[index].capacity_kj_k * 1000.0 / conductance) return limit
pub def assembly_export() -> dict[str, any] !{}: """Full assembly payload: placements, interfaces, kinematics, load paths, and the thermal graph.""" properties = mass_properties() parts = vehicle_parts() placements = part_placements() termination = port_termination_report() reports = mobility_reports() load_case = load_path_reactions(static_corner_load_n(), REFERENCE_BRAKING_N, REFERENCE_CORNERING_N) mut placement_rows = [] for index in range(0, len(placements)): placement = placements[index] mut centres = [] for row in placement_instances(placement): centres = centres + [[row[1], row[2], row[3]]] placement_rows = placement_rows + [{ "part_id": placement.part_id, "name": parts[index].name, "group": parts[index].group, "pattern": placement.pattern, "pattern_instances": placement.pattern_instances, "declared_count": parts[index].count, "mass_kg": placement.mass_kg, "centre": [placement.centre_x, placement.centre_y, placement.centre_z], "extent": [placement.extent_x, placement.extent_y, placement.extent_z], "span": [placement.span_x, placement.span_y, 0.0], "instance_centres": centres, "unsprung_fraction": placement.unsprung_fraction, }] mut port_rows = [] for port in vehicle_ports(): port_rows = port_rows + [{"id": port.id, "part": port.owner_part_id, "kind": port.kind, "mate_port": port.mate_port_id, "required": port.required, "terminated": port.terminated}] mut mate_rows = [] for mate in vehicle_mates(): mate_rows = mate_rows + [{"id": mate.id, "port_a": mate.port_a_id, "port_b": mate.port_b_id, "kind": mate.kind, "preload_n": mate.preload_n, "sealed": mate.sealed, "serviceable": mate.serviceable, "robot_accessible": mate.robot_accessible}] mut joint_rows = [] for item in vehicle_joints(): joint_rows = joint_rows + [{ "id": item.id, "name": item.name, "kind": item.kind, "parent_part": item.parent_part_id, "child_part": item.child_part_id, "parent_link": item.parent_link, "child_link": item.child_link, "axis": [item.axis_x, item.axis_y, item.axis_z], "origin": [item.origin_x, item.origin_y, item.origin_z], "limit_lower": item.limit_lower, "limit_upper": item.limit_upper, "ratio": item.ratio, "dof": item.dof, "idle_dof": item.idle_dof, "corner": item.corner, "loop_closing": item.loop_closing, }] mut load_path_rows = [] for path_result in load_case.results: mut entries = [] for entry in path_result.reactions: entries = entries + [{"index": entry.element_index, "part_id": entry.part_id, "role": entry.role, "force_n": entry.force_n, "moment_nm": entry.moment_nm}] load_path_rows = load_path_rows + [{ "id": path_result.path_id, "name": path_result.name, "kind": path_result.kind, "part_ids": [entry.part_id for entry in path_result.reactions], "applied_n": path_result.applied_n, "reaction_n": path_result.reaction_n, "residual_n": path_result.residual_n, "peak_force_n": path_result.peak_force_n, "rated_load_n": path_result.rated_load_n, "utilisation": path_result.utilisation, "reactions": entries, }] network = thermal_network() mut node_rows = [] for node in network.nodes: node_rows = node_rows + [{"id": node.id, "part_id": node.part_id, "capacity_kj_k": node.capacity_kj_k, "ambient_coupling_w_k": node.ambient_coupling_w_k, "initial_temp_c": node.initial_temp_c}] mut link_rows = [] for link in network.links: link_rows = link_rows + [{"id": link.id, "from": link.from_node_id, "to": link.to_node_id, "conductance_w_k": link.conductance_w_k, "medium": link.medium}] mut mobility_rows = [] mut mobility_ok = true for report in reports: mobility_rows = mobility_rows + [{ "corner": report.corner_id, "links": report.link_count, "joints": report.joint_count, "joint_dof_sum": report.joint_dof_sum, "gross_mobility": report.gross_mobility, "idle_dof": report.idle_dof, "effective_mobility": report.effective_mobility, "expected_mobility": report.expected_mobility, "independent_loops": report.independent_loops, "loop_closing_joints": report.loop_closing_joint_ids, "ok": report.ok, }] if not report.ok: mobility_ok = false mut suspension_rows = [] for load in [3800.0, static_corner_load_n(), 6500.0, 8500.0]: state = suspension_travel(load) suspension_rows = suspension_rows + [{ "load_n": state.vertical_load_n, "travel_m": state.wheel_travel_m, "spring_stroke_m": state.spring_stroke_m, "wheel_rate_n_m": state.wheel_rate_n_m, "motion_ratio": state.motion_ratio, "camber_deg": state.camber_deg, "toe_deg": state.toe_deg, "travel_utilisation": state.travel_utilisation, }] return { "schema": ASSEMBLY_SCHEMA, "configuration_variant": CONFIGURATION_VARIANT_DEFAULT, "authority": ASSEMBLY_AUTHORITY, "frame": { "origin": "ground plane at mid-wheelbase", "x": "forward", "y": "left", "z": "up", "front_axle_x_m": FRONT_AXLE_X_M, "rear_axle_x_m": REAR_AXLE_X_M, "half_track_m": HALF_TRACK_M, "wheel_radius_m": WHEEL_RADIUS_M, "ride_height_m": RIDE_HEIGHT_M, "wheelbase_m": WHEELBASE_M, }, "mass_properties": { "total_mass_kg": properties.total_mass_kg, "sprung_mass_kg": properties.sprung_mass_kg, "unsprung_mass_kg": properties.unsprung_mass_kg, "cg_x_m": properties.cg_x_m, "cg_y_m": properties.cg_y_m, "cg_z_m": properties.cg_z_m, "ixx_kg_m2": properties.ixx_kg_m2, "iyy_kg_m2": properties.iyy_kg_m2, "izz_kg_m2": properties.izz_kg_m2, "front_axle_load_kg": properties.front_axle_load_kg, "rear_axle_load_kg": properties.rear_axle_load_kg, "front_mass_fraction": properties.front_mass_fraction, "wheelbase_m": properties.wheelbase_m, "cg_height_m": properties.cg_height_m, "part_count": properties.part_count, "instance_count": properties.instance_count, "closure_residual_kg": properties.closure_residual_kg, }, "ports": port_rows, "mates": mate_rows, "joints": joint_rows, "placements": placement_rows, "load_paths": load_path_rows, "thermal": {"nodes": node_rows, "links": link_rows, "stability_limit_s": thermal_stability_limit_s()}, "mobility": mobility_rows, "suspension": { "static_corner_load_n": static_corner_load_n(), "travel_jounce_m": JOUNCE_LIMIT_M, "travel_rebound_m": REBOUND_LIMIT_M, "motion_ratio": motion_ratio(0.0), "static_camber_deg": STATIC_CAMBER_DEG, "static_toe_deg": STATIC_TOE_DEG, "samples": suspension_rows, }, "checks": { "mobility_ok": mobility_ok, "ports_terminated": termination["unterminated_required_ports"] == 0 and termination["mismated_ports"] == 0, "equilibrium_residual_n": load_case.equilibrium_residual_n, "moment_residual_nm": load_case.moment_residual_nm, "mass_closure_residual_kg": properties.closure_residual_kg, "unterminated_required_ports": termination["unterminated_required_ports"], "open_optional_ports": termination["open_optional_ports"], "mismated_ports": termination["mismated_ports"], "port_count": termination["ports"], "mate_count": termination["mates"], "joint_count": len(joint_rows), "placement_count": len(placement_rows), "load_path_count": len(load_path_rows), "thermal_node_count": len(node_rows), "thermal_link_count": len(link_rows), }, }
pub def assembly_equations() -> list[dict[str, str]] !{}: """Manifest rows for the assembly model, in the same shape as physics.sema:dynamics_equations().""" return [ {"id": "centre-of-mass", "source": "mass_properties", "expression": "r_cg = sum(m_i r_i) / sum(m_i)", "unit": "m", "owner": "Sema assembly mass model"}, {"id": "parallel-axis", "source": "parallel_axis_term", "expression": "I_zz = sum(m_i (a_i^2 + b_i^2)/12 + m_i ((x_i - x_cg)^2 + (y_i - y_cg)^2))", "unit": "kg m^2", "owner": "Sema assembly mass model"}, {"id": "axle-load-split", "source": "mass_properties", "expression": "f_front = (x_cg - x_rear) / L", "unit": "fraction", "owner": "Sema assembly mass model"}, {"id": "grubler-mobility", "source": "grubler_mobility", "expression": "M = 6 (n - 1 - j) + sum f_i - f_idle", "unit": "1", "owner": "Sema kinematic assembly"}, {"id": "four-bar-closure", "source": "corner_pose", "expression": "|p_ubj - p_uca| = L_upper and |p_ubj - p_lbj| = L_knuckle", "unit": "m", "owner": "Sema suspension kinematics"}, {"id": "motion-ratio", "source": "motion_ratio", "expression": "MR = -d(l_damper)/d(z_wheel)", "unit": "1", "owner": "Sema suspension kinematics"}, {"id": "air-spring", "source": "polytropic_pressure", "expression": "p = p_0 (V_0 / (V_0 - A s))^n; F_wheel = MR A (p - p_atm)", "unit": "N", "owner": "Sema suspension compliance"}, {"id": "bump-steer", "source": "bump_steer", "expression": "|R(u, delta) (p_tie - p_lbj) + p_lbj - p_rack| = L_tie", "unit": "rad", "owner": "Sema suspension kinematics"}, {"id": "static-equilibrium", "source": "corner_statics", "expression": "sum F = 0 and sum M = 0 solved per free body", "unit": "N", "owner": "Sema load-path statics"}, {"id": "load-path-closure", "source": "load_path_reactions", "expression": "R_total - F_applied = 0", "unit": "N", "owner": "Sema load-path statics"}, {"id": "thermal-network", "source": "thermal_step_network", "expression": "C_i dT_i/dt = Q_i + sum_j k_ij (T_j - T_i) - h_i (T_i - T_amb)", "unit": "degC", "owner": "Sema coupled thermal network"}, {"id": "thermal-stability", "source": "thermal_stability_limit_s", "expression": "dt_max = min_i 2 C_i / (sum_j k_ij + h_i)", "unit": "s", "owner": "Sema coupled thermal network"}, ]
test "part placements close the declared mass exactly": properties = mass_properties() ensure properties.closure_residual_kg == 0.0 ensure properties.total_mass_kg == ASSEMBLY_MASS_KG ensure properties.part_count == 38 ensure properties.instance_count > properties.part_count ensure abs(properties.sprung_mass_kg + properties.unsprung_mass_kg - properties.total_mass_kg) < 0.000000001 ensure properties.unsprung_mass_kg > 150.0 and properties.unsprung_mass_kg < 280.0
test "centre of gravity and inertia stay inside the crossover envelope": properties = mass_properties() ensure properties.cg_height_m >= 0.45 and properties.cg_height_m <= 0.60 ensure properties.front_mass_fraction >= 0.47 and properties.front_mass_fraction <= 0.53 ensure properties.izz_kg_m2 >= 2800.0 and properties.izz_kg_m2 <= 4500.0 ensure properties.ixx_kg_m2 >= 500.0 and properties.ixx_kg_m2 <= 1400.0 ensure properties.iyy_kg_m2 >= 2400.0 and properties.iyy_kg_m2 <= 4200.0 ensure abs(properties.cg_y_m) < 0.02 ensure abs(properties.front_axle_load_kg + properties.rear_axle_load_kg - properties.total_mass_kg) < 0.000000001
test "every required port is terminated by exactly one mate": report = port_termination_report() ensure report["unterminated_required_ports"] == 0 ensure report["mismated_ports"] == 0 ensure report["ports"] >= 90 ensure report["mates"] >= 45 ensure report["open_optional_ports"] > 0 ensure report["ports_mated_exactly_once"] == report["mates"] * 2
test "suspension corner loops have the mobility their joints imply": reports = mobility_reports() ensure len(reports) == 4 mut steered = 0 for report in reports: ensure report.ok ensure report.effective_mobility == report.expected_mobility ensure report.independent_loops == 3 ensure len(report.loop_closing_joint_ids) == 3 if report.expected_mobility == 2: steered = steered + 1 ensure steered == 2
test "the declared joint set covers the real chains": joints = vehicle_joints() ensure len(joints) >= 100 mut gears = 0 mut spherical = 0 mut prismatic = 0 mut ratio_product = 1.0 for item in joints: if item.kind == JointKind.gear: gears = gears + 1 if item.parent_link == "front-motor-rotor" or item.parent_link == "front-intermediate-shaft": ratio_product = ratio_product * item.ratio if item.kind == JointKind.spherical: spherical = spherical + 1 if item.kind == JointKind.prismatic: prismatic = prismatic + 1 ensure gears == 4 ensure abs(ratio_product - 9.1) < 0.000000001 ensure spherical >= 24 ensure prismatic >= 5
test "load path reactions close against the applied load": load_case = load_path_reactions(5200.0, 9000.0, 6000.0) ensure len(load_case.results) == 7 ensure load_case.equilibrium_residual_n < 0.000000001 ensure load_case.moment_residual_nm < 0.000000001 for path_result in load_case.results: ensure path_result.residual_n < 0.000000001 ensure path_result.peak_force_n > 0.0 ensure load_case.worst_utilisation > 0.0
test "an isolated thermal network conserves energy exactly": network = isolated_thermal_network() temps = [31.0, 58.0, 64.0, 47.0, 52.0, 21.0, 35.0, 27.0] heat = [1800.0, 2400.0, 3100.0, 900.0, 1100.0, 0.0, 0.0, 600.0] dt = 0.05 next_temps = thermal_step_network(network, temps, heat, 20.0, dt) mut stored = 0.0 mut injected = 0.0 for index in range(0, len(temps)): stored = stored + network.nodes[index].capacity_kj_k * 1000.0 * (next_temps[index] - temps[index]) injected = injected + heat[index] * dt ensure abs(stored - injected) < 0.000000001 * abs(injected)
test "the coupled network moves heat between components": temps = thermal_initial_temps() heat = [0.0, 9000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] next_temps = thermal_step(temps, heat, 24.0, 0.05) ensure next_temps[1] > temps[1] ensure next_temps[6] > temps[6] ensure thermal_stability_limit_s() > 1.0 ensure len(thermal_network().nodes) == 8 ensure len(thermal_network().links) == 10
test "suspension travel rises with load and keeps camber bounded": light = suspension_travel(3800.0) static = suspension_travel(static_corner_load_n()) heavy = suspension_travel(7200.0) limit = suspension_travel(20000.0) ensure light.wheel_travel_m < static.wheel_travel_m ensure static.wheel_travel_m < heavy.wheel_travel_m ensure heavy.wheel_travel_m <= limit.wheel_travel_m ensure abs(static.wheel_travel_m) < 0.001 ensure abs(static.camber_change_deg) < 0.05 ensure abs(light.camber_change_deg) < 6.0 and abs(limit.camber_change_deg) < 6.0 ensure abs(limit.toe_change_deg) < 1.0 ensure limit.at_limit and not static.at_limit ensure static.wheel_rate_n_m > 20000.0 and static.wheel_rate_n_m < 60000.0 ensure light.spring_stroke_m < 0.0 and heavy.spring_stroke_m > 0.0
test "declared damper stroke limits match the solved linkage": limits = arm_rotation_limits() joints = vehicle_joints() mut checked = 0 for item in joints: if item.id == "fl-damper-slide": ensure abs(item.limit_lower - (damper_length(0.0) - damper_length(limits[0]))) < 0.001 ensure abs(item.limit_upper - (damper_length(0.0) - damper_length(limits[1]))) < 0.001 checked = checked + 1 ensure checked == 1 ensure abs(corner_pose(limits[0])[7] - REBOUND_LIMIT_M) < 0.0001 ensure abs(corner_pose(limits[1])[7] - JOUNCE_LIMIT_M) < 0.0001
test "the assembly export publishes a complete and self-consistent payload": payload = assembly_export() checks = payload["checks"] ensure payload["schema"] == "sema.circuitframe-assembly/v1" ensure checks["mass_closure_residual_kg"] == 0.0 ensure checks["unterminated_required_ports"] == 0 ensure checks["mobility_ok"] ensure checks["ports_terminated"] ensure checks["equilibrium_residual_n"] < 0.000000001 ensure checks["port_count"] >= 90 ensure checks["mate_count"] >= 45 ensure checks["joint_count"] >= 100 ensure checks["placement_count"] == 38 ensure checks["load_path_count"] == 7 ensure len(assembly_equations()) >= 10
test "declared load paths report a real utilisation under the reference load case": paths = load_paths() ensure len(paths) == 7 mut covered = 0 for declared in paths: ensure declared.utilisation > 0.0 and declared.utilisation < 1.0 ensure len(declared.part_ids) >= 4 covered = covered + 1 ensure covered == len(load_path_declarations()) ensure paths[0].id == "lp-vertical-corner" ensure paths[6].id == "lp-belt-anchorage"src/authoring.sema
Section titled “src/authoring.sema”"""Bounded natural-language circuit authoring compiled into typed Sema net definitions."""
from magna_ev_digital_twin.domain import Fidelity, NetDefinition
assure silver
pub enum AuthoringIntent: connect | constrain | unknown
pub struct CircuitAuthoringResult: intent: AuthoringIntent accepted: bool natural_description: str formal_sema: str net: NetDefinition requirements: list[str] assumptions: list[str] diagnostics: list[str] invariant len(natural_description) > 0 and len(natural_description) <= 2048 invariant len(requirements) <= 16 and len(assumptions) <= 16 and len(diagnostics) <= 16
def formal_hv_drive_net(): sem "A typed formal Sema definition of the protected 800 V source-to-inverter relation" return NetDefinition( id="authoring-hv-drive", name="Authored pack-to-inverter protected power relation", domain="HV", source_part_id="hv-junction", target_part_ids=["front-inverter", "rear-inverter"], nominal_voltage_v=800.0, maximum_current_a=520.0, route="protected CircuitFrame sill conductors", fidelity=Fidelity.derived, evidence_ids=["circuitframe-electrical-v1"], )
def formal_zonal_supply_net(): sem "A typed formal Sema definition of the protected 48 V zonal supply relation" return NetDefinition( id="authoring-lv-zonal", name="Authored 48 V zonal supply relation", domain="LV", source_part_id="dc-dc", target_part_ids=["front-zone-controller", "rear-zone-controller", "cabin-zone-controller"], nominal_voltage_v=48.0, maximum_current_a=180.0, route="dual protected structural conductors", fidelity=Fidelity.derived, evidence_ids=["circuitframe-electrical-v1"], )
def formal_data_backbone_net(): sem "A typed formal Sema definition of the redundant vehicle data relation" return NetDefinition( id="authoring-data-backbone", name="Authored redundant Ethernet backbone", domain="DATA", source_part_id="central-compute", target_part_ids=["front-zone-controller", "rear-zone-controller", "cabin-zone-controller"], nominal_voltage_v=1.0, maximum_current_a=0.2, route="shielded differential structural links", fidelity=Fidelity.derived, evidence_ids=["circuitframe-electrical-v1"], )
def unsupported_net(): return NetDefinition( id="unsupported", name="Unsupported authoring request", domain="UNKNOWN", source_part_id="unknown", target_part_ids=["unknown"], nominal_voltage_v=0.0, maximum_current_a=0.0, route="none", fidelity=Fidelity.unknown, evidence_ids=[], )
pub def compile_circuit_description(description: str) -> CircuitAuthoringResult !{}: sem "Interpret bounded plain automotive circuit language and emit a typed formal Sema relation or fail visibly" require len(description) > 0 and len(description) <= 2048 text = description.lower() if text.contains("800") and (text.contains("battery") or text.contains("pack")) and text.contains("inverter"): net = formal_hv_drive_net() return CircuitAuthoringResult( intent=AuthoringIntent.connect, accepted=true, natural_description=description, formal_sema="NetDefinition(id=\"authoring-hv-drive\", domain=\"HV\", source_part_id=\"hv-junction\", target_part_ids=[\"front-inverter\", \"rear-inverter\"], nominal_voltage_v=800.0, maximum_current_a=520.0, route=\"protected CircuitFrame sill conductors\", fidelity=Fidelity.derived, evidence_ids=[\"circuitframe-electrical-v1\"])", net=net, requirements=["800 V nominal class", "protected source-to-load path", "front and rear inverter targets", "current limit not above 520 A"], assumptions=["battery source terminates at the canonical HV junction", "protected panel conductors use the active section profile"], diagnostics=[], ) if text.contains("48") and (text.contains("zone") or text.contains("controller")): net = formal_zonal_supply_net() return CircuitAuthoringResult( intent=AuthoringIntent.connect, accepted=true, natural_description=description, formal_sema="NetDefinition(id=\"authoring-lv-zonal\", domain=\"LV\", source_part_id=\"dc-dc\", target_part_ids=[\"front-zone-controller\", \"rear-zone-controller\", \"cabin-zone-controller\"], nominal_voltage_v=48.0, maximum_current_a=180.0, route=\"dual protected structural conductors\", fidelity=Fidelity.derived, evidence_ids=[\"circuitframe-electrical-v1\"])", net=net, requirements=["48 V nominal class", "front, rear, and cabin zone targets", "dual protected paths"], assumptions=["the canonical DC/DC is the low-voltage source"], diagnostics=[], ) if text.contains("ethernet") or (text.contains("data") and text.contains("zone")): net = formal_data_backbone_net() return CircuitAuthoringResult( intent=AuthoringIntent.connect, accepted=true, natural_description=description, formal_sema="NetDefinition(id=\"authoring-data-backbone\", domain=\"DATA\", source_part_id=\"central-compute\", target_part_ids=[\"front-zone-controller\", \"rear-zone-controller\", \"cabin-zone-controller\"], nominal_voltage_v=1.0, maximum_current_a=0.2, route=\"shielded differential structural links\", fidelity=Fidelity.derived, evidence_ids=[\"circuitframe-electrical-v1\"])", net=net, requirements=["redundant Ethernet relation", "shielded differential route", "all zone controllers addressed"], assumptions=["central compute is the logical source"], diagnostics=[], ) return CircuitAuthoringResult( intent=AuthoringIntent.unknown, accepted=false, natural_description=description, formal_sema="", net=unsupported_net(), requirements=[], assumptions=[], diagnostics=["Unsupported bounded intent. Name voltage/domain plus known source and target roles.", "Supported profiles: 800 V battery-to-inverters, 48 V zonal supply, redundant Ethernet data backbone."], )
test "natural 800 V request compiles to a typed protected drive relation": result = compile_circuit_description("Connect the 800 V battery to both inverters through protected CircuitFrame conductors") ensure result.accepted and result.intent == AuthoringIntent.connect ensure result.net.domain == "HV" and result.net.nominal_voltage_v == 800.0 ensure result.net.source_part_id == "hv-junction" and len(result.net.target_part_ids) == 2 ensure result.formal_sema.contains("NetDefinition")
test "natural 48 V request compiles to the three zonal controllers": result = compile_circuit_description("Supply every zone controller over dual protected 48 V conductors") ensure result.accepted and result.net.domain == "LV" ensure len(result.net.target_part_ids) == 3 and result.net.maximum_current_a == 180.0
test "unknown natural request fails visibly rather than inventing topology": result = compile_circuit_description("Make the circuit magical and perfect") ensure not result.accepted and result.intent == AuthoringIntent.unknown ensure len(result.formal_sema) == 0 and len(result.diagnostics) == 2src/candidate.sema
Section titled “src/candidate.sema”"""Bounded structural-conductor candidate search with immutable gate results."""
assure silver
struct PanelCandidate: id: str base_revision: int width_mm: f64 thickness_mm: f64 length_m: f64 mass_kg: f64 current_density_a_mm2: f64 voltage_drop_v: f64 loss_kw: f64 estimated_temp_c: f64 mass_reduction_fraction: f64 passed: bool reason: str invariant len(id) > 0 invariant base_revision >= 1 invariant width_mm > 0.0 invariant thickness_mm > 0.0 invariant length_m > 0.0 invariant mass_kg > 0.0 invariant current_density_a_mm2 >= 0.0 invariant voltage_drop_v >= 0.0 invariant loss_kw >= 0.0 invariant estimated_temp_c >= -40.0 invariant mass_reduction_fraction >= 0.0 and mass_reduction_fraction <= 1.0 invariant len(reason) > 0
pub equation conductor_resistance(resistivity_ohm_m, length_m, width_mm, thickness_mm) -> any: return resistivity_ohm_m * length_m / (width_mm * thickness_mm * 0.000001)
pub equation conductor_mass(density_kg_m3, length_m, width_mm, thickness_mm) -> any: return density_kg_m3 * length_m * width_mm * thickness_mm * 0.000001
def evaluate_panel_candidate(width_mm: f64, base_revision: int) !{}: thickness_mm = 1.2 length_m = 2.6 current_a = 420.0 area_mm2 = width_mm * thickness_mm resistance = conductor_resistance(0.0000000168, length_m, width_mm, thickness_mm) voltage_drop = current_a * resistance loss_kw = current_a * current_a * resistance / 1000.0 mass = conductor_mass(8960.0, length_m, width_mm, thickness_mm) baseline_mass = conductor_mass(8960.0, length_m, 90.0, thickness_mm) current_density = current_a / area_mm2 estimated_temp = 24.0 + loss_kw * 18.0 passed = current_density <= 6.5 and voltage_drop <= 1.8 and estimated_temp <= 90.0 and width_mm >= 40.0 reason = "all current-density, voltage-drop, thermal, manufacturability and service-width gates passed" if passed else "one or more electrical, thermal or manufacturability gates failed" return PanelCandidate( id="panel-width-" + str(int(width_mm)) + "-r" + str(base_revision), base_revision=base_revision, width_mm=width_mm, thickness_mm=thickness_mm, length_m=length_m, mass_kg=mass, current_density_a_mm2=current_density, voltage_drop_v=voltage_drop, loss_kw=loss_kw, estimated_temp_c=estimated_temp, mass_reduction_fraction=max(0.0, (baseline_mass - mass) / baseline_mass), passed=passed, reason=reason, )
pub def optimize_panel(base_revision: int) -> dict[str, any] !{}: require base_revision >= 1 widths = [80.0, 70.0, 60.0, 50.0, 40.0] mut evaluated = [] mut selected = evaluate_panel_candidate(90.0, base_revision) for width in widths: candidate = evaluate_panel_candidate(width, base_revision) evaluated.append(candidate) if candidate.passed and candidate.mass_kg < selected.mass_kg: selected = candidate ensure selected.passed return { "schema": "sema.circuitframe-candidate-preview/v1", "objective": "reduce structural-conductor mass", "protected_constraints": ["current density <= 6.5 A/mm2", "voltage drop <= 1.8 V", "estimated temperature <= 90 C", "manufacturable width >= 40 mm"], "evaluated": evaluated, "selected": selected, "commit_authorized": selected.passed, "decision_authority": false, "evidence": ["Sema deterministic bounded enumeration", "electrical and thermal equations", "immutable base revision"], }
test "bounded panel search selects the lightest candidate that passes every gate": preview = optimize_panel(1) selected = preview["selected"] ensure preview["commit_authorized"] ensure selected.passed ensure selected.width_mm == 60.0 ensure selected.mass_reduction_fraction > 0.3 ensure selected.current_density_a_mm2 <= 6.5 ensure selected.voltage_drop_v <= 1.8 ensure selected.estimated_temp_c <= 90.0src/domain.sema
Section titled “src/domain.sema”"""Canonical automotive identities, completeness, evidence, and interface contracts."""
assure silver
pub enum Fidelity: canonical | derived | reduced | illustrative | unknown | excluded
pub enum PartGroup: body | chassis | energy | drive | motion | thermal | electrical | control | cabin | safety | manufacturing
pub enum PortKind: mechanical | high_voltage | low_voltage | data | thermal | fluid | service
pub enum AutomationClass: robot_validated | assisted | manual | failed | unknown
pub enum QualificationStatus: executable_reduced | planned_external | reference_only | blocked_missing_evidence
pub enum EvidenceClass: computational | structural | direct_parity | reduced_model | manufacturing | negative | provenance
pub enum SourceAuthority: customer_oem | licensed_benchmark | open_research | engineered_assumption | unqualified_fixture
pub struct SourceRecord: id: str authority: SourceAuthority provider: str vehicle_model: str source_revision: str artifact_kind: str rights_scope: str digest: str fidelity: Fidelity render_allowed: bool simulation_allowed: bool export_allowed: bool invariant len(id) > 0 invariant len(provider) > 0 invariant len(vehicle_model) > 0 invariant len(source_revision) > 0 invariant len(artifact_kind) > 0 invariant len(rights_scope) > 0 invariant len(digest) > 0
pub struct SourceBinding: entity_id: str source_record_id: str source_entity_id: str fidelity: Fidelity invariant len(entity_id) > 0 invariant len(source_record_id) > 0 invariant len(source_entity_id) > 0
struct ConfigurationDelta: id: str base_configuration: str target_configuration: str unchanged_part_ids: list[str] removed_route_ids: list[str] added_feature_ids: list[str] changed_operation_ids: list[str] invariant len(id) > 0 invariant len(base_configuration) > 0 invariant len(target_configuration) > 0 invariant base_configuration != target_configuration invariant len(unchanged_part_ids) <= 100000 invariant len(removed_route_ids) <= 100000 invariant len(added_feature_ids) <= 100000 invariant len(changed_operation_ids) <= 100000
pub struct VehicleIdentity: id: str revision: int name: str category: str seats: int fidelity: Fidelity invariant len(id) > 0 invariant revision >= 1 invariant len(name) > 0 invariant len(category) > 0 invariant seats > 0 and seats <= 9
pub struct PartDefinition: id: str name: str group: PartGroup count: int material: str mass_kg: f64 fidelity: Fidelity opaque: bool evidence_ids: list[str] invariant len(id) > 0 invariant len(name) > 0 invariant count > 0 and count <= 10000 invariant len(material) > 0 invariant mass_kg >= 0.0 invariant len(evidence_ids) <= 64
pub struct PortDefinition: id: str owner_part_id: str kind: PortKind mate_port_id: str required: bool terminated: bool invariant len(id) > 0 invariant len(owner_part_id) > 0 invariant len(mate_port_id) > 0 or not required
pub struct NetDefinition: id: str name: str domain: str source_part_id: str target_part_ids: list[str] nominal_voltage_v: f64 maximum_current_a: f64 route: str fidelity: Fidelity evidence_ids: list[str] invariant len(id) > 0 invariant len(name) > 0 invariant len(domain) > 0 invariant len(source_part_id) > 0 invariant len(target_part_ids) > 0 and len(target_part_ids) <= 128 invariant nominal_voltage_v >= 0.0 invariant maximum_current_a >= 0.0 invariant len(route) > 0 invariant len(evidence_ids) <= 64
pub struct OperationDefinition: id: str name: str station: str resource: str consumed_part_ids: list[str] created_features: list[str] automation: AutomationClass cycle_time_s: f64 evidence_ids: list[str] invariant len(id) > 0 invariant len(name) > 0 invariant len(station) > 0 invariant len(resource) > 0 invariant len(consumed_part_ids) <= 128 invariant len(created_features) <= 128 invariant cycle_time_s >= 0.0 invariant len(evidence_ids) <= 64
pub struct ValidationCaseDefinition: id: str name: str domain: str authority: str method: str status: QualificationStatus fidelity: Fidelity evidence_ids: list[str] invariant len(id) > 0 invariant len(name) > 0 invariant len(domain) > 0 invariant len(authority) > 0 invariant len(method) > 0 invariant len(evidence_ids) <= 64
pub struct EvidenceRecord: id: str kind: EvidenceClass source: str summary: str fidelity: Fidelity accepted: bool invariant len(id) > 0 invariant len(source) > 0 invariant len(summary) > 0
pub struct CompletenessReport: profile: str declared_parts: int declared_instances: int declared_nets: int declared_operations: int unterminated_required_ports: int required_port_inventory_complete: bool unknown_operations: int major_assembly_complete: bool production_complete: bool exclusions: list[str] invariant len(profile) > 0 invariant declared_parts > 0 invariant declared_instances >= declared_parts invariant declared_nets > 0 invariant declared_operations > 0 invariant unterminated_required_ports >= 0 invariant unknown_operations >= 0 invariant len(exclusions) <= 128src/hardware.sema
Section titled “src/hardware.sema”"""Engineering specification of the declared drive, brake, tyre, pack and gear hardware.
The vehicle's mass is summed from the declared part placements in `assembly.sema`, but until nownothing that accelerated or stopped that mass was declared: peak torque, drive power, brake force,regenerative force, pack energy, pack resistance, tyre grip, cornering stiffness and rollingresistance were all bare literals in `physics.sema`. This module declares the engineeringparameters of the components `vehicle.sema` already lists and derives those limits from them, sothe performance follows from the hardware rather than sitting beside it.
Every specification is keyed to the part id it represents, and `part_reconciliation()` checks thateach id resolves in `vehicle_parts()`, that the declared instance counts agree, and that the massthe specification implies matches the mass the part already declares. `hardware_limits()` failsloudly on any mismatch, so a specification cannot quietly drift away from the part it describes.
Honesty: these are engineered assumptions for a reconstruction, not OEM data. No motor wasdynamometer tested, no caliper was pressure tested and no cell was cycled. Each declared parametercarries a `solved`, `correlated` or `assumed` tag in `hardware_parameters()`, using the samevocabulary as the aerodynamic and assembly modules: `assumed` for a bounded engineering choice,`correlated` for an empirical closure with a cited form, `solved` for a value that falls out of aclosed derivation over the declared inputs."""
import math
from magna_ev_digital_twin.domain import PartDefinitionfrom magna_ev_digital_twin.vehicle import vehicle_nets, vehicle_parts
assure silver
SOURCE_SOLVED = "solved"SOURCE_CORRELATED = "correlated"SOURCE_ASSUMED = "assumed"
GRAVITY_MPS2 = 9.80665ROOT_THREE = 1.7320508075688772ROOT_TWO = 1.4142135623730951INCH_M = 0.0254
# Shared inverter design choices: one silicon generation and one DC-link ripple budget for both# units, so only the current rating, the on-state resistance and the housing differ per axle.INVERTER_SWITCHING_FREQUENCY_HZ = 12000.0INVERTER_DC_LINK_RIPPLE_V = 8.0INVERTER_SWITCHING_ENERGY_MJ_PER_A = 0.060INVERTER_CAPACITOR_KG_PER_MF = 3.0INVERTER_MODULE_KG_PER_100A = 0.16INVERTER_SWITCH_COUNT = 6.0
# Shared machine build: mean density of the active stack (laminations, copper, magnets, slot air)# and the assembled-to-active mass ratio that covers housing, shaft, bearings and end windings.MOTOR_ACTIVE_DENSITY_KG_M3 = 4400.0MOTOR_ASSEMBLED_MASS_FACTOR = 1.34MOTOR_EFFICIENCY = 0.95
# Shared friction-brake material and hydraulic limits, common to both axles.PAD_FRICTION_COEFFICIENT = 0.42MAX_LINE_PRESSURE_PA = 16000000.0DISC_DENSITY_KG_M3 = 7200.0DISC_SPECIFIC_HEAT_J_KG_K = 460.0FADE_ONSET_TEMP_C = 340.0FADE_RATE_PER_K = 0.0012FADE_FLOOR_FRACTION = 0.45DISC_COOLING_BASE_W_PER_K = 1.6DISC_COOLING_SPEED_W_PER_K_MPS = 0.75
STEEL_DENSITY_KG_M3 = 7850.0GEAR_MESH_EFFICIENCY = 0.985GEAR_BLANK_WEB_FACTOR = 0.62GEARBOX_HOUSING_MASS_FACTOR = 1.16
MASS_TOLERANCE_FRACTION = 0.03
struct TractionMotorSpec: """One interior-permanent-magnet traction machine, keyed to its declared drive part.""" part_id: str pole_pairs: int rotor_radius_m: f64 stack_length_m: f64 stator_outer_radius_m: f64 magnet_flux_linkage_wb: f64 d_axis_inductance_h: f64 q_axis_inductance_h: f64 peak_phase_current_a: f64 continuous_phase_current_a: f64 current_angle_deg: f64 max_speed_rpm: f64 invariant len(part_id) > 0 invariant pole_pairs >= 1 invariant rotor_radius_m > 0.0 and rotor_radius_m < stator_outer_radius_m invariant stack_length_m > 0.0 invariant magnet_flux_linkage_wb > 0.0 invariant q_axis_inductance_h >= d_axis_inductance_h and d_axis_inductance_h > 0.0 invariant peak_phase_current_a >= continuous_phase_current_a and continuous_phase_current_a > 0.0 invariant current_angle_deg >= 0.0 and current_angle_deg < 90.0 invariant max_speed_rpm > 0.0
struct InverterSpec: """One traction inverter, keyed to its declared power-electronics part.""" part_id: str dc_link_voltage_v: f64 peak_phase_current_a: f64 on_state_resistance_ohm: f64 housing_mass_kg: f64 net_id: str invariant len(part_id) > 0 invariant dc_link_voltage_v > 0.0 invariant peak_phase_current_a > 0.0 invariant on_state_resistance_ohm > 0.0 invariant housing_mass_kg > 0.0 invariant len(net_id) > 0
struct BrakeCornerSpec: """One brake-by-wire corner module. Both axles are instances of the declared `brakes` part.""" part_id: str axle: str disc_outer_radius_m: f64 disc_inner_radius_m: f64 hat_radius_m: f64 disc_thickness_m: f64 vent_solid_fraction: f64 piston_count: int piston_bore_m: f64 caliper_mass_kg: f64 pad_mass_kg: f64 actuator_mass_kg: f64 invariant len(part_id) > 0 invariant disc_outer_radius_m > disc_inner_radius_m and disc_inner_radius_m > hat_radius_m invariant disc_thickness_m > 0.0 invariant vent_solid_fraction > 0.0 and vent_solid_fraction <= 1.0 invariant piston_count >= 1 invariant piston_bore_m > 0.0 invariant caliper_mass_kg > 0.0 and pad_mass_kg > 0.0 and actuator_mass_kg > 0.0
struct TireSpec: """The declared road tyre, in the size notation moulded on its sidewall.""" part_id: str section_width_m: f64 aspect_ratio: f64 rim_diameter_in: f64 rolling_radius_factor: f64 load_index_kg: f64 peak_friction: f64 slide_friction: f64 load_sensitivity: f64 friction_reference_load_fraction: f64 cornering_stiffness_per_n: f64 rolling_coefficient: f64 rolling_speed_coefficient: f64 carcass_thickness_m: f64 carcass_density_kg_m3: f64 invariant len(part_id) > 0 invariant section_width_m > 0.0 and aspect_ratio > 0.0 and rim_diameter_in > 0.0 invariant rolling_radius_factor > 0.0 and rolling_radius_factor <= 1.0 invariant load_index_kg > 0.0 invariant peak_friction > slide_friction and slide_friction > 0.0 invariant load_sensitivity >= 0.0 invariant friction_reference_load_fraction > 0.0 and friction_reference_load_fraction <= 1.0 invariant cornering_stiffness_per_n > 0.0 invariant rolling_coefficient > 0.0 and rolling_speed_coefficient >= 0.0 invariant carcass_thickness_m > 0.0 and carcass_density_kg_m3 > 0.0
struct PackSpec: """The declared battery modules, described at the cell that fills them.""" part_id: str module_count: int series_per_module: int parallel_per_module: int cell_nominal_voltage_v: f64 cell_capacity_ah: f64 cell_resistance_ohm: f64 cell_specific_energy_wh_kg: f64 module_packaging_factor: f64 module_interconnect_resistance_ohm: f64 pack_bus_resistance_ohm: f64 cell_pulse_charge_c_rate: f64 cell_pulse_discharge_c_rate: f64 invariant len(part_id) > 0 invariant module_count >= 1 and series_per_module >= 1 and parallel_per_module >= 1 invariant cell_nominal_voltage_v > 0.0 and cell_capacity_ah > 0.0 invariant cell_resistance_ohm > 0.0 and cell_specific_energy_wh_kg > 0.0 invariant module_packaging_factor >= 1.0 invariant module_interconnect_resistance_ohm >= 0.0 and pack_bus_resistance_ohm >= 0.0 invariant cell_pulse_charge_c_rate > 0.0 and cell_pulse_discharge_c_rate > 0.0
struct GearStageSpec: """One helical reduction stage, described by the two pitch diameters that set its ratio.""" pinion_pitch_diameter_m: f64 wheel_pitch_diameter_m: f64 face_width_m: f64 invariant wheel_pitch_diameter_m > pinion_pitch_diameter_m and pinion_pitch_diameter_m > 0.0 invariant face_width_m > 0.0
struct GearboxSpec: """The declared single-speed reduction gearset, one unit per drive axle.""" part_id: str unit_count: int first: GearStageSpec second: GearStageSpec invariant len(part_id) > 0 invariant unit_count >= 1
pub struct MotorLimits: """What one traction machine can actually deliver through its own inverter.""" part_id: str inverter_part_id: str effective_peak_current_a: f64 current_limited_by: str peak_torque_nm: f64 continuous_torque_nm: f64 base_speed_rpm: f64 max_speed_rpm: f64 constant_power_ratio: f64 corner_power_kw: f64 stator_flux_linkage_wb: f64 power_factor: f64 inverter_apparent_power_kva: f64 inverter_peak_power_kw: f64 inverter_loss_kw: f64 dc_link_peak_current_a: f64 airgap_shear_stress_kpa: f64 invariant len(part_id) > 0 invariant effective_peak_current_a > 0.0 invariant peak_torque_nm > continuous_torque_nm and continuous_torque_nm > 0.0 invariant base_speed_rpm > 0.0 and max_speed_rpm > base_speed_rpm invariant corner_power_kw > 0.0 invariant power_factor > 0.0 and power_factor <= 1.0 invariant inverter_peak_power_kw > 0.0 invariant airgap_shear_stress_kpa > 0.0
pub struct BrakeAxleLimits: """Both corner modules of one axle, reduced to the numbers the dynamics needs.""" part_id: str axle: str effective_radius_m: f64 piston_area_m2: f64 clamp_force_n: f64 corner_torque_nm: f64 axle_force_n: f64 disc_mass_kg: f64 disc_thermal_capacity_j_k: f64 invariant len(part_id) > 0 invariant effective_radius_m > 0.0 invariant piston_area_m2 > 0.0 invariant clamp_force_n > 0.0 invariant corner_torque_nm > 0.0 invariant axle_force_n > 0.0 invariant disc_thermal_capacity_j_k > 0.0
pub struct TireLimits: """The contact-patch model the dynamics reads, evaluated against live corner loads.""" part_id: str unloaded_radius_m: f64 rolling_radius_m: f64 rated_load_n: f64 friction_reference_load_n: f64 peak_friction: f64 slide_friction: f64 load_sensitivity: f64 cornering_stiffness_per_n: f64 rolling_coefficient: f64 rolling_speed_coefficient: f64 invariant len(part_id) > 0 invariant rolling_radius_m > 0.0 and rolling_radius_m < unloaded_radius_m invariant rated_load_n > 0.0 and friction_reference_load_n > 0.0 invariant peak_friction > slide_friction and slide_friction > 0.0
pub struct PackLimits: """The traction pack, summed from the declared cell out to the terminals.""" part_id: str cell_count: int series_count: int parallel_count: int energy_kwh: f64 nominal_voltage_v: f64 resistance_ohm: f64 charge_power_kw: f64 discharge_power_kw: f64 mass_kg: f64 invariant len(part_id) > 0 invariant cell_count >= 1 invariant energy_kwh > 0.0 invariant nominal_voltage_v > 0.0 invariant resistance_ohm > 0.0 invariant charge_power_kw > 0.0 and discharge_power_kw > 0.0 invariant mass_kg > 0.0
struct PartReconciliation: """One declared part, checked against the specification that claims to describe it.""" part_id: str resolved: bool declared_count: int expected_count: int declared_mass_kg: f64 derived_mass_kg: f64 mass_error_fraction: f64 tolerance_fraction: f64 derivation: str passed: bool invariant len(part_id) > 0 invariant expected_count >= 1 invariant mass_error_fraction >= 0.0 invariant tolerance_fraction >= 0.0 invariant len(derivation) > 0
pub struct AxleHardware: """Everything one axle's 20 Hz longitudinal solve needs, pre-solved into flat scalars.
Nothing on this struct depends on the live vehicle state, so it is built once with the rest of the hardware. The loop is then left with only the arithmetic that genuinely varies: the friction circle on this axle's live vertical load, the fade factor at its live disc temperature, and the comparison that decides which of the two is binding. Keeping it flat also keeps it cheap to pass around: the interpreter copies a struct argument by value, so the per-step helpers must never be handed anything that carries the panel basis or a pre-sampled table. """ axle: str brake_axle_force_n: f64 drive_force_per_throttle_n: f64 power_force_numerator_w: f64 power_constraint: str peak_friction: f64 slide_friction: f64 load_sensitivity: f64 friction_reference_load_n: f64 cornering_stiffness_per_n: f64 fade_onset_temp_c: f64 fade_rate_per_k: f64 fade_floor_fraction: f64 disc_thermal_capacity_j_k: f64 invariant len(axle) > 0 and len(power_constraint) > 0 invariant brake_axle_force_n > 0.0 invariant drive_force_per_throttle_n > 0.0 invariant power_force_numerator_w > 0.0 invariant peak_friction > slide_friction and slide_friction > 0.0 invariant load_sensitivity >= 0.0 invariant friction_reference_load_n > 0.0 invariant cornering_stiffness_per_n > 0.0 invariant fade_onset_temp_c > 0.0 invariant fade_rate_per_k >= 0.0 invariant fade_floor_fraction > 0.0 and fade_floor_fraction <= 1.0 invariant disc_thermal_capacity_j_k > 0.0
pub struct HardwareLimits: """Everything `physics.sema` used to hardcode, derived from the declared components.""" front_motor: MotorLimits rear_motor: MotorLimits front_brake: BrakeAxleLimits rear_brake: BrakeAxleLimits tire: TireLimits pack: PackLimits front_axle: AxleHardware rear_axle: AxleHardware final_drive_ratio: f64 gearbox_efficiency: f64 motor_efficiency: f64 inverter_efficiency: f64 max_motor_torque_nm: f64 max_drive_power_kw: f64 max_brake_force_n: f64 max_regenerative_force_n: f64 max_regenerative_power_kw: f64 regenerative_front_share: f64 max_line_pressure_pa: f64 pad_friction: f64 fade_onset_temp_c: f64 fade_rate_per_k: f64 fade_floor_fraction: f64 disc_cooling_base_w_per_k: f64 disc_cooling_speed_w_per_k_mps: f64 driveline_top_speed_mps: f64 invariant final_drive_ratio > 1.0 invariant gearbox_efficiency > 0.0 and gearbox_efficiency <= 1.0 invariant max_motor_torque_nm > 0.0 invariant max_drive_power_kw > 0.0 invariant motor_efficiency > 0.0 and motor_efficiency <= 1.0 invariant inverter_efficiency > 0.0 and inverter_efficiency <= 1.0 invariant max_brake_force_n > 0.0 invariant max_regenerative_force_n > 0.0 invariant max_regenerative_power_kw > 0.0 invariant regenerative_front_share > 0.0 and regenerative_front_share < 1.0 invariant fade_onset_temp_c > 0.0 invariant fade_rate_per_k >= 0.0 invariant fade_floor_fraction > 0.0 and fade_floor_fraction <= 1.0 invariant driveline_top_speed_mps > 0.0 invariant max_line_pressure_pa > 0.0
equation ipm_shaft_torque(pole_pairs, flux_linkage, d_inductance, q_inductance, current, angle_rad) -> any: return 1.5 * pole_pairs * current * cos(angle_rad) * (flux_linkage + (q_inductance - d_inductance) * current * sin(angle_rad))
equation stator_flux_linkage(flux_linkage, d_inductance, q_inductance, current, angle_rad) -> any: return sqrt((flux_linkage - d_inductance * current * sin(angle_rad))^2 + (q_inductance * current * cos(angle_rad))^2)
equation voltage_limited_speed(dc_link_voltage, stator_flux, pole_pairs) -> any: return dc_link_voltage / (sqrt(3.0) * stator_flux * pole_pairs)
equation airgap_shear_stress(torque, rotor_radius, stack_length) -> any: return torque / (2.0 * 3.141592653589793 * rotor_radius^2 * stack_length)
equation uniform_pressure_radius(outer_radius, inner_radius) -> any: return (2.0 / 3.0) * (outer_radius^3 - inner_radius^3) / (outer_radius^2 - inner_radius^2)
equation caliper_clamp_force(line_pressure, piston_count, piston_bore) -> any: return line_pressure * piston_count * 3.141592653589793 * (piston_bore / 2.0)^2
equation corner_brake_torque(pad_friction, clamp_force, effective_radius) -> any: return 2.0 * pad_friction * clamp_force * effective_radius
pub equation brake_fade_factor(disc_temp_c, onset_c, rate_per_k, floor_fraction) -> any: return max(floor_fraction, 1.0 - rate_per_k * max(0.0, disc_temp_c - onset_c))
pub equation tire_friction(peak, slide, sensitivity, reference_load, corner_load) -> any: return max(slide, min(peak, peak * (1.0 - sensitivity * (corner_load / reference_load - 1.0))))
pub equation rolling_coefficient(base, speed_coefficient, speed) -> any: return base + speed_coefficient * speed^2
def front_motor_spec(): """Front machine: smaller rotor, more turns, sized for the lighter front axle duty.""" return TractionMotorSpec( part_id="front-motor", pole_pairs=4, rotor_radius_m=0.078, stack_length_m=0.160, stator_outer_radius_m=0.140, magnet_flux_linkage_wb=0.260, d_axis_inductance_h=0.00070, q_axis_inductance_h=0.00095, peak_phase_current_a=145.0, continuous_phase_current_a=65.0, current_angle_deg=15.0, max_speed_rpm=17000.0, )
def rear_motor_spec(): """Rear machine: longer, wider rotor and roughly twice the current for the primary drive axle.""" return TractionMotorSpec( part_id="rear-motor", pole_pairs=4, rotor_radius_m=0.093, stack_length_m=0.170, stator_outer_radius_m=0.150, magnet_flux_linkage_wb=0.212, d_axis_inductance_h=0.00030, q_axis_inductance_h=0.00044, peak_phase_current_a=355.0, continuous_phase_current_a=145.0, current_angle_deg=15.0, max_speed_rpm=16000.0, )
def front_inverter_spec(): """Front inverter: standalone subframe housing, rated above the machine it feeds.""" return InverterSpec(part_id="front-inverter", dc_link_voltage_v=800.0, peak_phase_current_a=160.0, on_state_resistance_ohm=0.0045, housing_mass_kg=8.20, net_id="hv-pack-front")
def rear_inverter_spec(): """Rear inverter: shares the drive-unit casting, and is the binding current limit of that axle.""" return InverterSpec(part_id="rear-inverter", dc_link_voltage_v=800.0, peak_phase_current_a=335.0, on_state_resistance_ohm=0.0028, housing_mass_kg=6.20, net_id="hv-pack-rear")
def front_brake_spec(): """Front corner: 355 mm vented disc, four-piston fixed caliper.""" return BrakeCornerSpec(part_id="brakes", axle="front", disc_outer_radius_m=0.1775, disc_inner_radius_m=0.1175, hat_radius_m=0.090, disc_thickness_m=0.032, vent_solid_fraction=0.60, piston_count=4, piston_bore_m=0.024, caliper_mass_kg=6.60, pad_mass_kg=1.55, actuator_mass_kg=2.90)
def rear_brake_spec(): """Rear corner: 330 mm vented disc, two-piston caliper carrying the park-brake actuator.""" return BrakeCornerSpec(part_id="brakes", axle="rear", disc_outer_radius_m=0.1650, disc_inner_radius_m=0.1120, hat_radius_m=0.085, disc_thickness_m=0.022, vent_solid_fraction=0.65, piston_count=2, piston_bore_m=0.022, caliper_mass_kg=5.20, pad_mass_kg=1.15, actuator_mass_kg=4.15)
def tire_spec(): """255/45 R21 summer tyre on all four corners.""" return TireSpec( part_id="tires", section_width_m=0.255, aspect_ratio=0.45, rim_diameter_in=21.0, rolling_radius_factor=0.97, load_index_kg=950.0, peak_friction=1.04, slide_friction=0.86, load_sensitivity=0.09, friction_reference_load_fraction=0.55, cornering_stiffness_per_n=8.2, rolling_coefficient=0.0088, rolling_speed_coefficient=0.0000038, carcass_thickness_m=0.0098, carcass_density_kg_m3=1140.0, )
def pack_spec(): """Twenty-four 9s2p prismatic NMC modules wired in series into one 216s2p traction pack.""" return PackSpec( part_id="battery-modules", module_count=24, series_per_module=9, parallel_per_module=2, cell_nominal_voltage_v=3.68, cell_capacity_ah=57.9, cell_resistance_ohm=0.00072, cell_specific_energy_wh_kg=240.0, module_packaging_factor=1.142, module_interconnect_resistance_ohm=0.00022, pack_bus_resistance_ohm=0.0040, cell_pulse_charge_c_rate=2.0, cell_pulse_discharge_c_rate=5.0, )
def gearbox_spec(): """Two helical stages per drive unit; their pitch diameters are what make the 9.1:1 final drive.""" return GearboxSpec( part_id="reduction-gears", unit_count=2, first=GearStageSpec(pinion_pitch_diameter_m=0.062, wheel_pitch_diameter_m=0.217, face_width_m=0.045), second=GearStageSpec(pinion_pitch_diameter_m=0.078, wheel_pitch_diameter_m=0.2028, face_width_m=0.052), )
def motor_limits(motor: TractionMotorSpec, inverter: InverterSpec) !{}: """Solve one machine at its own current limit, then at whichever limit its inverter imposes.
Torque is the interior-permanent-magnet expression: magnet torque plus the reluctance term the declared saliency provides at the declared current angle. The base speed is where the stator flux linkage the same operating point produces can no longer be driven at the space-vector voltage ceiling, and the constant-power region runs from there to the declared maximum speed. Power factor comes out of the corner-point voltage and current phasors rather than being declared, so the inverter's apparent-power rating and the machine's shaft power stay consistent. """ current = min(motor.peak_phase_current_a, inverter.peak_phase_current_a) angle = motor.current_angle_deg * math.pi / 180.0 torque = ipm_shaft_torque(f64(motor.pole_pairs), motor.magnet_flux_linkage_wb, motor.d_axis_inductance_h, motor.q_axis_inductance_h, current, angle) flux = stator_flux_linkage(motor.magnet_flux_linkage_wb, motor.d_axis_inductance_h, motor.q_axis_inductance_h, current, angle) mechanical_rps = voltage_limited_speed(inverter.dc_link_voltage_v, flux, f64(motor.pole_pairs)) electrical_rps = mechanical_rps * f64(motor.pole_pairs) direct_current = 0.0 - current * math.sin(angle) quadrature_current = current * math.cos(angle) direct_voltage = 0.0 - electrical_rps * motor.q_axis_inductance_h * quadrature_current quadrature_voltage = electrical_rps * (motor.magnet_flux_linkage_wb + motor.d_axis_inductance_h * direct_current) real_power = 1.5 * (direct_voltage * direct_current + quadrature_voltage * quadrature_current) apparent_power = 1.5 * inverter.dc_link_voltage_v / ROOT_THREE * current switching_loss = INVERTER_SWITCH_COUNT * INVERTER_SWITCHING_FREQUENCY_HZ * INVERTER_SWITCHING_ENERGY_MJ_PER_A * current / 1000.0 conduction_loss = 3.0 * (current / ROOT_TWO) * (current / ROOT_TWO) * inverter.on_state_resistance_ohm inverter_loss = (switching_loss + conduction_loss) / 1000.0 corner_power = torque * mechanical_rps / 1000.0 return MotorLimits( part_id=motor.part_id, inverter_part_id=inverter.part_id, effective_peak_current_a=current, current_limited_by="motor-winding" if motor.peak_phase_current_a <= inverter.peak_phase_current_a else "inverter-rating", peak_torque_nm=torque, continuous_torque_nm=ipm_shaft_torque(f64(motor.pole_pairs), motor.magnet_flux_linkage_wb, motor.d_axis_inductance_h, motor.q_axis_inductance_h, motor.continuous_phase_current_a, angle), base_speed_rpm=mechanical_rps * 60.0 / (2.0 * math.pi), max_speed_rpm=motor.max_speed_rpm, constant_power_ratio=motor.max_speed_rpm / (mechanical_rps * 60.0 / (2.0 * math.pi)), corner_power_kw=corner_power, stator_flux_linkage_wb=flux, power_factor=real_power / apparent_power, inverter_apparent_power_kva=apparent_power / 1000.0, inverter_peak_power_kw=real_power / 1000.0, inverter_loss_kw=inverter_loss, dc_link_peak_current_a=(real_power / 1000.0 + inverter_loss) * 1000.0 / inverter.dc_link_voltage_v, airgap_shear_stress_kpa=airgap_shear_stress(torque, motor.rotor_radius_m, motor.stack_length_m) / 1000.0, )
def brake_axle_limits(corner: BrakeCornerSpec, rolling_radius_m: f64) !{}: """Pressure, clamp force, corner torque and the wheel force both corners of one axle can make.
The effective radius is the uniform-pressure integral over the swept annulus, and the disc thermal mass is the vented swept volume in cast iron. That thermal mass is what turns half m v squared into a disc temperature, so a heavier car heats the same disc further on the same stop. """ effective_radius = uniform_pressure_radius(corner.disc_outer_radius_m, corner.disc_inner_radius_m) clamp_force = caliper_clamp_force(MAX_LINE_PRESSURE_PA, f64(corner.piston_count), corner.piston_bore_m) torque = corner_brake_torque(PAD_FRICTION_COEFFICIENT, clamp_force, effective_radius) swept_volume = math.pi * (corner.disc_outer_radius_m * corner.disc_outer_radius_m - corner.hat_radius_m * corner.hat_radius_m) * corner.disc_thickness_m * corner.vent_solid_fraction disc_mass = DISC_DENSITY_KG_M3 * swept_volume return BrakeAxleLimits( part_id=corner.part_id, axle=corner.axle, effective_radius_m=effective_radius, piston_area_m2=clamp_force / MAX_LINE_PRESSURE_PA, clamp_force_n=clamp_force, corner_torque_nm=torque, axle_force_n=2.0 * torque / rolling_radius_m, disc_mass_kg=disc_mass, disc_thermal_capacity_j_k=disc_mass * DISC_SPECIFIC_HEAT_J_KG_K, )
def tire_limits(): """Turn the moulded tyre size into the radius, rated load and grip model the dynamics reads.""" spec = tire_spec() rim_radius = spec.rim_diameter_in * INCH_M / 2.0 unloaded_radius = rim_radius + spec.section_width_m * spec.aspect_ratio rated_load = spec.load_index_kg * GRAVITY_MPS2 return TireLimits( part_id=spec.part_id, unloaded_radius_m=unloaded_radius, rolling_radius_m=unloaded_radius * spec.rolling_radius_factor, rated_load_n=rated_load, friction_reference_load_n=rated_load * spec.friction_reference_load_fraction, peak_friction=spec.peak_friction, slide_friction=spec.slide_friction, load_sensitivity=spec.load_sensitivity, cornering_stiffness_per_n=spec.cornering_stiffness_per_n, rolling_coefficient=spec.rolling_coefficient, rolling_speed_coefficient=spec.rolling_speed_coefficient, )
def pack_limits(): """Sum 24 declared modules of cells into pack energy, voltage, resistance, power and mass.""" spec = pack_spec() series = spec.module_count * spec.series_per_module strings = spec.parallel_per_module cells = series * strings capacity_ah = f64(strings) * spec.cell_capacity_ah nominal_voltage = f64(series) * spec.cell_nominal_voltage_v cell_mass = spec.cell_nominal_voltage_v * spec.cell_capacity_ah / spec.cell_specific_energy_wh_kg return PackLimits( part_id=spec.part_id, cell_count=cells, series_count=series, parallel_count=strings, energy_kwh=nominal_voltage * capacity_ah / 1000.0, nominal_voltage_v=nominal_voltage, resistance_ohm=f64(series) * spec.cell_resistance_ohm / f64(strings) + f64(spec.module_count) * spec.module_interconnect_resistance_ohm + spec.pack_bus_resistance_ohm, charge_power_kw=nominal_voltage * spec.cell_pulse_charge_c_rate * capacity_ah / 1000.0, discharge_power_kw=nominal_voltage * spec.cell_pulse_discharge_c_rate * capacity_ah / 1000.0, mass_kg=f64(cells) * cell_mass * spec.module_packaging_factor, )
def final_drive_ratio(): """The 9.1:1 reduction is the product of the two declared stage pitch-diameter ratios.""" gearbox = gearbox_spec() return gearbox.first.wheel_pitch_diameter_m / gearbox.first.pinion_pitch_diameter_m * gearbox.second.wheel_pitch_diameter_m / gearbox.second.pinion_pitch_diameter_m
def gear_blank_mass(stage: GearStageSpec): """Both gear blanks of one stage as webbed steel discs of their pitch diameter and face width.""" area = math.pi / 4.0 * (stage.pinion_pitch_diameter_m * stage.pinion_pitch_diameter_m + stage.wheel_pitch_diameter_m * stage.wheel_pitch_diameter_m) return area * stage.face_width_m * STEEL_DENSITY_KG_M3 * GEAR_BLANK_WEB_FACTOR
def derived_part_mass(part_id: str) !{}: """Mass the declared specification implies for one part, summed over its declared instances.""" if part_id == "front-motor" or part_id == "rear-motor": motor = front_motor_spec() if part_id == "front-motor" else rear_motor_spec() return math.pi * motor.stator_outer_radius_m * motor.stator_outer_radius_m * motor.stack_length_m * MOTOR_ACTIVE_DENSITY_KG_M3 * MOTOR_ASSEMBLED_MASS_FACTOR if part_id == "front-inverter" or part_id == "rear-inverter": inverter = front_inverter_spec() if part_id == "front-inverter" else rear_inverter_spec() capacitance_mf = 0.5 * inverter.peak_phase_current_a / (2.0 * INVERTER_SWITCHING_FREQUENCY_HZ * INVERTER_DC_LINK_RIPPLE_V) * 1000.0 return capacitance_mf * INVERTER_CAPACITOR_KG_PER_MF + INVERTER_SWITCH_COUNT * inverter.peak_phase_current_a / 100.0 * INVERTER_MODULE_KG_PER_100A + inverter.housing_mass_kg if part_id == "brakes": radius = tire_limits().rolling_radius_m front = brake_axle_limits(front_brake_spec(), radius) rear = brake_axle_limits(rear_brake_spec(), radius) front_spec = front_brake_spec() rear_spec = rear_brake_spec() return 2.0 * (front.disc_mass_kg + front_spec.caliper_mass_kg + front_spec.pad_mass_kg + front_spec.actuator_mass_kg) + 2.0 * (rear.disc_mass_kg + rear_spec.caliper_mass_kg + rear_spec.pad_mass_kg + rear_spec.actuator_mass_kg) if part_id == "tires": spec = tire_spec() limits = tire_limits() rim_radius = spec.rim_diameter_in * INCH_M / 2.0 tread_area = 2.0 * math.pi * limits.unloaded_radius_m * spec.section_width_m sidewall_area = 2.0 * math.pi * (limits.unloaded_radius_m * limits.unloaded_radius_m - rim_radius * rim_radius) return 4.0 * (tread_area + sidewall_area) * spec.carcass_thickness_m * spec.carcass_density_kg_m3 if part_id == "battery-modules": return pack_limits().mass_kg gearbox = gearbox_spec() return f64(gearbox.unit_count) * (gear_blank_mass(gearbox.first) + gear_blank_mass(gearbox.second)) * GEARBOX_HOUSING_MASS_FACTOR
def declared_part(part_id: str): """The declared part this specification claims to describe. Fails loudly on an unknown id.""" for part in vehicle_parts(): if part.id == part_id: return part ensure false return vehicle_parts()[0]
def part_derivation(part_id: str): """One sentence naming what the derived mass for this part is actually built from.""" if part_id == "front-motor" or part_id == "rear-motor": return "active stack volume from the declared stator outer radius and stack length, at the shared active density and assembled-to-active mass ratio" if part_id == "front-inverter" or part_id == "rear-inverter": return "DC-link capacitance sized from the declared peak phase current and ripple budget, plus six power modules at the declared current rating, plus the declared housing" if part_id == "brakes": return "four vented cast-iron discs from the declared swept annulus, plus the declared caliper, pad and actuator masses per corner" if part_id == "tires": return "four carcasses as a tread band and two sidewall annuli at the declared thickness and compound density" if part_id == "battery-modules": return "432 cells at the declared cell specific energy, times the declared module packaging factor" return "four webbed steel gear blanks per unit at their declared pitch diameters and face widths, times the declared housing mass factor"
def reconcile_part(part_id: str, expected_count: int) !{}: """Check one specification against the part it claims to describe: identity, count and mass.""" part = declared_part(part_id) derived = derived_part_mass(part_id) error = abs(derived - part.mass_kg) / part.mass_kg return PartReconciliation( part_id=part_id, resolved=true, declared_count=part.count, expected_count=expected_count, declared_mass_kg=part.mass_kg, derived_mass_kg=derived, mass_error_fraction=error, tolerance_fraction=MASS_TOLERANCE_FRACTION, derivation=part_derivation(part_id), passed=part.count == expected_count and error <= MASS_TOLERANCE_FRACTION, )
def part_reconciliation() !{}: """Every specification checked against `vehicle_parts()`, in the order the drive chain runs.""" return [ reconcile_part("front-motor", 1), reconcile_part("rear-motor", 1), reconcile_part("front-inverter", 1), reconcile_part("rear-inverter", 1), reconcile_part("reduction-gears", 2), reconcile_part("brakes", 4), reconcile_part("tires", 4), reconcile_part("battery-modules", 24), ]
def net_current_limit(net_id: str): """Declared maximum current of one high-voltage net, read out of `vehicle_nets()`.""" for net in vehicle_nets("circuitframe"): if net.id == net_id: return net.maximum_current_a ensure false return 0.0
def generating_power(motor: MotorLimits): """Electrical power one machine can regenerate: its continuous torque at its own base speed.
A braking burst is short, but the machines still recover at their continuous rating rather than their peak: peak current is a traction-side thermal allowance the pad-and-disc side never needs. """ return motor.corner_power_kw * motor.continuous_torque_nm / motor.peak_torque_nm
def axle_hardware(axle: str, motor: MotorLimits, brake: BrakeAxleLimits, tire: TireLimits, ratio: f64, efficiency: f64): """Collapse one axle's declared machine, inverter, calipers and tyres into per-step scalars.""" return AxleHardware( axle=axle, brake_axle_force_n=brake.axle_force_n, drive_force_per_throttle_n=motor.peak_torque_nm * ratio * efficiency / tire.rolling_radius_m, power_force_numerator_w=motor.corner_power_kw * 1000.0 * efficiency, power_constraint="inverter-power" if motor.current_limited_by == "inverter-rating" else "motor-power", peak_friction=tire.peak_friction, slide_friction=tire.slide_friction, load_sensitivity=tire.load_sensitivity, friction_reference_load_n=tire.friction_reference_load_n, cornering_stiffness_per_n=tire.cornering_stiffness_per_n, fade_onset_temp_c=FADE_ONSET_TEMP_C, fade_rate_per_k=FADE_RATE_PER_K, fade_floor_fraction=FADE_FLOOR_FRACTION, disc_thermal_capacity_j_k=brake.disc_thermal_capacity_j_k, )
pub def hardware_limits() -> HardwareLimits !{}: """Derive every performance limit `physics.sema` needs, and refuse to return a drifted set.
The reconciliation runs first. A specification whose part id no longer resolves, whose instance count no longer matches the declared count, or whose derived mass has drifted outside the stated tolerance stops the build here rather than quietly feeding a wrong limit into the dynamics. """ reconciliation = part_reconciliation() for row in reconciliation: ensure row.resolved and row.passed front = motor_limits(front_motor_spec(), front_inverter_spec()) rear = motor_limits(rear_motor_spec(), rear_inverter_spec()) ensure front.dc_link_peak_current_a <= net_current_limit(front_inverter_spec().net_id) ensure rear.dc_link_peak_current_a <= net_current_limit(rear_inverter_spec().net_id) tire = tire_limits() pack = pack_limits() ratio = final_drive_ratio() efficiency = GEAR_MESH_EFFICIENCY * GEAR_MESH_EFFICIENCY front_brake = brake_axle_limits(front_brake_spec(), tire.rolling_radius_m) rear_brake = brake_axle_limits(rear_brake_spec(), tire.rolling_radius_m) regenerative_torque = front.continuous_torque_nm + rear.continuous_torque_nm driveline = MOTOR_EFFICIENCY * (1.0 - (front.inverter_loss_kw + rear.inverter_loss_kw) / (front.corner_power_kw + rear.corner_power_kw)) * efficiency return HardwareLimits( front_motor=front, rear_motor=rear, front_brake=front_brake, rear_brake=rear_brake, tire=tire, pack=pack, front_axle=axle_hardware("front", front, front_brake, tire, ratio, driveline), rear_axle=axle_hardware("rear", rear, rear_brake, tire, ratio, driveline), final_drive_ratio=ratio, gearbox_efficiency=efficiency, motor_efficiency=MOTOR_EFFICIENCY, inverter_efficiency=1.0 - (front.inverter_loss_kw + rear.inverter_loss_kw) / (front.corner_power_kw + rear.corner_power_kw), max_motor_torque_nm=front.peak_torque_nm + rear.peak_torque_nm, max_brake_force_n=front_brake.axle_force_n + rear_brake.axle_force_n, max_drive_power_kw=front.corner_power_kw + rear.corner_power_kw, max_regenerative_force_n=regenerative_torque * ratio * efficiency / tire.rolling_radius_m, max_regenerative_power_kw=min(pack.charge_power_kw, generating_power(front) + generating_power(rear)), regenerative_front_share=front.continuous_torque_nm / regenerative_torque, max_line_pressure_pa=MAX_LINE_PRESSURE_PA, pad_friction=PAD_FRICTION_COEFFICIENT, fade_onset_temp_c=FADE_ONSET_TEMP_C, fade_rate_per_k=FADE_RATE_PER_K, fade_floor_fraction=FADE_FLOOR_FRACTION, disc_cooling_base_w_per_k=DISC_COOLING_BASE_W_PER_K, disc_cooling_speed_w_per_k_mps=DISC_COOLING_SPEED_W_PER_K_MPS, driveline_top_speed_mps=min(front.max_speed_rpm, rear.max_speed_rpm) / 60.0 / ratio * 2.0 * math.pi * tire.rolling_radius_m, )
def parameter_row(part_id: str, key: str, label: str, value: f64, unit: str, source: str, note: str): """One declared scalar with the part it belongs to and how it was arrived at.""" return {"id": part_id + "." + key, "part_id": part_id, "label": label, "value": value, "unit": unit, "source": source, "note": note}
def motor_parameters(motor: TractionMotorSpec): """Provenance rows for one machine, read straight off the specification struct.""" return [ parameter_row(motor.part_id, "pole-pairs", "Pole pairs", f64(motor.pole_pairs), "1", SOURCE_ASSUMED, "eight-pole rotor, the usual choice for this speed and frequency class"), parameter_row(motor.part_id, "rotor-radius", "Rotor radius", motor.rotor_radius_m, "m", SOURCE_ASSUMED, "sized so the derived peak torque sits near 47 kPa of airgap shear stress"), parameter_row(motor.part_id, "stack-length", "Active stack length", motor.stack_length_m, "m", SOURCE_ASSUMED, "sets the torque per unit shear stress and, with the stator radius, the machine mass"), parameter_row(motor.part_id, "stator-outer-radius", "Stator outer radius", motor.stator_outer_radius_m, "m", SOURCE_ASSUMED, "outer envelope of the active stack; drives the reconciled machine mass"), parameter_row(motor.part_id, "magnet-flux-linkage", "Magnet flux linkage", motor.magnet_flux_linkage_wb, "Wb", SOURCE_ASSUMED, "800 V winding, so roughly twice the flux linkage and half the current of a 400 V machine"), parameter_row(motor.part_id, "d-axis-inductance", "d-axis inductance", motor.d_axis_inductance_h, "H", SOURCE_ASSUMED, "saturated value at peak current; no current-dependent inductance map is claimed"), parameter_row(motor.part_id, "q-axis-inductance", "q-axis inductance", motor.q_axis_inductance_h, "H", SOURCE_ASSUMED, "the declared saliency is what produces the reluctance share of the derived torque"), parameter_row(motor.part_id, "peak-phase-current", "Peak phase current", motor.peak_phase_current_a, "A", SOURCE_ASSUMED, "winding thermal and demagnetisation limit for a bounded peak-power burst"), parameter_row(motor.part_id, "continuous-phase-current", "Continuous phase current", motor.continuous_phase_current_a, "A", SOURCE_ASSUMED, "liquid-cooled continuous rating; sets the derived regenerative torque"), parameter_row(motor.part_id, "current-angle", "MTPA current angle", motor.current_angle_deg, "deg", SOURCE_CORRELATED, "fixed maximum-torque-per-amp angle; a real controller would walk this with speed and temperature"), parameter_row(motor.part_id, "max-speed", "Maximum speed", motor.max_speed_rpm, "rpm", SOURCE_ASSUMED, "rotor burst and bearing limit; with the base speed it sets the constant-power region"), ]
def inverter_parameters(inverter: InverterSpec): """Provenance rows for one inverter, read straight off the specification struct.""" return [ parameter_row(inverter.part_id, "dc-link-voltage", "DC link voltage", inverter.dc_link_voltage_v, "V", SOURCE_ASSUMED, "800 V class, consistent with the declared HV net voltage and the derived pack nominal"), parameter_row(inverter.part_id, "peak-phase-current", "Peak phase current", inverter.peak_phase_current_a, "A", SOURCE_ASSUMED, "silicon carbide module rating; caps the machine when it is below the winding limit"), parameter_row(inverter.part_id, "on-state-resistance", "On-state resistance", inverter.on_state_resistance_ohm, "ohm", SOURCE_ASSUMED, "per-phase conduction path through the paralleled dies"), parameter_row(inverter.part_id, "housing-mass", "Housing and coldplate mass", inverter.housing_mass_kg, "kg", SOURCE_ASSUMED, "front unit is standalone in the subframe, rear shares the drive-unit casting"), ]
def brake_parameters(corner: BrakeCornerSpec): """Provenance rows for one brake corner, read straight off the specification struct.""" return [ parameter_row(corner.part_id, corner.axle + ".disc-outer-radius", corner.axle + " disc outer radius", corner.disc_outer_radius_m, "m", SOURCE_ASSUMED, "swept annulus outer edge; with the inner edge it fixes the effective radius"), parameter_row(corner.part_id, corner.axle + ".disc-inner-radius", corner.axle + " disc inner radius", corner.disc_inner_radius_m, "m", SOURCE_ASSUMED, "inner edge of the pad sweep"), parameter_row(corner.part_id, corner.axle + ".hat-radius", corner.axle + " disc hat radius", corner.hat_radius_m, "m", SOURCE_ASSUMED, "mounting hat; excluded from the thermal mass because it is not swept"), parameter_row(corner.part_id, corner.axle + ".disc-thickness", corner.axle + " disc thickness", corner.disc_thickness_m, "m", SOURCE_ASSUMED, "vented disc overall thickness"), parameter_row(corner.part_id, corner.axle + ".vent-solid-fraction", corner.axle + " vented solid fraction", corner.vent_solid_fraction, "1", SOURCE_CORRELATED, "solid fraction of a vaned vented disc; sets both the disc mass and its thermal capacity"), parameter_row(corner.part_id, corner.axle + ".piston-count", corner.axle + " piston count", f64(corner.piston_count), "1", SOURCE_ASSUMED, "fixed-caliper pistons per side"), parameter_row(corner.part_id, corner.axle + ".piston-bore", corner.axle + " piston bore", corner.piston_bore_m, "m", SOURCE_ASSUMED, "with the line pressure this is the whole clamp force"), parameter_row(corner.part_id, corner.axle + ".caliper-mass", corner.axle + " caliper mass", corner.caliper_mass_kg, "kg", SOURCE_ASSUMED, "aluminium fixed caliper body"), parameter_row(corner.part_id, corner.axle + ".pad-mass", corner.axle + " pad set mass", corner.pad_mass_kg, "kg", SOURCE_ASSUMED, "both pads including backing plates"), parameter_row(corner.part_id, corner.axle + ".actuator-mass", corner.axle + " actuator mass", corner.actuator_mass_kg, "kg", SOURCE_ASSUMED, "brake-by-wire actuator and local electronics; the rear also carries the park-brake motor"), ]
def shared_parameters(): """Provenance rows for the constants shared across specifications and reconciliations.""" spec = tire_spec() pack = pack_spec() gearbox = gearbox_spec() return [ parameter_row("front-motor", "active-density", "Active stack mean density", MOTOR_ACTIVE_DENSITY_KG_M3, "kg/m^3", SOURCE_CORRELATED, "volume-mean of laminations, copper, magnets and slot air; shared by both machines"), parameter_row("front-motor", "assembled-mass-factor", "Assembled to active mass ratio", MOTOR_ASSEMBLED_MASS_FACTOR, "1", SOURCE_ASSUMED, "housing, shaft, bearings and end windings over the active stack; shared by both machines"), parameter_row("front-motor", "efficiency", "Shaft efficiency at the corner point", MOTOR_EFFICIENCY, "1", SOURCE_CORRELATED, "lumped copper, iron, magnet and windage loss for a liquid-cooled traction machine; no loss map is claimed, so this stays declared rather than derived"), parameter_row("front-inverter", "switching-frequency", "Switching frequency", INVERTER_SWITCHING_FREQUENCY_HZ, "Hz", SOURCE_ASSUMED, "shared silicon carbide switching frequency"), parameter_row("front-inverter", "switching-energy", "Switching energy coefficient", INVERTER_SWITCHING_ENERGY_MJ_PER_A, "mJ/A", SOURCE_CORRELATED, "turn-on, turn-off and reverse-recovery energy per amp at the declared DC link voltage"), parameter_row("front-inverter", "dc-link-ripple", "DC link ripple budget", INVERTER_DC_LINK_RIPPLE_V, "V", SOURCE_ASSUMED, "sizes the film capacitor, and through it the reconciled inverter mass"), parameter_row("front-inverter", "capacitor-specific-mass", "Capacitor specific mass", INVERTER_CAPACITOR_KG_PER_MF, "kg/mF", SOURCE_CORRELATED, "900 V film capacitor mass per millifarad"), parameter_row("front-inverter", "module-specific-mass", "Power module specific mass", INVERTER_MODULE_KG_PER_100A, "kg/100A", SOURCE_CORRELATED, "module, substrate and busbar mass per hundred amps per switch"), parameter_row("brakes", "pad-friction", "Pad friction coefficient", PAD_FRICTION_COEFFICIENT, "1", SOURCE_CORRELATED, "cold organic pad against cast iron; the fade curve degrades it with disc temperature"), parameter_row("brakes", "max-line-pressure", "Maximum line pressure", MAX_LINE_PRESSURE_PA, "Pa", SOURCE_ASSUMED, "160 bar at full pedal demand; the pedal-to-pressure map is taken as linear"), parameter_row("brakes", "disc-density", "Disc material density", DISC_DENSITY_KG_M3, "kg/m^3", SOURCE_ASSUMED, "grey cast iron"), parameter_row("brakes", "disc-specific-heat", "Disc specific heat", DISC_SPECIFIC_HEAT_J_KG_K, "J/(kg.K)", SOURCE_CORRELATED, "grey cast iron; with the swept mass this is the whole disc thermal capacity"), parameter_row("brakes", "fade-onset", "Fade onset temperature", FADE_ONSET_TEMP_C, "degC", SOURCE_CORRELATED, "temperature above which the declared pad compound starts losing friction"), parameter_row("brakes", "fade-rate", "Fade rate", FADE_RATE_PER_K, "1/K", SOURCE_CORRELATED, "fractional friction loss per kelvin above onset"), parameter_row("brakes", "fade-floor", "Fade floor", FADE_FLOOR_FRACTION, "1", SOURCE_CORRELATED, "residual friction fraction the compound never falls below"), parameter_row("brakes", "disc-cooling-base", "Disc cooling at rest", DISC_COOLING_BASE_W_PER_K, "W/K", SOURCE_CORRELATED, "natural convection and radiation from one disc"), parameter_row("brakes", "disc-cooling-speed", "Disc cooling per unit speed", DISC_COOLING_SPEED_W_PER_K_MPS, "W/(K.m/s)", SOURCE_CORRELATED, "forced convection through the vanes, linear in road speed"), parameter_row(spec.part_id, "section-width", "Section width", spec.section_width_m, "m", SOURCE_ASSUMED, "moulded tyre size 255/45 R21"), parameter_row(spec.part_id, "aspect-ratio", "Aspect ratio", spec.aspect_ratio, "1", SOURCE_ASSUMED, "sidewall height as a fraction of section width"), parameter_row(spec.part_id, "rim-diameter", "Rim diameter", spec.rim_diameter_in, "in", SOURCE_ASSUMED, "matches the declared alloy wheel"), parameter_row(spec.part_id, "rolling-radius-factor", "Rolling radius factor", spec.rolling_radius_factor, "1", SOURCE_CORRELATED, "loaded rolling radius over unloaded radius for a laden radial tyre"), parameter_row(spec.part_id, "load-index", "Load index mass", spec.load_index_kg, "kg", SOURCE_ASSUMED, "load index 106; four corners must cover the laden vehicle with margin"), parameter_row(spec.part_id, "peak-friction", "Peak friction coefficient", spec.peak_friction, "1", SOURCE_CORRELATED, "dry asphalt summer compound at the reference load"), parameter_row(spec.part_id, "slide-friction", "Sliding friction coefficient", spec.slide_friction, "1", SOURCE_CORRELATED, "past the peak of the slip curve; also the floor of the load-sensitivity model"), parameter_row(spec.part_id, "load-sensitivity", "Friction load sensitivity", spec.load_sensitivity, "1", SOURCE_CORRELATED, "fractional friction loss per unit of relative overload; this is how added mass costs grip"), parameter_row(spec.part_id, "friction-reference-load", "Friction reference load fraction", spec.friction_reference_load_fraction, "1", SOURCE_CORRELATED, "fraction of the load-index maximum at which the friction figures are quoted"), parameter_row(spec.part_id, "cornering-stiffness", "Cornering stiffness per unit load", spec.cornering_stiffness_per_n, "1/rad", SOURCE_CORRELATED, "linear-range slip stiffness per newton of vertical load, so an axle stiffens as it is loaded"), parameter_row(spec.part_id, "rolling-coefficient", "Rolling resistance coefficient", spec.rolling_coefficient, "1", SOURCE_CORRELATED, "standing-start value; ISO 28580 class for a low-rolling-resistance EV tyre"), parameter_row(spec.part_id, "rolling-speed-coefficient", "Rolling resistance speed term", spec.rolling_speed_coefficient, "s^2/m^2", SOURCE_CORRELATED, "quadratic speed growth from carcass hysteresis"), parameter_row(spec.part_id, "carcass-thickness", "Carcass thickness", spec.carcass_thickness_m, "m", SOURCE_ASSUMED, "mean rubber and belt thickness used for the reconciled tyre mass"), parameter_row(spec.part_id, "carcass-density", "Carcass density", spec.carcass_density_kg_m3, "kg/m^3", SOURCE_ASSUMED, "rubber, steel belt and textile composite"), parameter_row(pack.part_id, "module-count", "Module count", f64(pack.module_count), "1", SOURCE_ASSUMED, "checked against the declared part instance count"), parameter_row(pack.part_id, "series-per-module", "Cells in series per module", f64(pack.series_per_module), "1", SOURCE_ASSUMED, "24 modules of 9 in series make the 216s string"), parameter_row(pack.part_id, "parallel-per-module", "Cells in parallel per module", f64(pack.parallel_per_module), "1", SOURCE_ASSUMED, "sets pack capacity and divides the cell resistance"), parameter_row(pack.part_id, "cell-nominal-voltage", "Cell nominal voltage", pack.cell_nominal_voltage_v, "V", SOURCE_ASSUMED, "prismatic nickel-manganese-cobalt chemistry"), parameter_row(pack.part_id, "cell-capacity", "Cell capacity", pack.cell_capacity_ah, "Ah", SOURCE_ASSUMED, "with the series and parallel counts this is the whole pack energy"), parameter_row(pack.part_id, "cell-resistance", "Cell internal resistance", pack.cell_resistance_ohm, "ohm", SOURCE_CORRELATED, "DC internal resistance of a power-oriented prismatic cell at 25 degrees"), parameter_row(pack.part_id, "cell-specific-energy", "Cell specific energy", pack.cell_specific_energy_wh_kg, "Wh/kg", SOURCE_CORRELATED, "cell-level energy density; with the packaging factor it is the reconciled module mass"), parameter_row(pack.part_id, "module-packaging-factor", "Module packaging factor", pack.module_packaging_factor, "1", SOURCE_ASSUMED, "busbars, frame, cooling plate and sensing over bare cell mass"), parameter_row(pack.part_id, "module-interconnect-resistance", "Module interconnect resistance", pack.module_interconnect_resistance_ohm, "ohm", SOURCE_ASSUMED, "inter-module busbar and joint resistance"), parameter_row(pack.part_id, "pack-bus-resistance", "Pack bus resistance", pack.pack_bus_resistance_ohm, "ohm", SOURCE_ASSUMED, "contactors, fuse and pack terminals"), parameter_row(pack.part_id, "charge-c-rate", "Cell pulse charge rate", pack.cell_pulse_charge_c_rate, "1/h", SOURCE_CORRELATED, "regenerative charge acceptance for a bounded pulse; caps the derived regenerative power"), parameter_row(pack.part_id, "discharge-c-rate", "Cell pulse discharge rate", pack.cell_pulse_discharge_c_rate, "1/h", SOURCE_CORRELATED, "peak discharge pulse the cell will take"), parameter_row(gearbox.part_id, "stage-1-pinion", "First stage pinion pitch diameter", gearbox.first.pinion_pitch_diameter_m, "m", SOURCE_ASSUMED, "with the wheel diameter this is the 3.5:1 first stage"), parameter_row(gearbox.part_id, "stage-1-wheel", "First stage wheel pitch diameter", gearbox.first.wheel_pitch_diameter_m, "m", SOURCE_ASSUMED, "first reduction wheel"), parameter_row(gearbox.part_id, "stage-1-face", "First stage face width", gearbox.first.face_width_m, "m", SOURCE_ASSUMED, "helical face width; enters the reconciled gearbox mass only"), parameter_row(gearbox.part_id, "stage-2-pinion", "Second stage pinion pitch diameter", gearbox.second.pinion_pitch_diameter_m, "m", SOURCE_ASSUMED, "with the wheel diameter this is the 2.6:1 second stage"), parameter_row(gearbox.part_id, "stage-2-wheel", "Second stage wheel pitch diameter", gearbox.second.wheel_pitch_diameter_m, "m", SOURCE_ASSUMED, "output wheel on the differential carrier"), parameter_row(gearbox.part_id, "stage-2-face", "Second stage face width", gearbox.second.face_width_m, "m", SOURCE_ASSUMED, "helical face width; enters the reconciled gearbox mass only"), parameter_row(gearbox.part_id, "mesh-efficiency", "Mesh efficiency per stage", GEAR_MESH_EFFICIENCY, "1", SOURCE_CORRELATED, "loaded helical mesh, churning and bearing drag; applied once per stage"), parameter_row(gearbox.part_id, "blank-web-factor", "Gear blank web factor", GEAR_BLANK_WEB_FACTOR, "1", SOURCE_ASSUMED, "webbed blank mass over the solid disc of the same pitch diameter"), parameter_row(gearbox.part_id, "housing-mass-factor", "Gearbox housing mass factor", GEARBOX_HOUSING_MASS_FACTOR, "1", SOURCE_ASSUMED, "casing, shafts, bearings, oil and differential over the gear blanks"), ]
def hardware_parameters(): """The whole provenance ledger, built from the specification structs so it cannot drift.""" return (motor_parameters(front_motor_spec()) + motor_parameters(rear_motor_spec()) + inverter_parameters(front_inverter_spec()) + inverter_parameters(rear_inverter_spec()) + brake_parameters(front_brake_spec()) + brake_parameters(rear_brake_spec()) + shared_parameters())
def motor_export(limits: MotorLimits): return { "part_id": limits.part_id, "inverter_part_id": limits.inverter_part_id, "effective_peak_current_a": limits.effective_peak_current_a, "current_limited_by": limits.current_limited_by, "peak_torque_nm": limits.peak_torque_nm, "continuous_torque_nm": limits.continuous_torque_nm, "base_speed_rpm": limits.base_speed_rpm, "max_speed_rpm": limits.max_speed_rpm, "constant_power_ratio": limits.constant_power_ratio, "corner_power_kw": limits.corner_power_kw, "power_factor": limits.power_factor, "inverter_apparent_power_kva": limits.inverter_apparent_power_kva, "inverter_peak_power_kw": limits.inverter_peak_power_kw, "inverter_loss_kw": limits.inverter_loss_kw, "dc_link_peak_current_a": limits.dc_link_peak_current_a, "airgap_shear_stress_kpa": limits.airgap_shear_stress_kpa, }
def brake_export(limits: BrakeAxleLimits): return { "part_id": limits.part_id, "axle": limits.axle, "effective_radius_m": limits.effective_radius_m, "piston_area_m2": limits.piston_area_m2, "clamp_force_n": limits.clamp_force_n, "corner_torque_nm": limits.corner_torque_nm, "axle_force_n": limits.axle_force_n, "disc_mass_kg": limits.disc_mass_kg, "disc_thermal_capacity_j_k": limits.disc_thermal_capacity_j_k, }
def reconciliation_export(row: PartReconciliation): return { "part_id": row.part_id, "resolved": row.resolved, "declared_count": row.declared_count, "expected_count": row.expected_count, "declared_mass_kg": row.declared_mass_kg, "derived_mass_kg": row.derived_mass_kg, "mass_error_fraction": row.mass_error_fraction, "tolerance_fraction": row.tolerance_fraction, "derivation": row.derivation, "passed": row.passed, }
pub def hardware_export() -> dict[str, any] !{}: """API payload: the derived limits, the declared parameters behind them, and the reconciliation.""" limits = hardware_limits() tire = limits.tire pack = limits.pack return { "schema": "sema.circuitframe-hardware/v1", "authority": "engineered component assumptions for a reconstruction; no dynamometer, pressure-rig, tyre-rig or cell-cycling evidence exists for any value here", "method": "every performance limit is derived from the declared parameters of a part that already exists in vehicle.sema, and every specification is reconciled against that part's declared instance count and mass", "front_motor": motor_export(limits.front_motor), "rear_motor": motor_export(limits.rear_motor), "front_brake": brake_export(limits.front_brake), "rear_brake": brake_export(limits.rear_brake), "tire": { "part_id": tire.part_id, "size": "255/45 R21", "unloaded_radius_m": tire.unloaded_radius_m, "rolling_radius_m": tire.rolling_radius_m, "rated_load_n": tire.rated_load_n, "friction_reference_load_n": tire.friction_reference_load_n, "peak_friction": tire.peak_friction, "slide_friction": tire.slide_friction, "load_sensitivity": tire.load_sensitivity, "cornering_stiffness_per_n": tire.cornering_stiffness_per_n, "rolling_coefficient": tire.rolling_coefficient, "rolling_speed_coefficient": tire.rolling_speed_coefficient, }, "pack": { "part_id": pack.part_id, "cell_count": pack.cell_count, "series_count": pack.series_count, "parallel_count": pack.parallel_count, "energy_kwh": pack.energy_kwh, "nominal_voltage_v": pack.nominal_voltage_v, "resistance_ohm": pack.resistance_ohm, "charge_power_kw": pack.charge_power_kw, "discharge_power_kw": pack.discharge_power_kw, "mass_kg": pack.mass_kg, }, "driveline": { "part_id": "reduction-gears", "final_drive_ratio": limits.final_drive_ratio, "gearbox_efficiency": limits.gearbox_efficiency, "motor_efficiency": limits.motor_efficiency, "inverter_efficiency": limits.inverter_efficiency, "top_speed_mps": limits.driveline_top_speed_mps, }, "limits": { "max_motor_torque_nm": limits.max_motor_torque_nm, "max_drive_power_kw": limits.max_drive_power_kw, "max_brake_force_n": limits.max_brake_force_n, "max_regenerative_force_n": limits.max_regenerative_force_n, "max_regenerative_power_kw": limits.max_regenerative_power_kw, "regenerative_front_share": limits.regenerative_front_share, "max_line_pressure_pa": limits.max_line_pressure_pa, "pad_friction": limits.pad_friction, "fade_onset_temp_c": limits.fade_onset_temp_c, "fade_rate_per_k": limits.fade_rate_per_k, "fade_floor_fraction": limits.fade_floor_fraction, }, "chain": { "drive": "front-motor + rear-motor peak shaft torque at the effective peak current -> reduction-gears final drive and mesh efficiency -> tires rolling radius -> wheel force, held above the machine's own base speed to its corner shaft power and bounded by that axle's tyre friction circle. Where the inverter's current rating is below the winding rating it is the inverter that set the torque, and `current_limited_by` names which one did", "brake": "pedal demand -> brakes line pressure -> caliper clamp force -> corner torque at the derived effective radius -> tires rolling radius -> axle force, then bounded by the tyre friction circle on that axle's live vertical load", "energy": "battery-modules cells -> pack energy, nominal voltage and internal resistance -> terminal power and regenerative charge acceptance", }, "parameters": hardware_parameters(), "reconciliation": [reconciliation_export(row) for row in part_reconciliation()], "scientific_validated": false, }
pub def hardware_equations() -> list[dict[str, str]] !{}: """Manifest rows for the hardware model, in the same shape as physics.sema:dynamics_equations().""" return [ {"id": "ipm-torque", "source": "ipm_shaft_torque", "expression": "T = 3/2 p I cos(gamma) (lambda_m + (L_q - L_d) I sin(gamma))", "unit": "N.m", "owner": "Sema interior-permanent-magnet machine on declared winding parameters (solved on assumed inputs)"}, {"id": "stator-flux", "source": "stator_flux_linkage", "expression": "psi_s = sqrt((lambda_m - L_d I sin gamma)^2 + (L_q I cos gamma)^2)", "unit": "Wb", "owner": "Sema machine flux at the peak-torque operating point (solved)"}, {"id": "base-speed", "source": "voltage_limited_speed", "expression": "omega_m = V_dc / (sqrt(3) psi_s p)", "unit": "rad/s", "owner": "Sema space-vector voltage ceiling; start of the constant-power region (solved)"}, {"id": "inverter-power", "source": "motor_limits", "expression": "P_inv = 3/2 (V_dc/sqrt3) I cos(phi) - 6 f_sw k_sw I - 3 (I/sqrt2)^2 R_on", "unit": "kW", "owner": "Sema inverter apparent power at the derived corner power factor, less switching and conduction loss (solved on correlated loss coefficients)"}, {"id": "airgap-shear", "source": "airgap_shear_stress", "expression": "sigma = T / (2 pi r_rotor^2 l_stack)", "unit": "Pa", "owner": "Sema machine sizing check; a credible liquid-cooled peak is 40-55 kPa (correlated)"}, {"id": "brake-effective-radius", "source": "uniform_pressure_radius", "expression": "r_eff = 2/3 (r_o^3 - r_i^3) / (r_o^2 - r_i^2)", "unit": "m", "owner": "Sema uniform-pressure pad sweep (solved)"}, {"id": "brake-clamp", "source": "caliper_clamp_force", "expression": "F_clamp = p_line n_piston pi (d_bore/2)^2", "unit": "N", "owner": "Sema hydraulic caliper (exact)"}, {"id": "brake-torque", "source": "corner_brake_torque", "expression": "T_corner = 2 mu_pad F_clamp r_eff", "unit": "N.m", "owner": "Sema friction brake, two pads per disc (exact on a correlated pad friction)"}, {"id": "brake-fade", "source": "brake_fade_factor", "expression": "mu(T) = max(f_floor, 1 - k_fade max(0, T_disc - T_onset)) mu_pad", "unit": "1", "owner": "Sema pad fade curve above the declared onset temperature (correlated)"}, {"id": "disc-thermal-mass", "source": "brake_axle_limits", "expression": "C_disc = rho_iron pi (r_o^2 - r_hat^2) t phi_vent c_iron", "unit": "J/K", "owner": "Sema vented swept-volume disc thermal mass (solved on assumed geometry)"}, {"id": "tire-load-sensitivity", "source": "tire_friction", "expression": "mu(F_z) = clamp(mu_peak (1 - k_load (F_z/F_ref - 1)), mu_slide, mu_peak)", "unit": "1", "owner": "Sema tyre load sensitivity; this is why added mass buys less grip than it costs (correlated)"}, {"id": "tire-rolling", "source": "rolling_coefficient", "expression": "f_r(v) = f_0 + f_2 v^2", "unit": "1", "owner": "Sema rolling resistance with a quadratic speed term (correlated)"}, {"id": "tire-radius", "source": "tire_limits", "expression": "r_roll = k_roll (d_rim/2 + w_section a_ratio)", "unit": "m", "owner": "Sema moulded tyre size to loaded rolling radius (solved on a correlated deflection factor)"}, {"id": "pack-topology", "source": "pack_limits", "expression": "E = n_s n_p V_cell C_cell; V = n_s V_cell; R = n_s R_cell / n_p + n_mod R_link + R_bus", "unit": "kWh, V, ohm", "owner": "Sema series-parallel cell summation over the declared modules (solved)"}, {"id": "pack-mass-closure", "source": "reconcile_part", "expression": "|m_derived - m_declared| / m_declared <= tol", "unit": "1", "owner": "Sema specification-to-part reconciliation; fails the build on drift (solved)"}, {"id": "final-drive", "source": "final_drive_ratio", "expression": "i = (d_w1/d_p1) (d_w2/d_p2)", "unit": "1", "owner": "Sema two-stage helical reduction from the declared pitch diameters (exact)"}, ]
test "every hardware specification resolves to a declared part with the declared count and mass": rows = part_reconciliation() ensure len(rows) == 8 for row in rows: ensure row.resolved ensure row.declared_count == row.expected_count ensure row.mass_error_fraction <= row.tolerance_fraction ensure row.passed ensure declared_part("battery-modules").count == 24 ensure declared_part("brakes").count == 4 ensure declared_part("tires").count == 4 ensure declared_part("reduction-gears").count == 2
test "derived pack energy, nominal voltage and mass land on the declared pack": pack = pack_limits() ensure pack.cell_count == 432 and pack.series_count == 216 and pack.parallel_count == 2 ensure pack.energy_kwh > 91.0 and pack.energy_kwh < 93.0 ensure pack.nominal_voltage_v > 760.0 and pack.nominal_voltage_v < 840.0 ensure abs(pack.mass_kg - 438.0) < 0.02 * 438.0 ensure pack.resistance_ohm > 0.0 and pack.resistance_ohm < 0.15 ensure pack.charge_power_kw > 150.0 and pack.charge_power_kw < 220.0 ensure pack.discharge_power_kw > pack.charge_power_kw
test "the declared reduction stages make the 9.1 final drive and a credible tyre radius": limits = hardware_limits() ensure abs(limits.final_drive_ratio - 9.1) < 0.000000001 ensure abs(limits.tire.rolling_radius_m - 0.3695) < 0.005 ensure limits.tire.rolling_radius_m < limits.tire.unloaded_radius_m ensure limits.driveline_top_speed_mps > 55.0 and limits.driveline_top_speed_mps < 90.0
test "each machine runs at the lower of its own winding limit and its inverter rating": limits = hardware_limits() ensure limits.front_motor.current_limited_by == "motor-winding" ensure limits.rear_motor.current_limited_by == "inverter-rating" ensure limits.rear_motor.effective_peak_current_a == rear_inverter_spec().peak_phase_current_a ensure limits.front_motor.effective_peak_current_a == front_motor_spec().peak_phase_current_a ensure limits.rear_motor.peak_torque_nm > limits.front_motor.peak_torque_nm for motor in [limits.front_motor, limits.rear_motor]: ensure motor.airgap_shear_stress_kpa > 30.0 and motor.airgap_shear_stress_kpa < 60.0 ensure motor.constant_power_ratio > 2.5 and motor.constant_power_ratio < 5.0 ensure motor.base_speed_rpm > 3000.0 and motor.base_speed_rpm < 6500.0 ensure motor.power_factor > 0.8 ensure motor.dc_link_peak_current_a < 520.0
test "the derived limits reproduce the literals they replace": limits = hardware_limits() ensure abs(limits.max_motor_torque_nm - 660.0) < 0.02 * 660.0 ensure abs(limits.max_drive_power_kw - 320.0) < 0.06 * 320.0 ensure abs(limits.pack.energy_kwh - 92.0) < 0.02 * 92.0 ensure limits.max_regenerative_force_n > 5000.0 and limits.max_regenerative_force_n < 7000.0 # The 180 kW regenerative literal is what the declared cells will accept, and the pack lands on # it. The machines cannot deliver it: two liquid-cooled stators generate 133 kW at their # continuous rating, so the recovery limit is the drive units, not the pack. ensure abs(limits.pack.charge_power_kw - 180.0) < 0.06 * 180.0 ensure limits.max_regenerative_power_kw < limits.pack.charge_power_kw ensure limits.max_regenerative_power_kw > 110.0 and limits.max_regenerative_power_kw < 160.0 # The brake force is the one limit that must not reproduce its literal: 14.5 kN was below the # tyre limit at kerb mass, which would make every stop brake-bound. A real system is sized above # the contact patch it feeds. ensure limits.max_brake_force_n > 25000.0
test "the provenance ledger covers every specification and carries only known source tags": rows = hardware_parameters() ensure len(rows) >= 80 mut seen = [] for row in rows: ensure row["source"] == SOURCE_SOLVED or row["source"] == SOURCE_CORRELATED or row["source"] == SOURCE_ASSUMED ensure len(str(row["note"])) > 0 ensure not contains_id(seen, str(row["id"])) seen = seen + [str(row["id"])] for part_id in ["front-motor", "rear-motor", "front-inverter", "rear-inverter", "brakes", "tires", "battery-modules", "reduction-gears"]: ensure ledger_covers(rows, part_id)
def contains_id(seen: list[str], value: str): for item in seen: if item == value: return true return false
def ledger_covers(rows: list[dict[str, any]], part_id: str): for row in rows: if row["part_id"] == part_id: return true return false
test "the ledger reports the same numbers the specification structs carry": rows = hardware_parameters() motor = rear_motor_spec() pack = pack_spec() mut checked = 0 for row in rows: if row["id"] == "rear-motor.magnet-flux-linkage": ensure row["value"] == motor.magnet_flux_linkage_wb checked = checked + 1 if row["id"] == "rear-motor.peak-phase-current": ensure row["value"] == motor.peak_phase_current_a checked = checked + 1 if row["id"] == "battery-modules.cell-capacity": ensure row["value"] == pack.cell_capacity_ah checked = checked + 1 if row["id"] == "brakes.front.piston-bore": ensure row["value"] == front_brake_spec().piston_bore_m checked = checked + 1 ensure checked == 4
test "the export carries the derived limits, the parameters and the reconciliation": payload = hardware_export() ensure payload["schema"] == "sema.circuitframe-hardware/v1" ensure len(payload["reconciliation"]) == 8 ensure len(payload["parameters"]) >= 80 ensure payload["limits"]["max_motor_torque_nm"] > 600.0 ensure payload["front_motor"]["current_limited_by"] == "motor-winding" ensure payload["rear_motor"]["current_limited_by"] == "inverter-rating" for row in payload["reconciliation"]: ensure row["passed"] and row["resolved"] rows = hardware_equations() ensure len(rows) >= 12 for row in rows: ensure len(row["id"]) > 0 and len(row["source"]) > 0 and len(row["expression"]) > 0 and len(row["unit"]) > 0 and len(row["owner"]) > 0
test "tyre friction falls as the corner it carries is loaded": tire = tire_limits() light = tire_friction(tire.peak_friction, tire.slide_friction, tire.load_sensitivity, tire.friction_reference_load_n, 0.5 * tire.friction_reference_load_n) reference = tire_friction(tire.peak_friction, tire.slide_friction, tire.load_sensitivity, tire.friction_reference_load_n, tire.friction_reference_load_n) heavy = tire_friction(tire.peak_friction, tire.slide_friction, tire.load_sensitivity, tire.friction_reference_load_n, 2.0 * tire.friction_reference_load_n) ensure abs(reference - tire.peak_friction) < 0.000000001 ensure light == tire.peak_friction ensure heavy < reference ensure heavy >= tire.slide_friction ensure tire_friction(tire.peak_friction, tire.slide_friction, tire.load_sensitivity, tire.friction_reference_load_n, 20.0 * tire.friction_reference_load_n) == tire.slide_friction
test "pad friction is unaffected below the fade onset and degrades above it": limits = hardware_limits() cold = brake_fade_factor(limits.fade_onset_temp_c - 100.0, limits.fade_onset_temp_c, limits.fade_rate_per_k, limits.fade_floor_fraction) warm = brake_fade_factor(limits.fade_onset_temp_c + 200.0, limits.fade_onset_temp_c, limits.fade_rate_per_k, limits.fade_floor_fraction) cooked = brake_fade_factor(limits.fade_onset_temp_c + 2000.0, limits.fade_onset_temp_c, limits.fade_rate_per_k, limits.fade_floor_fraction) ensure cold == 1.0 ensure warm < 1.0 and warm > limits.fade_floor_fraction ensure cooked == limits.fade_floor_fractionsrc/live.sema
Section titled “src/live.sema”"""Automotive-native Sema HTTP service for CircuitFrame Lab.
Everything expensive is solved once when the service starts: the source-panel aerodynamic basis, bothreduced-order surrogates, the mass and joint budget, and the four printed-circuit boards with their DCnodal solutions. The 20 Hz drive loop and the on-demand wind-tunnel measurement then reuse those caches."""
import httpimport mathfrom std.adaptive_dynamics import RegimeStatus, append_boundedfrom std.json import decode as decode_json, encode as encode_jsonfrom magna_ev_digital_twin.adaptation import evaluate_yaw_candidate, initial_aero_detector, regime_label, retained_adaptation, update_aero_detectorfrom magna_ev_digital_twin.aero import aero_basis_export, aero_equations, aero_solvefrom magna_ev_digital_twin.assembly import assembly_equations, assembly_exportfrom magna_ev_digital_twin.hardware import hardware_equations, hardware_exportfrom magna_ev_digital_twin.authoring import compile_circuit_descriptionfrom magna_ev_digital_twin.candidate import optimize_panelfrom magna_ev_digital_twin.pcb import pcb_equations, pcb_exportfrom magna_ev_digital_twin.physics import ControlInput, PhysicsContext, aero_holdout_metrics, dynamics_equations, initial_vehicle_state, physics_context, step_vehiclefrom magna_ev_digital_twin.vehicle import source_gate, vehicle_manifest, vehicle_net_comparison
assure silver
MAX_WIND_MPS = 60.0AERO_PROVENANCE = { "cd": "solved+correlated", "cl": "solved+correlated", "cy": "solved+correlated", "cmz": "solved+correlated", "cd_pressure": "solved topology + correlated base pressure", "cd_base": "solved topology + correlated base pressure", "cd_friction": "correlated", "cd_wheels": "correlated", "drag_n": "solved+correlated", "lift_n": "solved+correlated", "side_n": "solved+correlated", "yaw_moment_nm": "solved+correlated", "front_lift_n": "solved+correlated", "rear_lift_n": "solved+correlated", "dynamic_pressure_pa": "exact", "reynolds_number": "exact", "separation_x_m": "solved", "cp_min": "solved", "cp_max": "solved", "base_pressure_coefficient": "correlated", "wake_width_m": "solved", "wake_deficit_fraction": "correlated", "wake_shedding_hz": "correlated", "turbulence_intensity": "correlated", "wake_recirculation_length_m": "correlated", "dalembert_residual_cd": "numerical", "solver_residual": "numerical", "force_closure_residual": "numerical",}AERO_METHOD = "3D constant-strength source-panel solve with an image ground plane, closed by an empirical viscous and base-pressure model; not a governing CFD field"RESIDUAL_HISTORY_CAPACITY = 120SWEEP_STEPS = 200SWEEP_RAMP_STEPS = 40SWEEP_TARGET_SIDESLIP_DEG = 25.0SWEEP_MIN_WIND_MPS = 6.0SWEEP_MAX_WIND_MPS = 30.0# The candidate's declared validity region, mirrored from `evaluate_yaw_candidate` in adaptation.sema# so the viewer can state the limits before a demonstration runs instead of hard-coding them.# `the declared validity region is published and enforced` pins the two together.ENVELOPE_MAX_SPEED_MPS = 55.0ENVELOPE_MAX_SIDESLIP_DEG = 35.0CALM_MODEL_ID = "aero-rom-calm-v1"SAMPLED_EQUATION_IDS = ["surrogate-fit", "mass-budget", "panel-neumann", "panel-influence", "panel-ground-image", "centre-of-mass", "parallel-axis", "axle-load-split", "four-bar-closure", "motion-ratio", "air-spring", "bump-steer", "ipm-torque", "stator-flux", "base-speed", "inverter-power", "airgap-shear", "brake-effective-radius", "brake-clamp", "brake-torque", "disc-thermal-mass", "tire-radius", "pack-topology", "pack-mass-closure", "final-drive"]PER_STEP_EQUATION_IDS = ["thermal-network"]THERMAL_EQUATION_IDS = ["thermal-network", "thermal-tire", "thermal-stability"]
def response(status: int, body: any) !{}: return { "status": status, "content_type": "application/json", "headers": {"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"}, "body": encode_json(body), }
def error_response(status: int, code: str, detail: str) !{}: return response(status, {"schema": "sema.circuitframe-error/v1", "error": code, "detail": detail})
def physical_actions_allowed(): return source_gate()["physical_completeness"]
def evidence_export_allowed(): return source_gate()["presentation_qualified"]
def initial_session(context: PhysicsContext): detector = initial_aero_detector() return { "generation": 0, "sequence": 0, "state_version": 0, "state": initial_vehicle_state(), "frame": None, "detector": detector, "model_id": "aero-rom-calm-v1", "adaptation": retained_adaptation(detector, "aero-rom-calm-v1"), "candidate": None, "panel_width_mm": 90.0, "vehicle_revision": 1, "assembly_step": 11, "configuration_variant": "circuitframe", "context": context, "aero_basis_payload": None, "pcb_payload": None, "assembly_payload": None, "hardware_payload": None, "residual_history": initial_residual_history(), "sweep": idle_sweep(), }
def cached_aero_basis(session: dict[str, any]): if session["aero_basis_payload"] is None: session["aero_basis_payload"] = aero_basis_export(session["context"].basis) return session["aero_basis_payload"]
def cached_pcb(session: dict[str, any]): if session["pcb_payload"] is None: session["pcb_payload"] = pcb_export() return session["pcb_payload"]
def cached_assembly(session: dict[str, any]): if session["assembly_payload"] is None: session["assembly_payload"] = assembly_export() return session["assembly_payload"]
def cached_hardware(session: dict[str, any]): """The derived component limits and their reconciliation. Solved once with the context.""" if session["hardware_payload"] is None: session["hardware_payload"] = hardware_export() return session["hardware_payload"]
def progress_report(): return [ {"id": "foundation", "label": "Sema automotive core", "percent": 100, "state": "implemented"}, {"id": "vehicle", "label": "Evidence-aware EV reconstruction", "percent": 88, "state": "attributed exterior with separated wheel surfaces plus 737-feature FreeCAD/OCCT mechanical assembly, source-measured wheel/body clearances, wheelhouse-cut chassis, axle-bounded battery packaging, mounted controllers, wheel/occupant route collision gates, declared-part semantic ledger, and source-gated browser LODs; OEM manufacturing authority and validation remain open"}, {"id": "circuit", "label": "Conventional / CircuitFrame E/E", "percent": 78, "state": "four reconstructed boards with pin-level nets, layer stackups, routed copper, vias and DC modified-nodal-analysis solutions, IPC-2221 conductor sizing, IEC 60664-1 creepage gates, generated netlists, and a quantified harness-versus-structural-conductor comparison over the same nine vehicle nets; packaging, EMC, shielding and physical validation remain open"}, {"id": "motion", "label": "Coupled vehicle dynamics", "percent": 72, "state": "live longitudinal, lateral, yaw, tire, equivalent-circuit battery and eight-node coupled thermal network driven by mass, centre of gravity and yaw inertia summed from the declared part placements; production parameterisation and correlation absent"}, {"id": "flow", "label": "Aerodynamics", "percent": 74, "state": "240-panel constant-strength source solve with an image ground plane, two exact freestream modes, Stratford separation walks and a calibrated base-pressure closure produce yaw-dependent drag, lift, side force and yaw moment; the browser advects smoke through the same solved singularity field. Governing CFD, wind-tunnel correlation and transient wake resolution remain absent"}, {"id": "build", "label": "Manufacturing and robot sequence", "percent": 40, "state": "complete canonical assembly by default with operation scrubbing, named parametric robot/cell geometry, and declared-operation replay; reach, collision, tooling, and process feasibility remain unvalidated"}, {"id": "experience", "label": "Premium web experience", "percent": 94, "state": "attributed metallic exterior, source-gated mechanical LODs, engineering cutaways, model-matched wheel kinematics, configuration comparison, wind-tunnel smoke and surface-pressure modes, live multiphysics, and evidence-aware inspection"}, {"id": "handoff", "label": "Engineering export", "percent": 58, "state": "source ledger, evidence manifest, selected-configuration GLB, FCStd/STEP engineering reconstruction, generated board netlists, and semantic BOM/BOP declarations available; receiving-tool reconciliation and FMI remain unavailable"}, ]
def contains(values: list[str], value: str): for item in values: if item == value: return true return false
def equation_phase(id: str, live: bool): """Where a body is evaluated: inside every 20 Hz step, once at startup, or only when asked.""" if contains(SAMPLED_EQUATION_IDS, id): return "sampled" if contains(PER_STEP_EQUATION_IDS, id): return "per_step" return "on_demand" if not live else "per_step"
def grouped_equations(rows: list[dict[str, str]], group: str, live: bool): """Tag each declared equation with the subsystem that owns it and when its body runs.""" return [{"id": row["id"], "source": row["source"], "expression": row["expression"], "unit": row["unit"], "owner": row["owner"], "group": "thermal network" if contains(THERMAL_EQUATION_IDS, row["id"]) else group, "phase": equation_phase(row["id"], live)} for row in rows]
def initial_residual_history(): return {"residual": [], "score": [], "count": 0, "last_sequence": 0, "activated_at_sequence": 0}
def pushed_residual_history(history: dict[str, any], residual: f64, score: f64, sequence: int, activated: bool) !{}: """Bounded ring of the surrogate-versus-solved residual and the detector score that tracks it.""" return { "residual": append_bounded(history["residual"], residual, RESIDUAL_HISTORY_CAPACITY), "score": append_bounded(history["score"], score, RESIDUAL_HISTORY_CAPACITY), "count": history["count"] + 1, "last_sequence": sequence, "activated_at_sequence": sequence if activated else history["activated_at_sequence"], }
def idle_sweep(): return {"active": false, "index": 0, "commanded_wind_mps": 0.0, "commanded_yaw_deg": 0.0, "restarted_from_calm": false}
def sweep_command(index: int, speed_mps: f64): """Wind speed and bearing that put the apparent wind at the scripted sideslip. Returns [m/s, deg].
The demonstration commands a sideslip, not a wind. A fixed crosswind that walks a parked car clear of the calm model's window barely moves the apparent angle at motorway speed, so a fixed wind would quietly stop demonstrating anything above about 30 m/s. Solving the wind vector for the target angle keeps it meaningful at any road speed, and both commanded values are published every step so nothing about the operating point is hidden. """ target_rad = SWEEP_TARGET_SIDESLIP_DEG * min(1.0, f64(index) / f64(SWEEP_RAMP_STEPS)) * math.pi / 180.0 lateral_demand = max(0.0, speed_mps) * math.sin(target_rad) wind = max(SWEEP_MIN_WIND_MPS, min(SWEEP_MAX_WIND_MPS, lateral_demand / 0.8)) bearing = target_rad + math.asin(min(1.0, lateral_demand / wind)) return [wind, bearing * 180.0 / math.pi]
def advanced_sweep(sweep: dict[str, any], controls: ControlInput): if not sweep["active"]: return sweep index = sweep["index"] + 1 return {"active": index < SWEEP_STEPS, "index": index, "commanded_wind_mps": controls.wind_mps, "commanded_yaw_deg": controls.wind_yaw_deg, "restarted_from_calm": sweep["restarted_from_calm"]}
def detector_body(session: dict[str, any]): detector = session["detector"] return { "score": detector.score, "threshold": detector.score_threshold, "status": regime_label(detector), "consecutive_high": detector.consecutive_high, "required_consecutive": detector.required_consecutive, "window_size": detector.window_size, "min_samples": detector.min_samples, "baseline_mean": detector.baseline_mean, "baseline_scale": detector.baseline_scale, }
def residual_history_body(session: dict[str, any]): """The whole ring, served on status only; a step carries one new sample and the client appends.""" history = session["residual_history"] return { "capacity": RESIDUAL_HISTORY_CAPACITY, "count": history["count"], "last_sequence": history["last_sequence"], "activated_at_sequence": history["activated_at_sequence"], "threshold": session["detector"].score_threshold, "residual": history["residual"], "score": history["score"], }
def sweep_body(session: dict[str, any]): sweep = session["sweep"] return { "kind": "scripted demonstration: a bounded ramp that walks the apparent-wind sideslip out of the calm surrogate's fitted window and holds it there. The detector, the validation gate and any activation that follow are the ordinary live lifecycle, not a replay", "active": sweep["active"], "step_index": sweep["index"], "total_steps": SWEEP_STEPS, "remaining_steps": max(0, SWEEP_STEPS - sweep["index"]), "ramp_steps": SWEEP_RAMP_STEPS, "target_sideslip_deg": SWEEP_TARGET_SIDESLIP_DEG, "min_wind_mps": SWEEP_MIN_WIND_MPS, "max_wind_mps": SWEEP_MAX_WIND_MPS, "envelope_max_speed_mps": ENVELOPE_MAX_SPEED_MPS, "envelope_max_sideslip_deg": ENVELOPE_MAX_SIDESLIP_DEG, "drives": ["wind_mps", "wind_yaw_deg"], "commanded_wind_mps": sweep["commanded_wind_mps"], "commanded_yaw_deg": sweep["commanded_yaw_deg"], "restarted_from_calm": sweep["restarted_from_calm"], }
def all_equations(): return (grouped_equations(dynamics_equations(), "dynamics", true) + grouped_equations(aero_equations(), "aero panel solve", true) + grouped_equations(pcb_equations(), "board analysis", false) + grouped_equations(assembly_equations(), "assembly", false) + grouped_equations(hardware_equations(), "declared hardware", false))
def status_body(session: dict[str, any]): context = session["context"] parameters = context.parameters return { "schema": "sema.circuitframe-live-status/v1", "backend": "Sema", "configuration_id": "circuitframe_gt_01", "configuration_variant": session["configuration_variant"], "configuration_revision": session["vehicle_revision"], "generation": session["generation"], "sequence": session["sequence"], "state_version": session["state_version"], "update_hz": 20, "state": session["state"], "frame": session["frame"], "model_id": session["model_id"], "adaptation": session["adaptation"], "detector": detector_body(session), "residual_history": residual_history_body(session), "sweep": sweep_body(session), "equations": all_equations(), "vehicle": vehicle_manifest(session["configuration_variant"]), "vehicle_nets": vehicle_net_comparison(), "panel": {"id": "sill-panel-left", "width_mm": session["panel_width_mm"], "revision": session["vehicle_revision"], "fidelity": "derived"}, "assembly_step": session["assembly_step"], "progress": progress_report(), "mass_properties": { "total_mass_kg": parameters.mass_kg, "cg_to_front_axle_m": parameters.cg_to_front_axle_m, "cg_to_rear_axle_m": parameters.cg_to_rear_axle_m, "cg_height_m": parameters.cg_height_m, "yaw_inertia_kg_m2": parameters.yaw_inertia_kg_m2, "wheelbase_m": parameters.wheelbase_m, "track_m": parameters.track_m, "front_mass_fraction": parameters.front_mass_fraction, "source": "mass, centre of gravity and yaw inertia summed from the declared part placements in assembly.sema; track read from the declared suspension hardpoints", }, "hardware": cached_hardware(session), "surrogates": { "active": {"id": context.calm.model_id, "fit_max_yaw_deg": context.calm.fit_max_yaw_rad * 180.0 / 3.141592653589793, "fit_nodes": context.calm.fit_nodes, "fit_rms_cd": context.calm.fit_rms_cd}, "candidate": {"id": context.crosswind.model_id, "fit_max_yaw_deg": context.crosswind.fit_max_yaw_rad * 180.0 / 3.141592653589793, "fit_nodes": context.crosswind.fit_nodes, "fit_rms_cd": context.crosswind.fit_rms_cd}, "holdout": aero_holdout_metrics(context), }, "capabilities": { "live_reduced_dynamics": true, "coupled_longitudinal_lateral_yaw": true, "equivalent_circuit_battery": true, "coupled_thermal_network": true, "derived_mass_budget": true, "derived_hardware_limits": true, "component_reconciled_performance": true, "source_panel_aerodynamics": true, "yaw_dependent_aero_coefficients": true, "wind_tunnel_measurement": true, "surface_pressure_field": true, "pin_level_netlist": true, "dc_nodal_analysis": true, "ipc_conductor_sizing": true, "kinematic_joint_model": true, "adaptive_aero_model": true, "candidate_search_algorithm": true, "physical_candidate_evaluation": false, "robot_kinematic_replay": true, "parametric_mechanical_assembly": true, "step_engineering_reconstruction": true, "evidence_export": true, "qualification_matrix": true, "governing_cfd": false, "governing_structural_dynamics": false, "manufacturing_cad_authority": false, "decision_authority": false, }, "fidelity": { "vehicle_geometry": "CC BY community exterior + source-gated FreeCAD/OCCT engineering reconstruction; not OEM manufacturing CAD", "dynamics": "live coupled reduced longitudinal/lateral/yaw/tire model on assembly-derived inertia, with every performance limit derived from the declared motors, inverters, brakes, tyres, pack and reduction gears and reconciled against those parts' declared counts and masses; uncorrelated parameters and no dynamometer, pressure-rig, tyre-rig or cell-cycling evidence", "aerodynamics": AERO_METHOD, "electrical": "live equivalent-circuit battery plus DC modified-nodal-analysis over reconstructed boards; EMC, transient and physical routing validation absent", "thermal": "live eight-node coupled coolant network, a lumped tire node and a per-axle brake-disc node integrating the friction share of each stop against the derived disc thermal mass; no 3D thermal solver or correlation", "structure": "live axle reactions, joint mobility and static load-path equilibrium only; no stress, deformation, modal, fatigue, or crash solve", "robotics": "deterministic kinematic demonstrator", }, "scientific_validated": false, "production_complete": false, "major_assembly_complete": false, }
def number_field_valid(payload: dict[str, any], field: str, minimum: f64, maximum: f64): if not payload.has(field) or not (payload[field] is int or payload[field] is float): return false value = f64(payload[field]) return value == value and value >= minimum and value <= maximum
def measurement_body(forces: any): return { "cd": forces.cd, "cl": forces.cl, "cy": forces.cy, "cmz": forces.cmz, "cd_pressure": forces.cd_pressure, "cd_base": forces.cd_base, "cd_friction": forces.cd_friction, "cd_wheels": forces.cd_wheels, "drag_n": forces.drag_n, "lift_n": forces.lift_n, "side_n": forces.side_n, "yaw_moment_nm": forces.yaw_moment_nm, "front_lift_n": forces.front_lift_n, "rear_lift_n": forces.rear_lift_n, "dynamic_pressure_pa": forces.dynamic_pressure_pa, "reynolds_number": forces.reynolds_number, "separation_x_m": forces.separation_x_m, "cp_min": forces.cp_min, "cp_max": forces.cp_max, "base_pressure_coefficient": forces.base_pressure_coefficient, "wake_width_m": forces.wake_width_m, "wake_deficit_fraction": forces.wake_deficit_fraction, "wake_shedding_hz": forces.wake_shedding_hz, "turbulence_intensity": forces.turbulence_intensity, "wake_recirculation_length_m": forces.wake_recirculation_length_m, "dalembert_residual_cd": forces.dalembert_residual_cd, "solver_residual": forces.solver_residual, "force_closure_residual": forces.force_closure_residual, }
def tunnel_response(payload: any, session: dict[str, any]) !{}: """On-demand wind-tunnel measurement: the car is held, the stream runs, the loads are integrated.""" if not (payload is dict): return error_response(422, "TunnelInvalid", "wind tunnel request must be an object") if not number_field_valid(payload, "wind_mps", 0.0, MAX_WIND_MPS) or not number_field_valid(payload, "yaw_deg", -180.0, 180.0): return error_response(422, "TunnelInvalid", "wind_mps must be 0-60 m/s and yaw_deg must be -180..180") wind_mps = f64(payload["wind_mps"]) yaw_deg = f64(payload["yaw_deg"]) yaw_rad = yaw_deg * 3.141592653589793 / 180.0 forces = aero_solve(session["context"].basis, wind_mps, yaw_rad) return response(200, { "schema": "sema.circuitframe-wind-tunnel/v1", "method": AERO_METHOD, "wind_mps": wind_mps, "yaw_deg": yaw_deg, "yaw_rad": yaw_rad, "measurement": measurement_body(forces), "provenance": AERO_PROVENANCE, "surface_cp": forces.surface_cp, "scientific_validated": false, })
def step_response(payload: any, session: dict[str, any]) !{}: if not (payload is dict): return error_response(422, "StepInvalid", "step request must be an object") required = ["generation", "sequence", "state_version", "dt_s", "throttle", "brake", "steering", "wind_mps", "wind_yaw_deg"] if not all(payload.has(field) for field in required): return error_response(422, "StepInvalid", "step request is missing a required field") if not (payload["generation"] is int) or not (payload["sequence"] is int) or not (payload["state_version"] is int): return error_response(422, "StepInvalid", "generation, sequence and state_version must be integers") expected_generation = session["generation"] expected_sequence = session["sequence"] expected_state_version = session["state_version"] expected_state = session["state"] expected_detector = session["detector"] expected_model_id = session["model_id"] if payload["generation"] != expected_generation: return error_response(409, "GenerationConflict", "generation does not match Sema-owned state") if payload["sequence"] != expected_sequence + 1 or payload["state_version"] != expected_state_version: return error_response(409, "StateConflict", "sequence or state version does not match Sema-owned state") if not number_field_valid(payload, "dt_s", 0.05, 0.05) or not number_field_valid(payload, "throttle", 0.0, 1.0) or not number_field_valid(payload, "brake", 0.0, 1.0) or not number_field_valid(payload, "steering", -1.0, 1.0) or not number_field_valid(payload, "wind_mps", 0.0, MAX_WIND_MPS) or not number_field_valid(payload, "wind_yaw_deg", -180.0, 180.0): return error_response(422, "StepInvalid", "numeric controls must be finite, in range, and use the fixed 0.05 s integration interval") dt_s = f64(payload["dt_s"]) mut controls = ControlInput( throttle=f64(payload["throttle"]), brake=f64(payload["brake"]), steering=f64(payload["steering"]), wind_mps=f64(payload["wind_mps"]), wind_yaw_deg=f64(payload["wind_yaw_deg"]), ) sweep = session["sweep"] if sweep["active"]: command = sweep_command(sweep["index"], expected_state.speed_mps) controls = ControlInput(throttle=controls.throttle, brake=controls.brake, steering=controls.steering, wind_mps=command[0], wind_yaw_deg=command[1]) sequence = payload["sequence"] context = session["context"] frame = step_vehicle(expected_state, controls, dt_s, sequence, expected_model_id, context) detector = update_aero_detector(expected_detector, frame.aero_residual_fraction, frame.state.time_s) adaptation = retained_adaptation(detector, expected_model_id) next_model_id = expected_model_id if detector.status == RegimeStatus.shifted and expected_model_id == "aero-rom-calm-v1": adaptation = evaluate_yaw_candidate(detector, sequence, frame.state.time_s, frame.state.speed_mps, frame.sideslip_rad * 180.0 / 3.141592653589793, aero_holdout_metrics(context)) if adaptation.activated: next_model_id = adaptation.selected_model_id if session["generation"] != expected_generation or session["sequence"] != expected_sequence or session["state_version"] != expected_state_version: return error_response(409, "LifecycleConflict", "Sema-owned state changed before the step could commit") session["model_id"] = next_model_id session["state"] = frame.state session["frame"] = frame session["detector"] = detector session["adaptation"] = adaptation session["sequence"] = sequence session["state_version"] = expected_state_version + 1 session["residual_history"] = pushed_residual_history(session["residual_history"], frame.aero_residual_fraction, detector.score, sequence, adaptation.activated) session["sweep"] = advanced_sweep(sweep, controls) return response(200, { "schema": "sema.circuitframe-dynamics-frame/v1", "generation": session["generation"], "sequence": session["sequence"], "state_version": session["state_version"], "frame": frame, "adaptation": adaptation, "model_id_next": session["model_id"], "residual_sample": {"sequence": sequence, "residual": frame.aero_residual_fraction, "score": detector.score, "threshold": detector.score_threshold, "status": regime_label(detector), "consecutive_high": detector.consecutive_high, "samples_seen": session["residual_history"]["count"], "activated_at_sequence": session["residual_history"]["activated_at_sequence"]}, "sweep": {"active": session["sweep"]["active"], "step_index": session["sweep"]["index"], "remaining_steps": max(0, SWEEP_STEPS - session["sweep"]["index"]), "commanded_wind_mps": session["sweep"]["commanded_wind_mps"], "commanded_yaw_deg": session["sweep"]["commanded_yaw_deg"]}, "provenance": AERO_PROVENANCE, "fidelity": AERO_METHOD, "scientific_validated": false, })
def reset_session(session: dict[str, any]): reset = initial_session(session["context"]) session["generation"] = session["generation"] + 1 session["sequence"] = reset["sequence"] session["state_version"] = reset["state_version"] session["state"] = reset["state"] session["frame"] = reset["frame"] session["detector"] = reset["detector"] session["model_id"] = reset["model_id"] session["adaptation"] = reset["adaptation"] session["candidate"] = None session["assembly_step"] = reset["assembly_step"] session["residual_history"] = reset["residual_history"] session["sweep"] = reset["sweep"]
def reset_response(session: dict[str, any]) !{}: reset_session(session) session["panel_width_mm"] = 90.0 session["vehicle_revision"] = 1 session["configuration_variant"] = "circuitframe" return response(200, status_body(session))
def candidate_response(session: dict[str, any]) !{}: if not physical_actions_allowed(): return error_response(409, "SourceVehicleRequired", "physical conductor candidate evaluation requires the reconstructed geometry, loads, routing, materials, and process data") preview = optimize_panel(session["vehicle_revision"]) session["candidate"] = preview["selected"] return response(200, preview)
def commit_response(payload: any, session: dict[str, any]) !{}: if not physical_actions_allowed(): return error_response(409, "SourceVehicleRequired", "candidate commit is blocked until the approved reconstruction source stack and physical routing are loaded") if not (payload is dict) or not payload.has("candidate_id") or not payload.has("base_revision"): return error_response(422, "CandidateCommitInvalid", "candidate_id and base_revision are required") if session["candidate"] is None: return error_response(409, "CandidateMissing", "create a candidate preview before commit") candidate = session["candidate"] if payload["candidate_id"] != candidate.id or payload["base_revision"] != session["vehicle_revision"] or candidate.base_revision != session["vehicle_revision"]: return error_response(409, "CandidateConflict", "candidate identity or base revision is stale") if not candidate.passed: return error_response(422, "CandidateRejected", candidate.reason) session["panel_width_mm"] = candidate.width_mm session["vehicle_revision"] = session["vehicle_revision"] + 1 session["candidate"] = None return response(200, { "schema": "sema.circuitframe-candidate-commit/v1", "candidate_id": candidate.id, "configuration_revision": session["vehicle_revision"], "panel_width_mm": session["panel_width_mm"], "committed": true, "reason": candidate.reason, })
def assembly_step_response(payload: any, session: dict[str, any]) !{}: if not (payload is dict) or not payload.has("step") or not payload.has("generation") or not (payload["step"] is int) or not (payload["generation"] is int): return error_response(422, "AssemblyStepInvalid", "step and generation must be integers") expected_generation = session["generation"] if payload["generation"] != expected_generation: return error_response(409, "GenerationConflict", "assembly generation does not match Sema-owned state") manifest = vehicle_manifest(session["configuration_variant"]) if payload["step"] < 0 or payload["step"] >= len(manifest["operations"]): return error_response(422, "AssemblyStepInvalid", "step is outside the operation graph") if session["generation"] != expected_generation: return error_response(409, "LifecycleConflict", "Sema-owned state changed before the assembly step could commit") session["assembly_step"] = payload["step"] return response(200, {"schema": "sema.circuitframe-assembly-state/v1", "generation": session["generation"], "step": session["assembly_step"], "operation": manifest["operations"][session["assembly_step"]]})
def sweep_state_body(session: dict[str, any]): return { "schema": "sema.circuitframe-regime-sweep/v1", "sweep": sweep_body(session), "detector": detector_body(session), "residual_history": residual_history_body(session), "adaptation": session["adaptation"], "model_id": session["model_id"], }
def regime_sweep_response(payload: any, session: dict[str, any]) !{}: """Arm or disarm the bounded wind-yaw demonstration; it never advances the causal sequence itself.
Anything short of an explicit `{"action": "start"}` disarms, so an empty body, `{}`, an explicit stop or a cancel flag all reliably stop the demonstration. """ if not (payload is dict): return error_response(422, "RegimeSweepInvalid", "regime sweep request must be an object") if payload.has("action") and not (payload["action"] is str): return error_response(422, "RegimeSweepInvalid", "action must be start or stop") action = str(payload["action"]) if payload.has("action") else "stop" if action != "start" and action != "stop": return error_response(422, "RegimeSweepInvalid", "action must be start or stop") cancel = payload.has("cancel") and payload["cancel"] is bool and payload["cancel"] if action == "stop" or cancel: session["sweep"] = idle_sweep() return response(200, sweep_state_body(session)) restart = payload.has("restart_from_calm") and payload["restart_from_calm"] is bool and payload["restart_from_calm"] if restart: detector = initial_aero_detector() session["detector"] = detector session["model_id"] = CALM_MODEL_ID session["adaptation"] = retained_adaptation(detector, CALM_MODEL_ID) session["residual_history"] = initial_residual_history() session["sweep"] = {"active": true, "index": 0, "commanded_wind_mps": SWEEP_MIN_WIND_MPS, "commanded_yaw_deg": 0.0, "restarted_from_calm": restart} return response(200, sweep_state_body(session))
def configuration_response(payload: any, session: dict[str, any]) !{}: if not (payload is dict) or not payload.has("configuration") or not (payload["configuration"] is str): return error_response(422, "ConfigurationInvalid", "configuration must be conventional or circuitframe") configuration = str(payload["configuration"]) if configuration != "conventional" and configuration != "circuitframe": return error_response(422, "ConfigurationInvalid", "configuration must be conventional or circuitframe") reset_session(session) session["configuration_variant"] = configuration session["panel_width_mm"] = 90.0 return response(200, { "schema": "sema.circuitframe-configuration-view/v1", "configuration": configuration, "shared_identity": "circuitframe_gt_01", "authority": "configuration-specific semantic routes and process delta over shared reconstruction identities; physical deltas remain engineered assumptions", "state_reset": true, "generation": session["generation"], })
def export_response(session: dict[str, any]) !{}: if not evidence_export_allowed(): return error_response(409, "SourceVehicleRequired", "evidence export is blocked until attributed presentation geometry is loaded") return response(200, { "schema": "sema.circuitframe-engineering-export/v1", "configuration_id": "circuitframe_gt_01", "configuration_revision": session["vehicle_revision"], "authority": "Sema semantic/evidence manifest with attributed exterior and assumption-derived parametric assembly; no OEM manufacturing authority", "vehicle": vehicle_manifest(session["configuration_variant"]), "vehicle_nets": vehicle_net_comparison(), "assembly": cached_assembly(session), "pcb": cached_pcb(session), "aerodynamics": cached_aero_basis(session), "equations": all_equations(), "runtime": {"model_id": session["model_id"], "state": session["state"], "frame": session["frame"], "panel_width_mm": session["panel_width_mm"]}, "configuration_variant": session["configuration_variant"], "representations": [ {"format": "glTF 2.0", "status": "generated web scene", "authority": "attributed exterior plus byte-verified engineering reconstruction"}, {"format": "STEP AP242", "status": "generated parametric reconstruction", "authority": "FreeCAD/OCCT engineering assumptions; not OEM manufacturing CAD or certified PMI"}, {"format": "FreeCAD FCStd", "status": "generated parametric master", "authority": "open-profile engineering reconstruction"}, {"format": "KiCad flat netlist", "status": "generated per board", "authority": "Sema reconstructed boards; not a manufacturable fabrication package"}, {"format": "FMI/SSP", "status": "contract only", "authority": "Sema reduced-model implementation"}, {"format": "BOM/MBOM/BOP", "status": "major-assembly manifest", "authority": "Sema research profile"}, ], "losses": ["no OEM revision-certified manufacturing B-rep or JT", "no homologation evidence", "no governing CFD/FEA/crash", "no wind-tunnel or dynamometer correlation", "no EMC or transient circuit analysis", "supplier internals opaque"], })
def authoring_response(payload: any) !{}: if not (payload is dict) or not payload.has("description") or not (payload["description"] is str): return error_response(422, "AuthoringInvalid", "authoring request requires a description string") description = str(payload["description"]) if len(description) == 0 or len(description) > 2048: return error_response(413, "AuthoringInvalid", "description must contain between 1 and 2048 characters") result = compile_circuit_description(description) return response(200, {"schema": "sema.circuitframe-authoring/v1", "result": result})
def live_response(request: dict[str, any], session: dict[str, any]) !{}: if request["method"] == "GET" and request["path"] == "/api/sema/health": return response(200, {"status": "ok", "backend": "Sema", "schema": "sema.circuitframe-health/v1"}) if request["method"] == "GET" and request["path"] == "/api/sema/status": return response(200, status_body(session)) if request["method"] == "GET" and request["path"] == "/api/sema/aero/basis": return response(200, cached_aero_basis(session)) if request["method"] == "GET" and request["path"] == "/api/sema/pcb": return response(200, cached_pcb(session)) if request["method"] == "GET" and request["path"] == "/api/sema/assembly": return response(200, cached_assembly(session)) if request["method"] == "GET" and request["path"] == "/api/sema/export": return export_response(session) if request["method"] == "POST" and request["path"] == "/api/sema/reset": return reset_response(session) if request["method"] == "POST" and request["path"] == "/api/sema/tunnel": if len(request["body"]) == 0 or len(request["body"]) > 1024: return error_response(413, "TunnelInvalid", "wind tunnel request must contain at most 1024 bytes") expect payload = decode_json(request["body"]): return tunnel_response(payload, session) except JsonError as error: return error_response(422, "TunnelInvalid", "wind tunnel request must contain valid JSON") if request["method"] == "POST" and request["path"] == "/api/sema/configuration": if len(request["body"]) == 0 or len(request["body"]) > 1024: return error_response(413, "ConfigurationInvalid", "configuration request must contain at most 1024 bytes") expect payload = decode_json(request["body"]): return configuration_response(payload, session) except JsonError as error: return error_response(422, "ConfigurationInvalid", "configuration request must contain valid JSON") if request["method"] == "POST" and request["path"] == "/api/sema/authoring": if len(request["body"]) == 0 or len(request["body"]) > 4096: return error_response(413, "AuthoringInvalid", "authoring request must contain at most 4096 bytes") expect payload = decode_json(request["body"]): return authoring_response(payload) except JsonError as error: return error_response(422, "AuthoringInvalid", "authoring request must contain valid JSON") if request["method"] == "POST" and request["path"] == "/api/sema/candidate": return candidate_response(session) if request["method"] == "POST" and request["path"] == "/api/sema/candidate/commit": if len(request["body"]) == 0 or len(request["body"]) > 2048: return error_response(413, "CandidateCommitInvalid", "candidate commit must contain at most 2048 bytes") expect payload = decode_json(request["body"]): return commit_response(payload, session) except JsonError as error: return error_response(422, "CandidateCommitInvalid", "candidate commit must contain valid JSON") if request["method"] == "POST" and request["path"] == "/api/sema/assembly": if len(request["body"]) == 0 or len(request["body"]) > 1024: return error_response(413, "AssemblyStepInvalid", "assembly request must contain at most 1024 bytes") expect payload = decode_json(request["body"]): return assembly_step_response(payload, session) except JsonError as error: return error_response(422, "AssemblyStepInvalid", "assembly request must contain valid JSON") if request["method"] == "POST" and request["path"] == "/api/sema/regime-sweep": if len(request["body"]) > 1024: return error_response(413, "RegimeSweepInvalid", "regime sweep request must contain at most 1024 bytes") if len(request["body"]) == 0: return regime_sweep_response({}, session) expect payload = decode_json(request["body"]): return regime_sweep_response(payload, session) except JsonError as error: return error_response(422, "RegimeSweepInvalid", "regime sweep request must contain valid JSON") if request["method"] == "POST" and request["path"] == "/api/sema/step": if len(request["body"]) == 0 or len(request["body"]) > 4096: return error_response(413, "StepInvalid", "step request must contain at most 4096 bytes") expect payload = decode_json(request["body"]): return step_response(payload, session) except JsonError as error: return error_response(422, "StepInvalid", "step request must contain valid finite JSON") return error_response(404, "NotFound", "unknown CircuitFrame Sema endpoint: " + str(request["method"]) + " " + str(request["path"]))
pub def serve_live(port: int) -> None !{net.listen}: sem "Serve an automotive-native CircuitFrame vehicle, aerodynamic, circuit, assembly, physics, adaptive-model and evidence API" require port >= 1024 and port <= 65535 context = physics_context() session = initial_session(context) cached_aero_basis(session) cached_pcb(session) cached_assembly(session) print("circuitframe_sema=http://127.0.0.1:" + str(port) + " configuration=circuitframe_gt_01 model=" + session["model_id"] + " panels=" + str(context.basis.panel_count) + " mass_kg=" + str(context.parameters.mass_kg)) http.serve(port, request => live_response(request, session))
test "a parked car in the tunnel measures yaw-dependent loads": session = initial_session(physics_context()) head_on = tunnel_response({"wind_mps": 27.8, "yaw_deg": 0.0}, session) yawed = tunnel_response({"wind_mps": 27.8, "yaw_deg": 25.0}, session) still = tunnel_response({"wind_mps": 0.0, "yaw_deg": 0.0}, session) rejected = tunnel_response({"wind_mps": 90.0, "yaw_deg": 0.0}, session) head_on_body = decode_json(head_on["body"]) yawed_body = decode_json(yawed["body"]) still_body = decode_json(still["body"]) ensure head_on["status"] == 200 and yawed["status"] == 200 and still["status"] == 200 ensure rejected["status"] == 422 ensure head_on_body["measurement"]["drag_n"] > 200.0 ensure yawed_body["measurement"]["cd"] > head_on_body["measurement"]["cd"] ensure yawed_body["measurement"]["side_n"] > 0.0 ensure still_body["measurement"]["drag_n"] == 0.0 ensure len(head_on_body["surface_cp"]) == session["context"].basis.panel_count ensure head_on_body["provenance"]["cd_friction"] == "correlated"
test "the aerodynamic basis, boards and assembly are served from one solve": session = initial_session(physics_context()) basis = cached_aero_basis(session) boards = cached_pcb(session) assembly = cached_assembly(session) ensure basis["schema"] == "sema.circuitframe-aero-basis/v1" ensure boards["schema"] == "sema.circuitframe-pcb/v1" ensure assembly["schema"] == "sema.circuitframe-assembly/v1" ensure len(basis["panels"]) == session["context"].basis.panel_count ensure session["aero_basis_payload"] is not None ensure session["pcb_payload"] is not None ensure session["assembly_payload"] is not None
test "physical commit fails closed while evidence export stays truthful": session = initial_session(physics_context()) step = step_response({"generation": session["generation"], "sequence": 1, "state_version": session["state_version"], "dt_s": 0.05, "throttle": 0.4, "brake": 0.0, "steering": 0.0, "wind_mps": 4.0, "wind_yaw_deg": 30.0}, session) candidate = candidate_response(session) commit = commit_response({"candidate_id": "none", "base_revision": 1}, session) export = export_response(session) payload = decode_json(export["body"]) ensure candidate["status"] == 409 ensure commit["status"] == 409 ensure step["status"] == 200 ensure export["status"] == 200 ensure payload["representations"][1]["status"] == "generated parametric reconstruction" ensure payload["runtime"]["frame"]["sequence"] == 1
test "the harness comparison quantifies the CircuitFrame conductor delta": rows = vehicle_net_comparison() mut lighter = 0 mut fewer_connectors = 0 for row in rows: if row["delta"]["mass_kg"] < 0.0: lighter = lighter + 1 if row["delta"]["connectors"] < 0: fewer_connectors = fewer_connectors + 1 ensure row["variants"]["conventional"]["resistance_mohm"] > 0.0 ensure row["variants"]["circuitframe"]["length_m"] < row["variants"]["conventional"]["length_m"] ensure len(rows) == 9 ensure lighter == 9 ensure fewer_connectors == 9
test "assembly selection rejects a stale lifecycle generation": session = initial_session(physics_context()) stale_generation = session["generation"] selected = configuration_response({"configuration": "circuitframe"}, session) rejected = assembly_step_response({"generation": stale_generation, "step": 3}, session) ensure selected["status"] == 200 ensure rejected["status"] == 409 ensure session["assembly_step"] == 11
test "configuration comparison accepts only declared variants": session = initial_session(physics_context()) selected = configuration_response({"configuration": "circuitframe"}, session) rejected = configuration_response({"configuration": "prototype"}, session) ensure selected["status"] == 200 ensure session["configuration_variant"] == "circuitframe" ensure rejected["status"] == 422 conventional = vehicle_manifest("conventional") circuitframe = vehicle_manifest("circuitframe") ensure conventional["configuration_delta"]["changed_net_ids"] == circuitframe["configuration_delta"]["changed_net_ids"] ensure conventional["nets"][0].route != circuitframe["nets"][0].route ensure conventional["operations"][1].name != circuitframe["operations"][1].name ensure session["generation"] == 1
test "invalid numeric controls fail as typed client errors": session = initial_session(physics_context()) invalid_range = step_response({"generation": 0, "sequence": 1, "state_version": 0, "dt_s": 0.05, "throttle": 2.0, "brake": 0.0, "steering": 0.0, "wind_mps": 0.0, "wind_yaw_deg": 0.0}, session) invalid_type = step_response({"generation": 0, "sequence": 1, "state_version": 0, "dt_s": "0.05", "throttle": 0.4, "brake": 0.0, "steering": 0.0, "wind_mps": 0.0, "wind_yaw_deg": 0.0}, session) invalid_rate = step_response({"generation": 0, "sequence": 1, "state_version": 0, "dt_s": 0.1, "throttle": 0.4, "brake": 0.0, "steering": 0.0, "wind_mps": 0.0, "wind_yaw_deg": 0.0}, session) invalid_wind = step_response({"generation": 0, "sequence": 1, "state_version": 0, "dt_s": 0.05, "throttle": 0.4, "brake": 0.0, "steering": 0.0, "wind_mps": 75.0, "wind_yaw_deg": 0.0}, session) ensure invalid_range["status"] == 422 ensure invalid_type["status"] == 422 ensure invalid_rate["status"] == 422 ensure invalid_wind["status"] == 422
test "generation transition rejects stale step without mutation": session = initial_session(physics_context()) stale_step = {"generation": 0, "sequence": 1, "state_version": 0, "dt_s": 0.05, "throttle": 0.4, "brake": 0.0, "steering": 0.0, "wind_mps": 0.0, "wind_yaw_deg": 0.0} selected = configuration_response({"configuration": "circuitframe"}, session) rejected = step_response(stale_step, session) ensure selected["status"] == 200 ensure rejected["status"] == 409 ensure session["generation"] == 1 ensure session["sequence"] == 0
test "the residual ring is bounded and the newest sample rides on every step": session = initial_session(physics_context()) mut sequence = 0 mut last = {} while sequence < RESIDUAL_HISTORY_CAPACITY + 10: sequence = sequence + 1 last = decode_json(step_response({"generation": 0, "sequence": sequence, "state_version": sequence - 1, "dt_s": 0.05, "throttle": 0.3, "brake": 0.0, "steering": 0.0, "wind_mps": 6.0, "wind_yaw_deg": 12.0}, session)["body"]) status = status_body(session) history = status["residual_history"] ensure len(history["residual"]) == RESIDUAL_HISTORY_CAPACITY ensure len(history["score"]) == RESIDUAL_HISTORY_CAPACITY ensure history["count"] == sequence ensure history["last_sequence"] == sequence ensure history["threshold"] == status["detector"]["threshold"] ensure last["residual_sample"]["residual"] == history["residual"][RESIDUAL_HISTORY_CAPACITY - 1] ensure last["residual_sample"]["score"] == history["score"][RESIDUAL_HISTORY_CAPACITY - 1] ensure last["residual_sample"]["samples_seen"] == sequence ensure not last.has("residual_history")
test "the scripted sweep drives the declared yaw ramp, passes the gate and always disarms": session = initial_session(physics_context()) rejected = regime_sweep_response({"action": "rewind"}, session) armed = regime_sweep_response({"action": "start"}, session) ensure rejected["status"] == 422 ensure armed["status"] == 200 ensure session["sweep"]["active"] and not session["sweep"]["restarted_from_calm"] mut sequence = 0 while sequence < 60 and session["model_id"] == CALM_MODEL_ID: sequence = sequence + 1 step_response({"generation": 0, "sequence": sequence, "state_version": sequence - 1, "dt_s": 0.05, "throttle": 0.35, "brake": 0.0, "steering": 0.0, "wind_mps": 0.0, "wind_yaw_deg": 0.0}, session) frame = session["frame"] adaptation = session["adaptation"] ensure frame.controls.wind_mps >= SWEEP_MIN_WIND_MPS and frame.controls.wind_mps <= SWEEP_MAX_WIND_MPS ensure frame.controls.wind_yaw_deg > 0.0 ensure abs(frame.sideslip_rad * 180.0 / math.pi) <= 35.0 ensure session["model_id"] == "aero-rom-crosswind-v2" ensure adaptation.activated and adaptation.validation_error < 0.04 and adaptation.invariant_violations == 0 ensure session["residual_history"]["activated_at_sequence"] == sequence ensure session["sweep"]["index"] == sequence and session["sweep"]["active"] disarmed = regime_sweep_response({}, session) ensure disarmed["status"] == 200 ensure not session["sweep"]["active"] and session["sweep"]["index"] == 0
test "resetting the causal state clears the residual ring and disarms the sweep": session = initial_session(physics_context()) regime_sweep_response({"action": "start"}, session) step_response({"generation": 0, "sequence": 1, "state_version": 0, "dt_s": 0.05, "throttle": 0.3, "brake": 0.0, "steering": 0.0, "wind_mps": 0.0, "wind_yaw_deg": 0.0}, session) ensure session["residual_history"]["count"] == 1 and session["sweep"]["active"] reset_session(session) ensure session["residual_history"]["count"] == 0 ensure len(session["residual_history"]["residual"]) == 0 ensure not session["sweep"]["active"]
test "every declared equation carries a subsystem group and an evaluation phase": rows = all_equations() mut per_step = 0 mut sampled = 0 mut on_demand = 0 for row in rows: ensure len(row["group"]) > 0 and len(row["owner"]) > 0 and len(row["expression"]) > 0 if row["phase"] == "per_step": per_step = per_step + 1 elif row["phase"] == "sampled": sampled = sampled + 1 else: on_demand = on_demand + 1 ensure per_step + sampled + on_demand == len(rows) ensure sampled == len(SAMPLED_EQUATION_IDS) ensure per_step > sampled and on_demand > 0 ensure contains([row["group"] for row in rows], "thermal network") ensure contains([row["group"] for row in rows], "aero panel solve")
test "the declared validity region is published and enforced": session = initial_session(physics_context()) published = sweep_body(session) ensure published["envelope_max_speed_mps"] == 55.0 ensure published["envelope_max_sideslip_deg"] == 35.0 detector = initial_aero_detector() mut shifted = detector for index in range(0, 8): shifted = update_aero_detector(shifted, 0.4, f64(index) * 0.05) inside = evaluate_yaw_candidate(shifted, 24, 1.2, published["envelope_max_speed_mps"] - 1.0, 20.0, aero_holdout_metrics(session["context"])) too_fast = evaluate_yaw_candidate(shifted, 24, 1.2, published["envelope_max_speed_mps"] + 1.0, 20.0, aero_holdout_metrics(session["context"])) too_yawed = evaluate_yaw_candidate(shifted, 24, 1.2, 20.0, published["envelope_max_sideslip_deg"] + 1.0, aero_holdout_metrics(session["context"])) ensure inside.activated ensure not too_fast.activated and too_fast.validation_status == "blocked_out_of_scope" ensure not too_yawed.activated and too_yawed.validation_status == "blocked_out_of_scope"src/pcb.sema
Section titled “src/pcb.sema”"""Printed-circuit design and DC electrical analysis for the CircuitFrame boards.
Four boards are declared at component, pin, net, trace and via level and then analysed:
* DC modified nodal analysis. Copper branch conductances come from geometry (R = rho * L / (w * t), rho corrected by the copper temperature coefficient), every net's feed pin is held at its declared operating potential, declared loads inject current at their sink pin and return it at their return pin, and the reference net pin is the 0 V node. `G v = i` is solved with `solve(matrix, rhs)`. * IPC-2221 external/internal conductor sizing plus the inverted temperature-rise relation, evaluated per trace against the layer's copper weight. * IEC 60664-1 creepage screening on the 800 V board. * A geometric design-rule check (track width, annular ring, pads inside outline).
Every board is an *engineered reconstruction* published for the CircuitFrame researchdemo. None of it is an OEM design, an OEM netlist, or a released manufacturing data set.Part numbers name the device *class* the reconstruction is dimensioned against."""
from std.json import encode as encode_jsonimport math
assure silver
enum PcbLayerFunction: signal | plane | core | prepreg | soldermask | coverlay
enum PcbNetClass: hv | power | ground | signal | can | ethernet | analog
enum PcbPinKind: power | ground | signal | analog | thermal | nc
struct PcbLayer: id: str name: str function: PcbLayerFunction copper_um: f64 dielectric_mm: f64 dielectric_er: f64 material: str invariant len(id) > 0 invariant len(name) > 0 invariant copper_um >= 0.0 and copper_um <= 5000.0 invariant dielectric_mm >= 0.0 and dielectric_mm <= 20.0 invariant dielectric_er >= 1.0 and dielectric_er <= 20.0 invariant len(material) > 0
struct PcbStackup: id: str name: str layers: list[PcbLayer] total_thickness_mm: f64 ipc_class: int impedance_target_ohm: f64 invariant len(id) > 0 invariant len(name) > 0 invariant len(layers) >= 2 and len(layers) <= 64 invariant total_thickness_mm > 0.0 invariant ipc_class >= 1 and ipc_class <= 3 invariant impedance_target_ohm > 0.0
struct PcbComponent: refdes: str part_number: str description: str footprint: str pin_count: int x_mm: f64 y_mm: f64 rotation_deg: f64 power_dissipation_w: f64 mass_g: f64 invariant len(refdes) > 0 invariant len(part_number) > 0 invariant len(description) > 0 invariant len(footprint) > 0 invariant pin_count >= 2 and pin_count <= 2048 invariant x_mm >= 0.0 invariant y_mm >= 0.0 invariant rotation_deg >= 0.0 and rotation_deg < 360.0 invariant power_dissipation_w >= 0.0 invariant mass_g > 0.0
struct PcbPin: id: str refdes: str number: int name: str net_id: str x_mm: f64 y_mm: f64 kind: PcbPinKind invariant len(id) > 0 invariant len(refdes) > 0 invariant number >= 1 invariant len(name) > 0 invariant len(net_id) > 0 invariant x_mm >= 0.0 invariant y_mm >= 0.0
struct PcbNet: id: str name: str net_class: PcbNetClass pin_ids: list[str] nominal_voltage_v: f64 current_a: f64 is_reference: bool invariant len(id) > 0 invariant len(name) > 0 invariant len(pin_ids) >= 2 and len(pin_ids) <= 512 invariant nominal_voltage_v >= 0.0 invariant current_a >= 0.0
struct PcbTrace: id: str net_id: str layer_id: str width_mm: f64 length_mm: f64 current_a: f64 from_pin_id: str to_pin_id: str resistance_mohm: f64 voltage_drop_mv: f64 power_loss_mw: f64 temperature_rise_c: f64 ipc_min_width_mm: f64 compliant: bool invariant len(id) > 0 invariant len(net_id) > 0 invariant len(layer_id) > 0 invariant width_mm > 0.0 invariant length_mm > 0.0 invariant current_a >= 0.0 invariant len(from_pin_id) > 0 invariant len(to_pin_id) > 0 invariant resistance_mohm >= 0.0 invariant voltage_drop_mv >= 0.0 invariant power_loss_mw >= 0.0 invariant temperature_rise_c >= 0.0 invariant ipc_min_width_mm >= 0.0
struct PcbVia: id: str net_id: str drill_mm: f64 pad_mm: f64 from_layer_id: str to_layer_id: str current_a: f64 resistance_mohm: f64 invariant len(id) > 0 invariant len(net_id) > 0 invariant drill_mm > 0.0 invariant pad_mm > drill_mm invariant len(from_layer_id) > 0 invariant len(to_layer_id) > 0 invariant current_a >= 0.0 invariant resistance_mohm >= 0.0
struct PcbBoard: id: str name: str host_part_id: str stackup: PcbStackup components: list[PcbComponent] pins: list[PcbPin] nets: list[PcbNet] traces: list[PcbTrace] vias: list[PcbVia] outline_width_mm: f64 outline_height_mm: f64 zone: str substrate: str invariant len(id) > 0 invariant len(name) > 0 invariant len(host_part_id) > 0 invariant len(components) > 0 and len(components) <= 4096 invariant len(pins) > 0 and len(pins) <= 16384 invariant len(nets) > 0 and len(nets) <= 4096 invariant len(traces) > 0 and len(traces) <= 16384 invariant len(vias) <= 16384 invariant outline_width_mm > 0.0 invariant outline_height_mm > 0.0 invariant len(zone) > 0 invariant len(substrate) > 0
struct PcbAnalysis: board_id: str node_count: int branch_count: int source_node: str worst_net_id: str worst_voltage_drop_mv: f64 worst_drop_fraction: f64 total_copper_loss_w: f64 max_temperature_rise_c: f64 ipc_violations: int drc_violations: int isolation_violations: int solver_residual: f64 kcl_residual_a: f64 invariant len(board_id) > 0 invariant node_count > 0 invariant branch_count > 0 invariant len(source_node) > 0 invariant len(worst_net_id) > 0 invariant worst_voltage_drop_mv >= 0.0 invariant worst_drop_fraction >= 0.0 invariant total_copper_loss_w >= 0.0 invariant max_temperature_rise_c >= 0.0 invariant ipc_violations >= 0 invariant drc_violations >= 0 invariant isolation_violations >= 0 invariant solver_residual >= 0.0 invariant kcl_residual_a >= 0.0
struct PcbRoutePlan: net_id: str layer_id: str width_mm: f64 topology: str via_count: int via_drill_mm: f64 via_pad_mm: f64 via_layer_id: str invariant len(net_id) > 0 invariant len(layer_id) > 0 invariant width_mm > 0.0 invariant topology == "chain" or topology == "star" invariant via_count >= 0 and via_count <= 32 invariant via_drill_mm > 0.0 invariant via_pad_mm > via_drill_mm
struct PcbSegment: id: str net_id: str layer_id: str width_mm: f64 from_pin_id: str to_pin_id: str via_count: int via_drill_mm: f64 via_pad_mm: f64 via_layer_id: str invariant len(id) > 0 invariant len(net_id) > 0 invariant len(layer_id) > 0 invariant width_mm > 0.0 invariant via_count >= 0 and via_count <= 32 invariant via_drill_mm > 0.0 invariant via_pad_mm > via_drill_mm
struct PcbLoad: id: str board_id: str pin_id: str return_pin_id: str current_a: f64 description: str invariant len(id) > 0 invariant len(board_id) > 0 invariant len(pin_id) > 0 invariant len(return_pin_id) > 0 invariant current_a >= 0.0 invariant len(description) > 0
struct PcbSolution: node_ids: list[str] node_voltages: list[f64] branch_currents: list[f64] fixed_count: int solver_residual: f64 kcl_residual_a: f64 source_current_a: f64 invariant len(node_ids) == len(node_voltages) invariant fixed_count >= 0 invariant solver_residual >= 0.0 invariant kcl_residual_a >= 0.0 invariant source_current_a >= 0.0
struct PcbIsolationCheck: id: str board_id: str category: str from_pin_id: str to_pin_id: str working_voltage_v: f64 required_mm: f64 measured_mm: f64 reinforced: bool compliant: bool invariant len(id) > 0 invariant len(board_id) > 0 invariant len(category) > 0 invariant working_voltage_v >= 0.0 invariant required_mm >= 0.0 invariant measured_mm >= 0.0
# Copper at 20 degC, IACS annealed, with the standard temperature coefficient.COPPER_RESISTIVITY_OHM_M = 0.00000001724COPPER_TEMPCO_PER_K = 0.00393# Declared conductor operating temperature used for the resistance correction.CONDUCTOR_OPERATING_TEMP_C = 45.0# IPC-2221 conductor sizing: A_mils2 = (I / (k * dT^0.44))^(1/0.725).IPC_ALLOWED_RISE_C = 20.0IPC_K_EXTERNAL = 0.048IPC_K_INTERNAL = 0.024VIA_PLATING_UM = 25.0MIN_TRACK_WIDTH_MM = 0.15MIN_ANNULAR_RING_MM = 0.125MIN_SEGMENT_LENGTH_MM = 0.05# IEC 60664-1 table 4 basic creepage, pollution degree 2, material group IIIa.IEC60664_VOLTAGE_STEPS = [63.0, 100.0, 125.0, 160.0, 200.0, 250.0, 320.0, 400.0, 500.0, 630.0, 800.0, 1000.0]IEC60664_CREEPAGE_MM = [0.63, 1.0, 1.25, 1.6, 2.0, 2.5, 3.2, 4.0, 5.0, 6.3, 8.0, 10.0]# Below 60 V DC no hazardous-voltage creepage screening is applied.HAZARDOUS_VOLTAGE_V = 60.0HV_WORKING_VOLTAGE_V = 800.0# Clearance for a 6 kV rated impulse, pollution degree 2, inhomogeneous field.HV_CLEARANCE_MM = 5.5BUCK_EFFICIENCY = 0.94HV_BUS_CURRENT_A = 160.0SILL_HV_CURRENT_A = 120.0FZC_OUT1_A = 3.1FZC_OUT2_A = 2.2FZC_OUT3_A = 1.4FZC_MCU_A = 0.32FZC_PHY_A = 0.11FZC_CAN_SUPPLY_A = 0.06HVJ_COIL_A = 0.42HVJ_PYRO_MONITOR_A = 0.005HVJ_ISO_SUPPLY_A = 0.012HVJ_ISENSE_A = 0.0045HVJ_IMD_A = 0.005HVJ_IMD_DIVIDER_A = 0.00035SILL_ZONE_A = 22.0SILL_SERVICE_A = 6.0SILL_BPILLAR_A = 9.0
equation conductor_resistance_ohm(resistivity, length_m, width_m, thickness_m) -> any: return resistivity * length_m / (width_m * thickness_m)
equation temperature_corrected_resistivity(rho_20, tempco, temperature_c) -> any: return rho_20 * (1.0 + tempco * (temperature_c - 20.0))
equation ipc2221_cross_section_mils2(current_a, k, rise_c) -> any: return (current_a / (k * rise_c^0.44))^(1.0 / 0.725)
equation ipc2221_temperature_rise_c(current_a, k, area_mils2) -> any: return (current_a / (k * area_mils2^0.725))^(1.0 / 0.44)
equation via_barrel_resistance_ohm(resistivity, height_m, drill_m, plating_m) -> any: return resistivity * height_m / (3.141592653589793 * (drill_m + plating_m) * plating_m)
equation joule_loss_w(current_a, resistance_ohm) -> any: return current_a^2 * resistance_ohm
def operating_resistivity() !{}: return temperature_corrected_resistivity(COPPER_RESISTIVITY_OHM_M, COPPER_TEMPCO_PER_K, CONDUCTOR_OPERATING_TEMP_C)
def net_class_key(net_class: PcbNetClass): if net_class == PcbNetClass.hv: return "hv" if net_class == PcbNetClass.power: return "power" if net_class == PcbNetClass.ground: return "ground" if net_class == PcbNetClass.can: return "can" if net_class == PcbNetClass.ethernet: return "ethernet" if net_class == PcbNetClass.analog: return "analog" return "signal"
def layer_function_key(function: PcbLayerFunction): if function == PcbLayerFunction.signal: return "signal" if function == PcbLayerFunction.plane: return "plane" if function == PcbLayerFunction.core: return "core" if function == PcbLayerFunction.prepreg: return "prepreg" if function == PcbLayerFunction.soldermask: return "soldermask" return "coverlay"
def pin_kind_key(kind: PcbPinKind): if kind == PcbPinKind.power: return "power" if kind == PcbPinKind.ground: return "ground" if kind == PcbPinKind.analog: return "analog" if kind == PcbPinKind.thermal: return "thermal" if kind == PcbPinKind.nc: return "nc" return "signal"
def pcb_pin(refdes: str, number: int, name: str, net_id: str, x_mm: f64, y_mm: f64, kind: PcbPinKind): return PcbPin(id=refdes + "." + str(number), refdes=refdes, number=number, name=name, net_id=net_id, x_mm=x_mm, y_mm=y_mm, kind=kind)
def pcb_load(board_id: str, pin_id: str, return_pin_id: str, current_a: f64, description: str): return PcbLoad(id=board_id + ":" + pin_id + ">" + return_pin_id, board_id=board_id, pin_id=pin_id, return_pin_id=return_pin_id, current_a=current_a, description=description)
def route_points(x1: f64, y1: f64, x2: f64, y2: f64): # One orthogonal leg then a 45-degree diagonal, the way real copper is routed. dx = x2 - x1 dy = y2 - y1 if abs(dx) >= abs(dy): run = abs(dx) - abs(dy) mid_x = x1 + run if dx >= 0.0 else x1 - run return [[x1, y1], [mid_x, y1], [x2, y2]] rise = abs(dy) - abs(dx) mid_y = y1 + rise if dy >= 0.0 else y1 - rise return [[x1, y1], [x1, mid_y], [x2, y2]]
def polyline_length_mm(points: list[list[f64]]): mut total = 0.0 for index in range(1, len(points)): dx = points[index][0] - points[index - 1][0] dy = points[index][1] - points[index - 1][1] total = total + math.sqrt(dx * dx + dy * dy) return total
def pin_index_map(pins: list[PcbPin]) -> dict[str, int]: mut mapping = {} for index in range(0, len(pins)): mapping[pins[index].id] = index return mapping
def layer_depth_mm(stackup: PcbStackup, layer_id: str): mut depth = 0.0 for layer in stackup.layers: if layer.id == layer_id: return depth depth = depth + layer.copper_um * 0.001 + layer.dielectric_mm return depth
def layer_copper_um(stackup: PcbStackup, layer_id: str): for layer in stackup.layers: if layer.id == layer_id: return layer.copper_um return 35.0
def layer_is_external(stackup: PcbStackup, layer_id: str): mut first = "" mut last = "" for layer in stackup.layers: if layer.copper_um > 0.0: if len(first) == 0: first = layer.id last = layer.id return layer_id == first or layer_id == last
def stackup_thickness_mm(layers: list[PcbLayer]): mut total = 0.0 for layer in layers: total = total + layer.copper_um * 0.001 + layer.dielectric_mm return total
def ipc_min_width_mm(current_a: f64, copper_um: f64, external: bool) !{}: # A_mils2 = (I / (k * dT^0.44))^(1/0.725); width_mils = A_mils2 / thickness_mils. # 1 mil = 0.0254 mm, so copper_um / 25.4 turns a copper weight into mils and # the final * 0.0254 turns the required width back into millimetres. # IPC-2221B figure 6-4 is fitted for I <= 35 A and A <= 700 mils^2. The traction # busbar and the printed sill conductors run well above that, so their numbers are # an extrapolation of the published curve, not a standard-validated result. if current_a <= 0.0: return 0.0 factor = IPC_K_EXTERNAL if external else IPC_K_INTERNAL area_mils2 = ipc2221_cross_section_mils2(current_a, factor, IPC_ALLOWED_RISE_C) return area_mils2 / (copper_um / 25.4) * 0.0254
def ipc_temperature_rise_c(current_a: f64, width_mm: f64, copper_um: f64, external: bool) !{}: # Same relation inverted: dT = (I / (k * A^0.725))^(1/0.44). if current_a <= 0.0: return 0.0 factor = IPC_K_EXTERNAL if external else IPC_K_INTERNAL area_mils2 = (width_mm / 0.0254) * (copper_um / 25.4) return ipc2221_temperature_rise_c(current_a, factor, area_mils2)
def iec60664_creepage_mm(working_voltage_v: f64): # IEC 60664-1 table 4, pollution degree 2, material group IIIa, linear between steps. last = len(IEC60664_VOLTAGE_STEPS) - 1 if working_voltage_v <= IEC60664_VOLTAGE_STEPS[0]: return IEC60664_CREEPAGE_MM[0] for index in range(1, last + 1): if working_voltage_v <= IEC60664_VOLTAGE_STEPS[index]: low_v = IEC60664_VOLTAGE_STEPS[index - 1] high_v = IEC60664_VOLTAGE_STEPS[index] low_mm = IEC60664_CREEPAGE_MM[index - 1] high_mm = IEC60664_CREEPAGE_MM[index] return low_mm + (high_mm - low_mm) * (working_voltage_v - low_v) / (high_v - low_v) return IEC60664_CREEPAGE_MM[last] * working_voltage_v / IEC60664_VOLTAGE_STEPS[last]
def route_plan_for(net: PcbNet, defaults: list[PcbRoutePlan], overrides: list[PcbRoutePlan]): for plan in overrides: if plan.net_id == net.id: return plan key = net_class_key(net.net_class) for plan in defaults: if plan.net_id == key: return plan return defaults[0]
def build_segments(nets: list[PcbNet], defaults: list[PcbRoutePlan], overrides: list[PcbRoutePlan]): mut segments = [] for net in nets: plan = route_plan_for(net, defaults, overrides) for index in range(1, len(net.pin_ids)): origin = net.pin_ids[0] if plan.topology == "star" else net.pin_ids[index - 1] segments.append(PcbSegment( id=net.id + "-s" + str(index), net_id=net.id, layer_id=plan.layer_id, width_mm=plan.width_mm, from_pin_id=origin, to_pin_id=net.pin_ids[index], via_count=plan.via_count, via_drill_mm=plan.via_drill_mm, via_pad_mm=plan.via_pad_mm, via_layer_id=plan.via_layer_id, )) return segments
def segment_polyline(segment: PcbSegment, pins: list[PcbPin], index_of: dict[str, int]): origin = pins[index_of.get(segment.from_pin_id, 0)] target = pins[index_of.get(segment.to_pin_id, 0)] return route_points(origin.x_mm, origin.y_mm, target.x_mm, target.y_mm)
def single_via_resistance_ohm(stackup: PcbStackup, segment: PcbSegment) !{}: height_mm = max(0.05, abs(layer_depth_mm(stackup, segment.via_layer_id) - layer_depth_mm(stackup, segment.layer_id))) return via_barrel_resistance_ohm(operating_resistivity(), height_mm * 0.001, segment.via_drill_mm * 0.001, VIA_PLATING_UM * 0.000001)
def via_series_resistance_ohm(stackup: PcbStackup, segment: PcbSegment) !{}: # A buried run needs one transition down and one back up; each is a parallel group. if segment.via_count == 0: return 0.0 return 2.0 * single_via_resistance_ohm(stackup, segment) / float(segment.via_count)
def copper_resistances_ohm(stackup: PcbStackup, segments: list[PcbSegment], lengths_mm: list[f64]) !{}: rho = operating_resistivity() mut values = [] for index in range(0, len(segments)): segment = segments[index] thickness = layer_copper_um(stackup, segment.layer_id) * 0.000001 values.append(conductor_resistance_ohm(rho, lengths_mm[index] * 0.001, segment.width_mm * 0.001, thickness)) return values
def solve_linear(matrix: list[list[f64]], rhs: list[f64]): equation: solution := solve(matrix, rhs) mut values = [] for index in range(0, len(rhs)): values.append(float(solution[index])) return values
def solve_network(pins: list[PcbPin], nets: list[PcbNet], segments: list[PcbSegment], resistances_ohm: list[f64], loads: list[PcbLoad]): # The unknown is the deviation from each net's declared feed potential. Every # branch stays inside one net, so the nominal levels cancel out of `G v = i` # exactly and the solve runs on millivolt-scale numbers instead of hundreds of # volts, which keeps the KCL residual at the floating-point floor on the 800 V # busbars. Absolute node potentials are restored after the solve. index_of = pin_index_map(pins) count = len(pins) mut nominal = [] mut is_fixed = [] for index in range(0, count): nominal.append(0.0) is_fixed.append(false) for net in nets: level = 0.0 if net.is_reference else net.nominal_voltage_v for pin_id in net.pin_ids: nominal[index_of.get(pin_id, 0)] = level is_fixed[index_of.get(net.pin_ids[0], 0)] = true mut unknown_of = [] mut unknowns = 0 for index in range(0, count): if is_fixed[index]: unknown_of.append(-1) else: unknown_of.append(unknowns) unknowns = unknowns + 1 mut conductance = [] mut rhs = [] for row in range(0, unknowns): mut cells = [] for column in range(0, unknowns): cells.append(0.0) conductance.append(cells) rhs.append(0.0) for index in range(0, len(segments)): left = index_of.get(segments[index].from_pin_id, 0) right = index_of.get(segments[index].to_pin_id, 0) gain = 1.0 / resistances_ohm[index] left_unknown = unknown_of[left] right_unknown = unknown_of[right] if left_unknown >= 0: conductance[left_unknown][left_unknown] = conductance[left_unknown][left_unknown] + gain if right_unknown >= 0: conductance[left_unknown][right_unknown] = conductance[left_unknown][right_unknown] - gain if right_unknown >= 0: conductance[right_unknown][right_unknown] = conductance[right_unknown][right_unknown] + gain if left_unknown >= 0: conductance[right_unknown][left_unknown] = conductance[right_unknown][left_unknown] - gain mut injected = [] for index in range(0, count): injected.append(0.0) for load in loads: sink = index_of.get(load.pin_id, 0) source = index_of.get(load.return_pin_id, 0) injected[sink] = injected[sink] - load.current_a injected[source] = injected[source] + load.current_a if unknown_of[sink] >= 0: rhs[unknown_of[sink]] = rhs[unknown_of[sink]] - load.current_a if unknown_of[source] >= 0: rhs[unknown_of[source]] = rhs[unknown_of[source]] + load.current_a deviations = solve_linear(conductance, rhs) mut residual = 0.0 for row in range(0, unknowns): mut accumulated = 0.0 for column in range(0, unknowns): accumulated = accumulated + conductance[row][column] * deviations[column] residual = max(residual, abs(accumulated - rhs[row])) mut node_ids = [] mut voltages = [] mut offsets = [] mut flow = [] for index in range(0, count): node_ids.append(pins[index].id) offset = 0.0 if is_fixed[index] else deviations[unknown_of[index]] offsets.append(offset) voltages.append(nominal[index] + offset) flow.append(0.0) mut currents = [] for index in range(0, len(segments)): left = index_of.get(segments[index].from_pin_id, 0) right = index_of.get(segments[index].to_pin_id, 0) branch = (offsets[left] - offsets[right]) / resistances_ohm[index] currents.append(branch) flow[left] = flow[left] + branch flow[right] = flow[right] - branch mut kcl = 0.0 mut supplied = 0.0 for index in range(0, count): if is_fixed[index]: supplied = supplied + max(0.0, flow[index] - injected[index]) else: kcl = max(kcl, abs(flow[index] - injected[index])) return PcbSolution(node_ids=node_ids, node_voltages=voltages, branch_currents=currents, fixed_count=count - unknowns, solver_residual=residual, kcl_residual_a=kcl, source_current_a=supplied)
def assemble_board(board_id: str, name: str, host_part_id: str, zone: str, substrate: str, stackup: PcbStackup, components: list[PcbComponent], pins: list[PcbPin], nets: list[PcbNet], defaults: list[PcbRoutePlan], overrides: list[PcbRoutePlan], loads: list[PcbLoad], outline_width_mm: f64, outline_height_mm: f64) !{}: index_of = pin_index_map(pins) segments = build_segments(nets, defaults, overrides) mut lengths = [] for segment in segments: lengths.append(max(MIN_SEGMENT_LENGTH_MM, polyline_length_mm(segment_polyline(segment, pins, index_of)))) copper_ohm = copper_resistances_ohm(stackup, segments, lengths) mut branch_ohm = [] for index in range(0, len(segments)): branch_ohm.append(copper_ohm[index] + via_series_resistance_ohm(stackup, segments[index])) solution = solve_network(pins, nets, segments, branch_ohm, loads) mut traces = [] mut vias = [] for index in range(0, len(segments)): segment = segments[index] current = abs(solution.branch_currents[index]) copper_um = layer_copper_um(stackup, segment.layer_id) external = layer_is_external(stackup, segment.layer_id) required = ipc_min_width_mm(current, copper_um, external) traces.append(PcbTrace( id=segment.id, net_id=segment.net_id, layer_id=segment.layer_id, width_mm=segment.width_mm, length_mm=lengths[index], current_a=current, from_pin_id=segment.from_pin_id, to_pin_id=segment.to_pin_id, resistance_mohm=copper_ohm[index] * 1000.0, voltage_drop_mv=current * copper_ohm[index] * 1000.0, power_loss_mw=joule_loss_w(current, copper_ohm[index]) * 1000.0, temperature_rise_c=ipc_temperature_rise_c(current, segment.width_mm, copper_um, external), ipc_min_width_mm=required, compliant=segment.width_mm >= required, )) if segment.via_count > 0: barrel_mohm = single_via_resistance_ohm(stackup, segment) * 1000.0 share = current / float(segment.via_count) for barrel in range(1, segment.via_count + 1): vias.append(PcbVia(id=segment.id + "-vd" + str(barrel), net_id=segment.net_id, drill_mm=segment.via_drill_mm, pad_mm=segment.via_pad_mm, from_layer_id=segment.via_layer_id, to_layer_id=segment.layer_id, current_a=share, resistance_mohm=barrel_mohm)) vias.append(PcbVia(id=segment.id + "-vu" + str(barrel), net_id=segment.net_id, drill_mm=segment.via_drill_mm, pad_mm=segment.via_pad_mm, from_layer_id=segment.layer_id, to_layer_id=segment.via_layer_id, current_a=share, resistance_mohm=barrel_mohm)) return PcbBoard(id=board_id, name=name, host_part_id=host_part_id, stackup=stackup, components=components, pins=pins, nets=nets, traces=traces, vias=vias, outline_width_mm=outline_width_mm, outline_height_mm=outline_height_mm, zone=zone, substrate=substrate)
def bms_slave_stackup(): layers = [ PcbLayer(id="bms-smt", name="Top soldermask", function=PcbLayerFunction.soldermask, copper_um=0.0, dielectric_mm=0.0254, dielectric_er=3.6, material="LPI soldermask"), PcbLayer(id="bms-l1", name="Top signal", function=PcbLayerFunction.signal, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="electrodeposited copper 1 oz"), PcbLayer(id="bms-pp1", name="Prepreg 1", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.2032, dielectric_er=4.3, material="2x1080 FR-4 prepreg"), PcbLayer(id="bms-l2", name="Inner ground plane", function=PcbLayerFunction.plane, copper_um=17.5, dielectric_mm=0.0, dielectric_er=1.0, material="rolled copper 0.5 oz"), PcbLayer(id="bms-core", name="Core", function=PcbLayerFunction.core, copper_um=0.0, dielectric_mm=1.0668, dielectric_er=4.5, material="FR-4 Tg170 core"), PcbLayer(id="bms-l3", name="Inner cell-tap plane", function=PcbLayerFunction.plane, copper_um=17.5, dielectric_mm=0.0, dielectric_er=1.0, material="rolled copper 0.5 oz"), PcbLayer(id="bms-pp2", name="Prepreg 2", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.2032, dielectric_er=4.3, material="2x1080 FR-4 prepreg"), PcbLayer(id="bms-l4", name="Bottom signal", function=PcbLayerFunction.signal, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="electrodeposited copper 1 oz"), PcbLayer(id="bms-smb", name="Bottom soldermask", function=PcbLayerFunction.soldermask, copper_um=0.0, dielectric_mm=0.0254, dielectric_er=3.6, material="LPI soldermask"), ] return PcbStackup(id="stk-bms", name="FR-4 4-layer 1.63 mm, 35 um outer copper", layers=layers, total_thickness_mm=stackup_thickness_mm(layers), ipc_class=3, impedance_target_ohm=100.0)
def bms_slave_components(): return [ PcbComponent(refdes="U1", part_number="LTC6811-1", description="12-cell monitor with integrated isoSPI; 3 channels modelled at pin level", footprint="SSOP-48", pin_count=48, x_mm=64.0, y_mm=39.0, rotation_deg=0.0, power_dissipation_w=0.42, mass_g=0.9), PcbComponent(refdes="T1", part_number="HX1188NL", description="isoSPI pulse transformer, port A", footprint="SM-6P", pin_count=6, x_mm=22.0, y_mm=58.0, rotation_deg=0.0, power_dissipation_w=0.04, mass_g=0.6), PcbComponent(refdes="T2", part_number="HX1188NL", description="isoSPI pulse transformer, port B", footprint="SM-6P", pin_count=6, x_mm=22.0, y_mm=20.0, rotation_deg=0.0, power_dissipation_w=0.04, mass_g=0.6), PcbComponent(refdes="J1", part_number="GH-4P", description="Daisy-chain connector, upstream module", footprint="JST-GH-4", pin_count=4, x_mm=6.0, y_mm=58.0, rotation_deg=90.0, power_dissipation_w=0.0, mass_g=0.5), PcbComponent(refdes="J2", part_number="GH-4P", description="Daisy-chain connector, downstream module", footprint="JST-GH-4", pin_count=4, x_mm=6.0, y_mm=20.0, rotation_deg=90.0, power_dissipation_w=0.0, mass_g=0.5), PcbComponent(refdes="J3", part_number="MX150-7", description="Cell-tap and thermistor harness header", footprint="MOLEX-MX150-7", pin_count=7, x_mm=118.0, y_mm=39.0, rotation_deg=270.0, power_dissipation_w=0.0, mass_g=6.0), PcbComponent(refdes="Q1", part_number="2N7002E", description="Passive balancing switch, channel 1", footprint="SOT-23", pin_count=3, x_mm=92.0, y_mm=26.0, rotation_deg=0.0, power_dissipation_w=0.01, mass_g=0.01), PcbComponent(refdes="Q2", part_number="2N7002E", description="Passive balancing switch, channel 2", footprint="SOT-23", pin_count=3, x_mm=92.0, y_mm=39.0, rotation_deg=0.0, power_dissipation_w=0.01, mass_g=0.01), PcbComponent(refdes="Q3", part_number="2N7002E", description="Passive balancing switch, channel 3", footprint="SOT-23", pin_count=3, x_mm=92.0, y_mm=52.0, rotation_deg=0.0, power_dissipation_w=0.01, mass_g=0.01), PcbComponent(refdes="R1", part_number="CRGH1206-33R", description="Balancing bleed resistor, channel 1", footprint="1206", pin_count=2, x_mm=104.0, y_mm=26.0, rotation_deg=0.0, power_dissipation_w=0.53, mass_g=0.02), PcbComponent(refdes="R2", part_number="CRGH1206-33R", description="Balancing bleed resistor, channel 2", footprint="1206", pin_count=2, x_mm=104.0, y_mm=39.0, rotation_deg=0.0, power_dissipation_w=0.53, mass_g=0.02), PcbComponent(refdes="R3", part_number="CRGH1206-33R", description="Balancing bleed resistor, channel 3", footprint="1206", pin_count=2, x_mm=104.0, y_mm=52.0, rotation_deg=0.0, power_dissipation_w=0.53, mass_g=0.02), PcbComponent(refdes="RT1", part_number="NTCG163JF103FT", description="Module thermistor, cell block front", footprint="0603", pin_count=2, x_mm=40.0, y_mm=33.0, rotation_deg=0.0, power_dissipation_w=0.001, mass_g=0.005), PcbComponent(refdes="RT2", part_number="NTCG163JF103FT", description="Module thermistor, cell block rear", footprint="0603", pin_count=2, x_mm=40.0, y_mm=27.0, rotation_deg=0.0, power_dissipation_w=0.001, mass_g=0.005), PcbComponent(refdes="C1", part_number="GRM188R71H104K", description="Monitor regulator decoupling, 100 nF", footprint="0603", pin_count=2, x_mm=48.0, y_mm=24.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=0.005), PcbComponent(refdes="C2", part_number="GRM31CR71H105K", description="Cell-group bulk capacitor, 1 uF", footprint="1206", pin_count=2, x_mm=80.0, y_mm=60.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=0.02), ]
def bms_slave_pins(): return [ pcb_pin("U1", 1, "V+", "bmsA-stack-plus", 72.0, 52.0, PcbPinKind.power), pcb_pin("U1", 2, "C3", "bmsA-stack-plus", 72.0, 49.0, PcbPinKind.analog), pcb_pin("U1", 3, "C2", "bmsA-tap2", 72.0, 46.0, PcbPinKind.analog), pcb_pin("U1", 4, "C1", "bmsA-tap1", 72.0, 43.0, PcbPinKind.analog), pcb_pin("U1", 5, "V-", "bmsA-gnd", 72.0, 40.0, PcbPinKind.ground), pcb_pin("U1", 6, "S3", "bmsA-bal3", 72.0, 37.0, PcbPinKind.signal), pcb_pin("U1", 7, "S2", "bmsA-bal2", 72.0, 34.0, PcbPinKind.signal), pcb_pin("U1", 8, "S1", "bmsA-bal1", 72.0, 31.0, PcbPinKind.signal), pcb_pin("U1", 9, "VREG", "bmsA-vreg", 56.0, 31.0, PcbPinKind.power), pcb_pin("U1", 10, "GPIO1", "bmsA-ntc1", 56.0, 34.0, PcbPinKind.analog), pcb_pin("U1", 11, "GPIO2", "bmsA-ntc2", 56.0, 37.0, PcbPinKind.analog), pcb_pin("U1", 12, "IPA", "bmsA-isoa-p", 56.0, 40.0, PcbPinKind.signal), pcb_pin("U1", 13, "IMA", "bmsA-isoa-n", 56.0, 43.0, PcbPinKind.signal), pcb_pin("U1", 14, "IPB", "bmsA-isob-p", 56.0, 46.0, PcbPinKind.signal), pcb_pin("U1", 15, "IMB", "bmsA-isob-n", 56.0, 49.0, PcbPinKind.signal), pcb_pin("T1", 1, "P1", "bmsA-isoa-p", 28.0, 62.0, PcbPinKind.signal), pcb_pin("T1", 2, "P2", "bmsA-isoa-n", 28.0, 58.0, PcbPinKind.signal), pcb_pin("T1", 3, "PSH", "bmsA-gnd", 28.0, 54.0, PcbPinKind.ground), pcb_pin("T1", 4, "S1", "bmsA-cablea-p", 16.0, 62.0, PcbPinKind.signal), pcb_pin("T1", 5, "S2", "bmsA-cablea-n", 16.0, 58.0, PcbPinKind.signal), pcb_pin("T1", 6, "SSH", "bmsA-gnd", 16.0, 54.0, PcbPinKind.ground), pcb_pin("T2", 1, "P1", "bmsA-isob-p", 28.0, 24.0, PcbPinKind.signal), pcb_pin("T2", 2, "P2", "bmsA-isob-n", 28.0, 20.0, PcbPinKind.signal), pcb_pin("T2", 3, "PSH", "bmsA-gnd", 28.0, 16.0, PcbPinKind.ground), pcb_pin("T2", 4, "S1", "bmsA-cableb-p", 16.0, 24.0, PcbPinKind.signal), pcb_pin("T2", 5, "S2", "bmsA-cableb-n", 16.0, 20.0, PcbPinKind.signal), pcb_pin("T2", 6, "SSH", "bmsA-gnd", 16.0, 16.0, PcbPinKind.ground), pcb_pin("J1", 1, "UP_P", "bmsA-cablea-p", 6.0, 64.0, PcbPinKind.signal), pcb_pin("J1", 2, "UP_N", "bmsA-cablea-n", 6.0, 60.0, PcbPinKind.signal), pcb_pin("J1", 3, "UP_SH", "bmsA-gnd", 6.0, 56.0, PcbPinKind.ground), pcb_pin("J1", 4, "UP_DRAIN", "bmsA-gnd", 6.0, 52.0, PcbPinKind.ground), pcb_pin("J2", 1, "DN_P", "bmsA-cableb-p", 6.0, 26.0, PcbPinKind.signal), pcb_pin("J2", 2, "DN_N", "bmsA-cableb-n", 6.0, 22.0, PcbPinKind.signal), pcb_pin("J2", 3, "DN_SH", "bmsA-gnd", 6.0, 18.0, PcbPinKind.ground), pcb_pin("J2", 4, "DN_DRAIN", "bmsA-gnd", 6.0, 14.0, PcbPinKind.ground), pcb_pin("J3", 1, "STACK+", "bmsA-stack-plus", 118.0, 57.0, PcbPinKind.power), pcb_pin("J3", 2, "TAP2", "bmsA-tap2", 118.0, 51.0, PcbPinKind.power), pcb_pin("J3", 3, "TAP1", "bmsA-tap1", 118.0, 45.0, PcbPinKind.power), pcb_pin("J3", 4, "STACK-", "bmsA-gnd", 118.0, 39.0, PcbPinKind.ground), pcb_pin("J3", 5, "NTC1", "bmsA-ntc1", 118.0, 33.0, PcbPinKind.thermal), pcb_pin("J3", 6, "NTC2", "bmsA-ntc2", 118.0, 27.0, PcbPinKind.thermal), pcb_pin("J3", 7, "NTC_RTN", "bmsA-gnd", 118.0, 21.0, PcbPinKind.ground), pcb_pin("Q1", 1, "G", "bmsA-bal1", 89.0, 26.0, PcbPinKind.signal), pcb_pin("Q1", 2, "S", "bmsA-gnd", 95.0, 24.0, PcbPinKind.ground), pcb_pin("Q1", 3, "D", "bmsA-bleed1", 95.0, 28.0, PcbPinKind.power), pcb_pin("Q2", 1, "G", "bmsA-bal2", 89.0, 39.0, PcbPinKind.signal), pcb_pin("Q2", 2, "S", "bmsA-tap1", 95.0, 37.0, PcbPinKind.power), pcb_pin("Q2", 3, "D", "bmsA-bleed2", 95.0, 41.0, PcbPinKind.power), pcb_pin("Q3", 1, "G", "bmsA-bal3", 89.0, 52.0, PcbPinKind.signal), pcb_pin("Q3", 2, "S", "bmsA-tap2", 95.0, 50.0, PcbPinKind.power), pcb_pin("Q3", 3, "D", "bmsA-bleed3", 95.0, 54.0, PcbPinKind.power), pcb_pin("R1", 1, "A", "bmsA-bleed1", 101.0, 26.0, PcbPinKind.power), pcb_pin("R1", 2, "B", "bmsA-tap1", 107.0, 26.0, PcbPinKind.power), pcb_pin("R2", 1, "A", "bmsA-bleed2", 101.0, 39.0, PcbPinKind.power), pcb_pin("R2", 2, "B", "bmsA-tap2", 107.0, 39.0, PcbPinKind.power), pcb_pin("R3", 1, "A", "bmsA-bleed3", 101.0, 52.0, PcbPinKind.power), pcb_pin("R3", 2, "B", "bmsA-stack-plus", 107.0, 52.0, PcbPinKind.power), pcb_pin("RT1", 1, "A", "bmsA-ntc1", 38.2, 33.0, PcbPinKind.thermal), pcb_pin("RT1", 2, "B", "bmsA-gnd", 41.8, 33.0, PcbPinKind.ground), pcb_pin("RT2", 1, "A", "bmsA-ntc2", 38.2, 27.0, PcbPinKind.thermal), pcb_pin("RT2", 2, "B", "bmsA-gnd", 41.8, 27.0, PcbPinKind.ground), pcb_pin("C1", 1, "A", "bmsA-vreg", 46.2, 24.0, PcbPinKind.power), pcb_pin("C1", 2, "B", "bmsA-gnd", 49.8, 24.0, PcbPinKind.ground), pcb_pin("C2", 1, "A", "bmsA-stack-plus", 78.0, 60.0, PcbPinKind.power), pcb_pin("C2", 2, "B", "bmsA-gnd", 82.0, 60.0, PcbPinKind.ground), ]
def bms_slave_nets(): return [ PcbNet(id="bmsA-stack-plus", name="Modelled cell-group positive", net_class=PcbNetClass.power, pin_ids=["J3.1", "U1.1", "U1.2", "C2.1", "R3.2"], nominal_voltage_v=12.6, current_a=0.1405, is_reference=false), PcbNet(id="bmsA-tap2", name="Cell tap 2", net_class=PcbNetClass.power, pin_ids=["J3.2", "U1.3", "Q3.2", "R2.2"], nominal_voltage_v=8.4, current_a=0.127, is_reference=false), PcbNet(id="bmsA-tap1", name="Cell tap 1", net_class=PcbNetClass.power, pin_ids=["J3.3", "U1.4", "Q2.2", "R1.2"], nominal_voltage_v=4.2, current_a=0.127, is_reference=false), PcbNet(id="bmsA-gnd", name="Module reference, cell-group negative", net_class=PcbNetClass.ground, pin_ids=["J3.4", "J3.7", "U1.5", "Q1.2", "C1.2", "C2.2", "RT1.2", "RT2.2", "T1.3", "T1.6", "T2.3", "T2.6", "J1.3", "J1.4", "J2.3", "J2.4"], nominal_voltage_v=0.0, current_a=0.1414, is_reference=true), PcbNet(id="bmsA-vreg", name="Monitor regulator rail", net_class=PcbNetClass.power, pin_ids=["U1.9", "C1.1"], nominal_voltage_v=5.0, current_a=0.0, is_reference=false), PcbNet(id="bmsA-ntc1", name="Thermistor channel 1", net_class=PcbNetClass.analog, pin_ids=["U1.10", "RT1.1", "J3.5"], nominal_voltage_v=2.5, current_a=0.00025, is_reference=false), PcbNet(id="bmsA-ntc2", name="Thermistor channel 2", net_class=PcbNetClass.analog, pin_ids=["U1.11", "RT2.1", "J3.6"], nominal_voltage_v=2.5, current_a=0.00025, is_reference=false), PcbNet(id="bmsA-isoa-p", name="isoSPI port A positive", net_class=PcbNetClass.signal, pin_ids=["U1.12", "T1.1"], nominal_voltage_v=1.25, current_a=0.02, is_reference=false), PcbNet(id="bmsA-isoa-n", name="isoSPI port A negative", net_class=PcbNetClass.signal, pin_ids=["U1.13", "T1.2"], nominal_voltage_v=0.0, current_a=0.02, is_reference=false), PcbNet(id="bmsA-isob-p", name="isoSPI port B positive", net_class=PcbNetClass.signal, pin_ids=["U1.14", "T2.1"], nominal_voltage_v=1.25, current_a=0.02, is_reference=false), PcbNet(id="bmsA-isob-n", name="isoSPI port B negative", net_class=PcbNetClass.signal, pin_ids=["U1.15", "T2.2"], nominal_voltage_v=0.0, current_a=0.02, is_reference=false), PcbNet(id="bmsA-cablea-p", name="Daisy-chain cable A positive", net_class=PcbNetClass.signal, pin_ids=["T1.4", "J1.1"], nominal_voltage_v=1.25, current_a=0.02, is_reference=false), PcbNet(id="bmsA-cablea-n", name="Daisy-chain cable A negative", net_class=PcbNetClass.signal, pin_ids=["T1.5", "J1.2"], nominal_voltage_v=0.0, current_a=0.02, is_reference=false), PcbNet(id="bmsA-cableb-p", name="Daisy-chain cable B positive", net_class=PcbNetClass.signal, pin_ids=["T2.4", "J2.1"], nominal_voltage_v=1.25, current_a=0.02, is_reference=false), PcbNet(id="bmsA-cableb-n", name="Daisy-chain cable B negative", net_class=PcbNetClass.signal, pin_ids=["T2.5", "J2.2"], nominal_voltage_v=0.0, current_a=0.02, is_reference=false), PcbNet(id="bmsA-bal1", name="Balancing gate drive 1", net_class=PcbNetClass.signal, pin_ids=["U1.8", "Q1.1"], nominal_voltage_v=5.0, current_a=0.0002, is_reference=false), PcbNet(id="bmsA-bal2", name="Balancing gate drive 2", net_class=PcbNetClass.signal, pin_ids=["U1.7", "Q2.1"], nominal_voltage_v=5.0, current_a=0.0002, is_reference=false), PcbNet(id="bmsA-bal3", name="Balancing gate drive 3", net_class=PcbNetClass.signal, pin_ids=["U1.6", "Q3.1"], nominal_voltage_v=5.0, current_a=0.0002, is_reference=false), PcbNet(id="bmsA-bleed1", name="Bleed path 1", net_class=PcbNetClass.power, pin_ids=["Q1.3", "R1.1"], nominal_voltage_v=0.0, current_a=0.127, is_reference=false), PcbNet(id="bmsA-bleed2", name="Bleed path 2", net_class=PcbNetClass.power, pin_ids=["Q2.3", "R2.1"], nominal_voltage_v=4.2, current_a=0.127, is_reference=false), PcbNet(id="bmsA-bleed3", name="Bleed path 3", net_class=PcbNetClass.power, pin_ids=["Q3.3", "R3.1"], nominal_voltage_v=8.4, current_a=0.127, is_reference=false), ]
def bms_slave_plans(): return [ PcbRoutePlan(net_id="power", layer_id="bms-l1", width_mm=1.2, topology="chain", via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="ground", layer_id="bms-l2", width_mm=3.0, topology="star", via_count=1, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="signal", layer_id="bms-l1", width_mm=0.3, topology="chain", via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="analog", layer_id="bms-l1", width_mm=0.3, topology="chain", via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="hv", layer_id="bms-l1", width_mm=1.2, topology="chain", via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="can", layer_id="bms-l1", width_mm=0.3, topology="chain", via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="ethernet", layer_id="bms-l1", width_mm=0.25, topology="chain", via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), ]
def bms_slave_overrides(): return [ PcbRoutePlan(net_id="bmsA-stack-plus", layer_id="bms-l3", width_mm=1.6, topology="chain", via_count=2, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="bmsA-tap2", layer_id="bms-l3", width_mm=1.6, topology="chain", via_count=2, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), PcbRoutePlan(net_id="bmsA-tap1", layer_id="bms-l3", width_mm=1.6, topology="chain", via_count=2, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id="bms-l1"), ]
def bms_slave_loads(): board = "bms-slave" return [ pcb_load(board, "U1.1", "U1.5", 0.0135, "stack monitor active supply current"), pcb_load(board, "R3.2", "R3.1", 0.127, "bleed resistor body, channel 3"), pcb_load(board, "Q3.3", "Q3.2", 0.127, "balancing switch channel, channel 3"), pcb_load(board, "R2.2", "R2.1", 0.127, "bleed resistor body, channel 2"), pcb_load(board, "Q2.3", "Q2.2", 0.127, "balancing switch channel, channel 2"), pcb_load(board, "R1.2", "R1.1", 0.127, "bleed resistor body, channel 1"), pcb_load(board, "Q1.3", "Q1.2", 0.127, "balancing switch channel, channel 1"), pcb_load(board, "RT1.1", "RT1.2", 0.00025, "thermistor 1 bias current"), pcb_load(board, "RT2.1", "RT2.2", 0.00025, "thermistor 2 bias current"), pcb_load(board, "T1.1", "T1.2", 0.02, "isoSPI port A transformer primary"), pcb_load(board, "J1.1", "J1.2", 0.02, "isoSPI port A cable pair"), pcb_load(board, "T2.1", "T2.2", 0.02, "isoSPI port B transformer primary"), pcb_load(board, "J2.1", "J2.2", 0.02, "isoSPI port B cable pair"), pcb_load(board, "Q1.1", "Q1.2", 0.0002, "gate drive current, channel 1"), pcb_load(board, "Q2.1", "Q2.2", 0.0002, "gate drive current, channel 2"), pcb_load(board, "Q3.1", "Q3.2", 0.0002, "gate drive current, channel 3"), ]
def bms_slave_board() !{}: return assemble_board("bms-slave", "BMS cell-monitoring slave board", "battery-management", "battery enclosure, module head 3", "FR-4 Tg170, 4 copper layers, 35 um outer", bms_slave_stackup(), bms_slave_components(), bms_slave_pins(), bms_slave_nets(), bms_slave_plans(), bms_slave_overrides(), bms_slave_loads(), 128.0, 78.0)
def front_zone_stackup(): layers = [ PcbLayer(id="fzc-smt", name="Top soldermask", function=PcbLayerFunction.soldermask, copper_um=0.0, dielectric_mm=0.0254, dielectric_er=3.6, material="LPI soldermask"), PcbLayer(id="fzc-l1", name="Top signal and power", function=PcbLayerFunction.signal, copper_um=70.0, dielectric_mm=0.0, dielectric_er=1.0, material="electrodeposited copper 2 oz"), PcbLayer(id="fzc-pp1", name="Prepreg 1", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.1016, dielectric_er=4.2, material="1080 FR-4 prepreg"), PcbLayer(id="fzc-l2", name="Dedicated ground plane", function=PcbLayerFunction.plane, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="rolled copper 1 oz"), PcbLayer(id="fzc-core1", name="Core 1", function=PcbLayerFunction.core, copper_um=0.0, dielectric_mm=0.3, dielectric_er=4.5, material="FR-4 Tg170 core"), PcbLayer(id="fzc-l3", name="Inner power routing", function=PcbLayerFunction.signal, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="rolled copper 1 oz"), PcbLayer(id="fzc-pp2", name="Prepreg 2", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.5, dielectric_er=4.4, material="7628 FR-4 prepreg"), PcbLayer(id="fzc-l4", name="Inner signal routing", function=PcbLayerFunction.signal, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="rolled copper 1 oz"), PcbLayer(id="fzc-core2", name="Core 2", function=PcbLayerFunction.core, copper_um=0.0, dielectric_mm=0.3, dielectric_er=4.5, material="FR-4 Tg170 core"), PcbLayer(id="fzc-l5", name="Supply plane", function=PcbLayerFunction.plane, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="rolled copper 1 oz"), PcbLayer(id="fzc-pp3", name="Prepreg 3", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.1016, dielectric_er=4.2, material="1080 FR-4 prepreg"), PcbLayer(id="fzc-l6", name="Bottom signal and power", function=PcbLayerFunction.signal, copper_um=70.0, dielectric_mm=0.0, dielectric_er=1.0, material="electrodeposited copper 2 oz"), PcbLayer(id="fzc-smb", name="Bottom soldermask", function=PcbLayerFunction.soldermask, copper_um=0.0, dielectric_mm=0.0254, dielectric_er=3.6, material="LPI soldermask"), ] return PcbStackup(id="stk-fzc", name="FR-4 6-layer 1.63 mm, dedicated ground plane", layers=layers, total_thickness_mm=stackup_thickness_mm(layers), ipc_class=3, impedance_target_ohm=100.0)
def front_zone_components(): return [ PcbComponent(refdes="U1", part_number="S32K344", description="Lockstep automotive MCU; 12 pins modelled of 172", footprint="LQFP-172", pin_count=172, x_mm=40.0, y_mm=70.0, rotation_deg=0.0, power_dissipation_w=1.05, mass_g=1.4), PcbComponent(refdes="U2", part_number="LM5145", description="48 V to 12 V synchronous buck controller", footprint="HTSSOP-20", pin_count=20, x_mm=95.0, y_mm=88.0, rotation_deg=0.0, power_dissipation_w=0.18, mass_g=0.3), PcbComponent(refdes="Q1", part_number="BSC098N10NS5", description="Buck high-side switch, 100 V", footprint="PQFN-5x6", pin_count=8, x_mm=108.0, y_mm=96.0, rotation_deg=0.0, power_dissipation_w=0.62, mass_g=0.2), PcbComponent(refdes="Q2", part_number="BSC098N10NS5", description="Buck low-side switch, 100 V", footprint="PQFN-5x6", pin_count=8, x_mm=108.0, y_mm=82.0, rotation_deg=0.0, power_dissipation_w=0.88, mass_g=0.2), PcbComponent(refdes="L1", part_number="XAL1510-103", description="10 uH shielded power inductor", footprint="IND-15x15", pin_count=2, x_mm=120.0, y_mm=89.0, rotation_deg=0.0, power_dissipation_w=0.71, mass_g=6.4), PcbComponent(refdes="C1", part_number="CGA9N3X7S2A106K", description="48 V input bulk capacitor, 10 uF 100 V", footprint="1210", pin_count=2, x_mm=95.0, y_mm=100.0, rotation_deg=0.0, power_dissipation_w=0.02, mass_g=0.1), PcbComponent(refdes="C2", part_number="CGA9P1X7R1E476M", description="12 V bulk capacitor, 47 uF 25 V", footprint="1210", pin_count=2, x_mm=130.0, y_mm=100.0, rotation_deg=0.0, power_dissipation_w=0.02, mass_g=0.1), PcbComponent(refdes="U6", part_number="TPS7B6950-Q1", description="12 V to 5 V low-dropout regulator", footprint="DPAK", pin_count=3, x_mm=95.0, y_mm=62.0, rotation_deg=0.0, power_dissipation_w=3.85, mass_g=0.4), PcbComponent(refdes="C3", part_number="GRM21BR61A106K", description="5 V output capacitor, 10 uF", footprint="0805", pin_count=2, x_mm=105.0, y_mm=55.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=0.01), PcbComponent(refdes="U7", part_number="TPS7A2033", description="5 V to 3V3 low-dropout regulator", footprint="SOT-23-5", pin_count=5, x_mm=80.0, y_mm=55.0, rotation_deg=0.0, power_dissipation_w=0.73, mass_g=0.02), PcbComponent(refdes="C4", part_number="GRM21BR61A106K", description="3V3 output capacitor, 10 uF", footprint="0805", pin_count=2, x_mm=68.0, y_mm=50.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=0.01), PcbComponent(refdes="U3", part_number="TCAN1462-Q1", description="CAN-FD transceiver, powertrain bus", footprint="SOIC-8", pin_count=8, x_mm=24.0, y_mm=92.0, rotation_deg=0.0, power_dissipation_w=0.21, mass_g=0.07), PcbComponent(refdes="U4", part_number="TCAN1462-Q1", description="CAN-FD transceiver, chassis bus", footprint="SOIC-8", pin_count=8, x_mm=24.0, y_mm=76.0, rotation_deg=0.0, power_dissipation_w=0.21, mass_g=0.07), PcbComponent(refdes="R1", part_number="CRCW0603120R", description="CAN bus split termination, powertrain", footprint="0603", pin_count=2, x_mm=14.0, y_mm=96.0, rotation_deg=90.0, power_dissipation_w=0.07, mass_g=0.005), PcbComponent(refdes="R2", part_number="CRCW0603120R", description="CAN bus split termination, chassis", footprint="0603", pin_count=2, x_mm=14.0, y_mm=72.0, rotation_deg=90.0, power_dissipation_w=0.07, mass_g=0.005), PcbComponent(refdes="U5", part_number="TJA1101B", description="100BASE-T1 Ethernet PHY", footprint="HVQFN-36", pin_count=36, x_mm=26.0, y_mm=40.0, rotation_deg=0.0, power_dissipation_w=0.42, mass_g=0.1), PcbComponent(refdes="T1", part_number="TP-23015NL", description="100BASE-T1 common-mode choke and magnetics", footprint="SM-4P", pin_count=4, x_mm=12.0, y_mm=40.0, rotation_deg=0.0, power_dissipation_w=0.03, mass_g=0.4), PcbComponent(refdes="U8", part_number="VN7016AJ", description="High-side eFuse channel 1, low-beam lamp", footprint="PowerSSO-12", pin_count=12, x_mm=120.0, y_mm=62.0, rotation_deg=0.0, power_dissipation_w=0.24, mass_g=0.3), PcbComponent(refdes="U9", part_number="VN7016AJ", description="High-side eFuse channel 2, washer heater", footprint="PowerSSO-12", pin_count=12, x_mm=120.0, y_mm=44.0, rotation_deg=0.0, power_dissipation_w=0.17, mass_g=0.3), PcbComponent(refdes="U10", part_number="VN7016AJ", description="High-side eFuse channel 3, grille shutter actuator", footprint="PowerSSO-12", pin_count=12, x_mm=120.0, y_mm=26.0, rotation_deg=0.0, power_dissipation_w=0.11, mass_g=0.3), PcbComponent(refdes="J1", part_number="MX150-10", description="Zone supply, CAN and Ethernet header", footprint="MOLEX-MX150-10", pin_count=10, x_mm=139.0, y_mm=86.0, rotation_deg=270.0, power_dissipation_w=0.0, mass_g=8.0), PcbComponent(refdes="J2", part_number="MX150-7", description="Switched load output header", footprint="MOLEX-MX150-7", pin_count=7, x_mm=139.0, y_mm=34.0, rotation_deg=270.0, power_dissipation_w=0.0, mass_g=6.0), ]
def front_zone_pins(): return [ pcb_pin("U1", 1, "VDD_3V3", "fzc-3v3", 33.0, 77.0, PcbPinKind.power), pcb_pin("U1", 2, "VSS", "fzc-gnd", 33.0, 73.0, PcbPinKind.ground), pcb_pin("U1", 3, "CAN0_TX", "fzc-can0-tx", 33.0, 69.0, PcbPinKind.signal), pcb_pin("U1", 4, "CAN0_RX", "fzc-can0-rx", 33.0, 65.0, PcbPinKind.signal), pcb_pin("U1", 5, "CAN1_TX", "fzc-can1-tx", 33.0, 63.0, PcbPinKind.signal), pcb_pin("U1", 6, "CAN1_RX", "fzc-can1-rx", 37.0, 63.0, PcbPinKind.signal), pcb_pin("U1", 7, "MII_TXD", "fzc-phy-txd", 41.0, 63.0, PcbPinKind.signal), pcb_pin("U1", 8, "MII_RXD", "fzc-phy-rxd", 45.0, 63.0, PcbPinKind.signal), pcb_pin("U1", 9, "PWM_HS1", "fzc-hs1-in", 47.0, 65.0, PcbPinKind.signal), pcb_pin("U1", 10, "ADC_ISENSE", "fzc-hs-sense", 47.0, 69.0, PcbPinKind.analog), pcb_pin("U1", 11, "PWM_HS2", "fzc-hs2-in", 47.0, 73.0, PcbPinKind.signal), pcb_pin("U1", 12, "PWM_HS3", "fzc-hs3-in", 47.0, 77.0, PcbPinKind.signal), pcb_pin("U2", 1, "VIN", "fzc-48v", 89.0, 92.0, PcbPinKind.power), pcb_pin("U2", 2, "HO", "fzc-gate-hs", 101.0, 92.0, PcbPinKind.signal), pcb_pin("U2", 3, "SW", "fzc-sw", 101.0, 88.0, PcbPinKind.power), pcb_pin("U2", 4, "LO", "fzc-gate-ls", 101.0, 84.0, PcbPinKind.signal), pcb_pin("U2", 5, "AGND", "fzc-gnd", 89.0, 84.0, PcbPinKind.ground), pcb_pin("Q1", 1, "D", "fzc-48v", 108.0, 100.0, PcbPinKind.power), pcb_pin("Q1", 2, "G", "fzc-gate-hs", 104.0, 96.0, PcbPinKind.signal), pcb_pin("Q1", 3, "S", "fzc-sw", 108.0, 92.0, PcbPinKind.power), pcb_pin("Q2", 1, "D", "fzc-sw", 108.0, 86.0, PcbPinKind.power), pcb_pin("Q2", 2, "G", "fzc-gate-ls", 104.0, 82.0, PcbPinKind.signal), pcb_pin("Q2", 3, "S", "fzc-gnd", 108.0, 78.0, PcbPinKind.ground), pcb_pin("L1", 1, "A", "fzc-sw", 114.0, 89.0, PcbPinKind.power), pcb_pin("L1", 2, "B", "fzc-12v", 126.0, 89.0, PcbPinKind.power), pcb_pin("C1", 1, "A", "fzc-48v", 92.0, 100.0, PcbPinKind.power), pcb_pin("C1", 2, "B", "fzc-gnd", 98.0, 100.0, PcbPinKind.ground), pcb_pin("C2", 1, "A", "fzc-12v", 127.0, 100.0, PcbPinKind.power), pcb_pin("C2", 2, "B", "fzc-gnd", 133.0, 100.0, PcbPinKind.ground), pcb_pin("U6", 1, "IN", "fzc-12v", 95.0, 66.0, PcbPinKind.power), pcb_pin("U6", 2, "GND", "fzc-gnd", 91.0, 60.0, PcbPinKind.ground), pcb_pin("U6", 3, "OUT", "fzc-5v", 99.0, 60.0, PcbPinKind.power), pcb_pin("C3", 1, "A", "fzc-5v", 103.0, 55.0, PcbPinKind.power), pcb_pin("C3", 2, "B", "fzc-gnd", 107.0, 55.0, PcbPinKind.ground), pcb_pin("U7", 1, "IN", "fzc-5v", 84.0, 57.0, PcbPinKind.power), pcb_pin("U7", 2, "GND", "fzc-gnd", 84.0, 53.0, PcbPinKind.ground), pcb_pin("U7", 3, "OUT", "fzc-3v3", 76.0, 53.0, PcbPinKind.power), pcb_pin("U7", 4, "EN", "fzc-5v", 76.0, 57.0, PcbPinKind.signal), pcb_pin("C4", 1, "A", "fzc-3v3", 66.0, 50.0, PcbPinKind.power), pcb_pin("C4", 2, "B", "fzc-gnd", 70.0, 50.0, PcbPinKind.ground), pcb_pin("U3", 1, "TXD", "fzc-can0-tx", 29.0, 95.0, PcbPinKind.signal), pcb_pin("U3", 2, "GND", "fzc-gnd", 29.0, 92.0, PcbPinKind.ground), pcb_pin("U3", 3, "VCC", "fzc-5v", 29.0, 89.0, PcbPinKind.power), pcb_pin("U3", 4, "RXD", "fzc-can0-rx", 32.0, 95.0, PcbPinKind.signal), pcb_pin("U3", 5, "CANH", "fzc-can0-h", 19.0, 95.0, PcbPinKind.signal), pcb_pin("U3", 6, "CANL", "fzc-can0-l", 19.0, 92.0, PcbPinKind.signal), pcb_pin("U4", 1, "TXD", "fzc-can1-tx", 29.0, 79.0, PcbPinKind.signal), pcb_pin("U4", 2, "GND", "fzc-gnd", 29.0, 76.0, PcbPinKind.ground), pcb_pin("U4", 3, "VCC", "fzc-5v", 29.0, 73.0, PcbPinKind.power), pcb_pin("U4", 4, "RXD", "fzc-can1-rx", 32.0, 79.0, PcbPinKind.signal), pcb_pin("U4", 5, "CANH", "fzc-can1-h", 19.0, 79.0, PcbPinKind.signal), pcb_pin("U4", 6, "CANL", "fzc-can1-l", 19.0, 76.0, PcbPinKind.signal), pcb_pin("R1", 1, "A", "fzc-can0-h", 14.0, 97.6, PcbPinKind.signal), pcb_pin("R1", 2, "B", "fzc-can0-l", 14.0, 94.4, PcbPinKind.signal), pcb_pin("R2", 1, "A", "fzc-can1-h", 14.0, 73.6, PcbPinKind.signal), pcb_pin("R2", 2, "B", "fzc-can1-l", 14.0, 70.4, PcbPinKind.signal), pcb_pin("U5", 1, "VDDD", "fzc-3v3", 31.0, 45.0, PcbPinKind.power), pcb_pin("U5", 2, "VSS", "fzc-gnd", 31.0, 41.0, PcbPinKind.ground), pcb_pin("U5", 3, "TRX_P", "fzc-eth-p", 21.0, 42.0, PcbPinKind.signal), pcb_pin("U5", 4, "TRX_N", "fzc-eth-n", 21.0, 38.0, PcbPinKind.signal), pcb_pin("U5", 5, "MII_RXD", "fzc-phy-txd", 31.0, 37.0, PcbPinKind.signal), pcb_pin("U5", 6, "MII_TXD", "fzc-phy-rxd", 31.0, 34.0, PcbPinKind.signal), pcb_pin("T1", 1, "P1", "fzc-eth-p", 16.0, 42.0, PcbPinKind.signal), pcb_pin("T1", 2, "P2", "fzc-eth-n", 16.0, 38.0, PcbPinKind.signal), pcb_pin("T1", 3, "S1", "fzc-cable-eth-p", 8.0, 42.0, PcbPinKind.signal), pcb_pin("T1", 4, "S2", "fzc-cable-eth-n", 8.0, 38.0, PcbPinKind.signal), pcb_pin("U8", 1, "VCC", "fzc-12v", 116.0, 66.0, PcbPinKind.power), pcb_pin("U8", 2, "IN", "fzc-hs1-in", 116.0, 62.0, PcbPinKind.signal), pcb_pin("U8", 3, "OUT", "fzc-out1", 124.0, 62.0, PcbPinKind.power), pcb_pin("U8", 4, "GND", "fzc-gnd", 116.0, 58.0, PcbPinKind.ground), pcb_pin("U8", 5, "CS", "fzc-hs-sense", 124.0, 58.0, PcbPinKind.analog), pcb_pin("U9", 1, "VCC", "fzc-12v", 116.0, 48.0, PcbPinKind.power), pcb_pin("U9", 2, "IN", "fzc-hs2-in", 116.0, 44.0, PcbPinKind.signal), pcb_pin("U9", 3, "OUT", "fzc-out2", 124.0, 44.0, PcbPinKind.power), pcb_pin("U9", 4, "GND", "fzc-gnd", 116.0, 40.0, PcbPinKind.ground), pcb_pin("U10", 1, "VCC", "fzc-12v", 116.0, 30.0, PcbPinKind.power), pcb_pin("U10", 2, "IN", "fzc-hs3-in", 116.0, 26.0, PcbPinKind.signal), pcb_pin("U10", 3, "OUT", "fzc-out3", 124.0, 26.0, PcbPinKind.power), pcb_pin("U10", 4, "GND", "fzc-gnd", 116.0, 22.0, PcbPinKind.ground), pcb_pin("J1", 1, "48V_A", "fzc-48v", 136.0, 104.0, PcbPinKind.power), pcb_pin("J1", 2, "48V_B", "fzc-48v", 136.0, 100.0, PcbPinKind.power), pcb_pin("J1", 3, "GND_A", "fzc-gnd", 136.0, 96.0, PcbPinKind.ground), pcb_pin("J1", 4, "GND_B", "fzc-gnd", 136.0, 92.0, PcbPinKind.ground), pcb_pin("J1", 5, "CAN0_H", "fzc-can0-h", 136.0, 88.0, PcbPinKind.signal), pcb_pin("J1", 6, "CAN0_L", "fzc-can0-l", 136.0, 84.0, PcbPinKind.signal), pcb_pin("J1", 7, "CAN1_H", "fzc-can1-h", 136.0, 80.0, PcbPinKind.signal), pcb_pin("J1", 8, "CAN1_L", "fzc-can1-l", 136.0, 76.0, PcbPinKind.signal), pcb_pin("J1", 9, "ETH_P", "fzc-cable-eth-p", 136.0, 72.0, PcbPinKind.signal), pcb_pin("J1", 10, "ETH_N", "fzc-cable-eth-n", 136.0, 68.0, PcbPinKind.signal), pcb_pin("J2", 1, "OUT1", "fzc-out1", 136.0, 46.0, PcbPinKind.power), pcb_pin("J2", 2, "OUT2", "fzc-out2", 136.0, 42.0, PcbPinKind.power), pcb_pin("J2", 3, "OUT3", "fzc-out3", 136.0, 38.0, PcbPinKind.power), pcb_pin("J2", 4, "RTN1", "fzc-gnd", 136.0, 34.0, PcbPinKind.ground), pcb_pin("J2", 5, "RTN2", "fzc-gnd", 136.0, 30.0, PcbPinKind.ground), pcb_pin("J2", 6, "RTN3", "fzc-gnd", 136.0, 26.0, PcbPinKind.ground), pcb_pin("J2", 7, "RTN4", "fzc-gnd", 136.0, 22.0, PcbPinKind.ground), ]
def front_zone_12v_load_a(): return FZC_OUT1_A + FZC_OUT2_A + FZC_OUT3_A + front_zone_5v_load_a()
def front_zone_5v_load_a(): return FZC_CAN_SUPPLY_A + FZC_CAN_SUPPLY_A + front_zone_3v3_load_a()
def front_zone_3v3_load_a(): return FZC_MCU_A + FZC_PHY_A
def front_zone_48v_load_a(): return 12.0 * front_zone_12v_load_a() / (48.0 * BUCK_EFFICIENCY)
def front_zone_nets(): return [ PcbNet(id="fzc-48v", name="48 V zonal supply input", net_class=PcbNetClass.power, pin_ids=["J1.1", "J1.2", "C1.1", "U2.1", "Q1.1"], nominal_voltage_v=48.0, current_a=front_zone_48v_load_a(), is_reference=false), PcbNet(id="fzc-sw", name="Buck switch node", net_class=PcbNetClass.power, pin_ids=["Q1.3", "Q2.1", "U2.3", "L1.1"], nominal_voltage_v=12.0, current_a=front_zone_12v_load_a(), is_reference=false), PcbNet(id="fzc-gate-hs", name="High-side gate drive", net_class=PcbNetClass.signal, pin_ids=["U2.2", "Q1.2"], nominal_voltage_v=12.0, current_a=0.012, is_reference=false), PcbNet(id="fzc-gate-ls", name="Low-side gate drive", net_class=PcbNetClass.signal, pin_ids=["U2.4", "Q2.2"], nominal_voltage_v=12.0, current_a=0.012, is_reference=false), PcbNet(id="fzc-12v", name="12 V distribution rail", net_class=PcbNetClass.power, pin_ids=["L1.2", "C2.1", "U8.1", "U9.1", "U10.1", "U6.1"], nominal_voltage_v=12.0, current_a=front_zone_12v_load_a(), is_reference=false), PcbNet(id="fzc-5v", name="5 V transceiver rail", net_class=PcbNetClass.power, pin_ids=["U6.3", "C3.1", "U7.1", "U7.4", "U3.3", "U4.3"], nominal_voltage_v=5.0, current_a=front_zone_5v_load_a(), is_reference=false), PcbNet(id="fzc-3v3", name="3V3 logic rail", net_class=PcbNetClass.power, pin_ids=["U7.3", "C4.1", "U1.1", "U5.1"], nominal_voltage_v=3.3, current_a=front_zone_3v3_load_a(), is_reference=false), PcbNet(id="fzc-gnd", name="Zone controller ground", net_class=PcbNetClass.ground, pin_ids=["J1.3", "J1.4", "C1.2", "U2.5", "Q2.3", "C2.2", "U6.2", "C3.2", "U7.2", "C4.2", "U1.2", "U3.2", "U4.2", "U5.2", "U8.4", "U9.4", "U10.4", "J2.4", "J2.5", "J2.6", "J2.7"], nominal_voltage_v=0.0, current_a=front_zone_12v_load_a(), is_reference=true), PcbNet(id="fzc-can0-tx", name="CAN0 transmit", net_class=PcbNetClass.signal, pin_ids=["U1.3", "U3.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-can0-rx", name="CAN0 receive", net_class=PcbNetClass.signal, pin_ids=["U3.4", "U1.4"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-can1-tx", name="CAN1 transmit", net_class=PcbNetClass.signal, pin_ids=["U1.5", "U4.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-can1-rx", name="CAN1 receive", net_class=PcbNetClass.signal, pin_ids=["U4.4", "U1.6"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-can0-h", name="CAN0 bus high", net_class=PcbNetClass.can, pin_ids=["U3.5", "R1.1", "J1.5"], nominal_voltage_v=3.5, current_a=0.0333, is_reference=false), PcbNet(id="fzc-can0-l", name="CAN0 bus low", net_class=PcbNetClass.can, pin_ids=["U3.6", "R1.2", "J1.6"], nominal_voltage_v=1.5, current_a=0.0333, is_reference=false), PcbNet(id="fzc-can1-h", name="CAN1 bus high", net_class=PcbNetClass.can, pin_ids=["U4.5", "R2.1", "J1.7"], nominal_voltage_v=3.5, current_a=0.0333, is_reference=false), PcbNet(id="fzc-can1-l", name="CAN1 bus low", net_class=PcbNetClass.can, pin_ids=["U4.6", "R2.2", "J1.8"], nominal_voltage_v=1.5, current_a=0.0333, is_reference=false), PcbNet(id="fzc-phy-txd", name="PHY transmit data", net_class=PcbNetClass.signal, pin_ids=["U1.7", "U5.5"], nominal_voltage_v=3.3, current_a=0.0005, is_reference=false), PcbNet(id="fzc-phy-rxd", name="PHY receive data", net_class=PcbNetClass.signal, pin_ids=["U5.6", "U1.8"], nominal_voltage_v=3.3, current_a=0.0005, is_reference=false), PcbNet(id="fzc-eth-p", name="100BASE-T1 pair positive", net_class=PcbNetClass.ethernet, pin_ids=["U5.3", "T1.1"], nominal_voltage_v=1.1, current_a=0.011, is_reference=false), PcbNet(id="fzc-eth-n", name="100BASE-T1 pair negative", net_class=PcbNetClass.ethernet, pin_ids=["U5.4", "T1.2"], nominal_voltage_v=0.0, current_a=0.011, is_reference=false), PcbNet(id="fzc-cable-eth-p", name="Ethernet cable pair positive", net_class=PcbNetClass.ethernet, pin_ids=["T1.3", "J1.9"], nominal_voltage_v=1.1, current_a=0.011, is_reference=false), PcbNet(id="fzc-cable-eth-n", name="Ethernet cable pair negative", net_class=PcbNetClass.ethernet, pin_ids=["T1.4", "J1.10"], nominal_voltage_v=0.0, current_a=0.011, is_reference=false), PcbNet(id="fzc-hs1-in", name="eFuse channel 1 command", net_class=PcbNetClass.signal, pin_ids=["U1.9", "U8.2"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-hs2-in", name="eFuse channel 2 command", net_class=PcbNetClass.signal, pin_ids=["U1.11", "U9.2"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-hs3-in", name="eFuse channel 3 command", net_class=PcbNetClass.signal, pin_ids=["U1.12", "U10.2"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="fzc-hs-sense", name="eFuse current-sense feedback", net_class=PcbNetClass.analog, pin_ids=["U8.5", "U1.10"], nominal_voltage_v=1.5, current_a=0.0012, is_reference=false), PcbNet(id="fzc-out1", name="Switched output 1, low-beam lamp", net_class=PcbNetClass.power, pin_ids=["U8.3", "J2.1"], nominal_voltage_v=12.0, current_a=FZC_OUT1_A, is_reference=false), PcbNet(id="fzc-out2", name="Switched output 2, washer heater", net_class=PcbNetClass.power, pin_ids=["U9.3", "J2.2"], nominal_voltage_v=12.0, current_a=FZC_OUT2_A, is_reference=false), PcbNet(id="fzc-out3", name="Switched output 3, grille shutter actuator", net_class=PcbNetClass.power, pin_ids=["U10.3", "J2.3"], nominal_voltage_v=12.0, current_a=FZC_OUT3_A, is_reference=false), ]
def front_zone_plans(): return [ PcbRoutePlan(net_id="power", layer_id="fzc-l1", width_mm=2.5, topology="chain", via_count=2, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l5"), PcbRoutePlan(net_id="ground", layer_id="fzc-l2", width_mm=6.0, topology="star", via_count=2, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="signal", layer_id="fzc-l4", width_mm=0.25, topology="chain", via_count=1, via_drill_mm=0.25, via_pad_mm=0.5, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="analog", layer_id="fzc-l4", width_mm=0.3, topology="chain", via_count=1, via_drill_mm=0.25, via_pad_mm=0.5, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="can", layer_id="fzc-l1", width_mm=0.35, topology="chain", via_count=0, via_drill_mm=0.25, via_pad_mm=0.5, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="ethernet", layer_id="fzc-l1", width_mm=0.28, topology="chain", via_count=0, via_drill_mm=0.25, via_pad_mm=0.5, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="hv", layer_id="fzc-l1", width_mm=3.0, topology="chain", via_count=2, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l5"), ]
def front_zone_overrides(): return [ PcbRoutePlan(net_id="fzc-sw", layer_id="fzc-l1", width_mm=5.0, topology="chain", via_count=0, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="fzc-12v", layer_id="fzc-l1", width_mm=4.0, topology="chain", via_count=0, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="fzc-48v", layer_id="fzc-l1", width_mm=2.5, topology="chain", via_count=0, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="fzc-5v", layer_id="fzc-l1", width_mm=1.2, topology="chain", via_count=0, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="fzc-3v3", layer_id="fzc-l1", width_mm=1.2, topology="chain", via_count=0, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), # Channel 1 is routed on an inner layer where IPC-2221 halves the sizing # constant; 2.35 mm is the deliberate marginal case the DRC has to catch. PcbRoutePlan(net_id="fzc-out1", layer_id="fzc-l3", width_mm=2.35, topology="chain", via_count=4, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="fzc-out2", layer_id="fzc-l3", width_mm=2.6, topology="chain", via_count=4, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), PcbRoutePlan(net_id="fzc-out3", layer_id="fzc-l3", width_mm=2.2, topology="chain", via_count=4, via_drill_mm=0.35, via_pad_mm=0.7, via_layer_id="fzc-l1"), ]
def front_zone_loads(): board = "front-zone-controller" return [ pcb_load(board, "Q1.1", "Q1.3", front_zone_48v_load_a(), "buck high-side switch, average conduction"), pcb_load(board, "Q2.3", "Q2.1", front_zone_12v_load_a() - front_zone_48v_load_a(), "buck low-side switch, average freewheel"), pcb_load(board, "L1.1", "L1.2", front_zone_12v_load_a(), "buck output inductor"), pcb_load(board, "U6.1", "U6.3", front_zone_5v_load_a(), "12 V to 5 V regulator pass element"), pcb_load(board, "U7.1", "U7.3", front_zone_3v3_load_a(), "5 V to 3V3 regulator pass element"), pcb_load(board, "U1.1", "U1.2", FZC_MCU_A, "MCU core and I/O supply"), pcb_load(board, "U5.1", "U5.2", FZC_PHY_A, "Ethernet PHY supply"), pcb_load(board, "U3.3", "U3.2", FZC_CAN_SUPPLY_A, "CAN0 transceiver supply"), pcb_load(board, "U4.3", "U4.2", FZC_CAN_SUPPLY_A, "CAN1 transceiver supply"), pcb_load(board, "U8.1", "U8.3", FZC_OUT1_A, "eFuse channel 1 pass element"), pcb_load(board, "U9.1", "U9.3", FZC_OUT2_A, "eFuse channel 2 pass element"), pcb_load(board, "U10.1", "U10.3", FZC_OUT3_A, "eFuse channel 3 pass element"), pcb_load(board, "J2.1", "J2.4", FZC_OUT1_A, "low-beam lamp module"), pcb_load(board, "J2.2", "J2.5", FZC_OUT2_A, "washer nozzle heater"), pcb_load(board, "J2.3", "J2.6", FZC_OUT3_A, "grille shutter actuator"), pcb_load(board, "R1.1", "R1.2", 0.0333, "CAN0 split termination, dominant state"), pcb_load(board, "R2.1", "R2.2", 0.0333, "CAN1 split termination, dominant state"), pcb_load(board, "T1.1", "T1.2", 0.011, "100BASE-T1 magnetics primary"), pcb_load(board, "J1.9", "J1.10", 0.011, "100BASE-T1 cable pair"), pcb_load(board, "Q1.2", "Q1.3", 0.012, "high-side gate charge, averaged"), pcb_load(board, "Q2.2", "Q2.3", 0.012, "low-side gate charge, averaged"), pcb_load(board, "U3.1", "U3.2", 0.00025, "CAN0 transmit line switching current"), pcb_load(board, "U1.4", "U1.2", 0.00025, "CAN0 receive line switching current"), pcb_load(board, "U4.1", "U4.2", 0.00025, "CAN1 transmit line switching current"), pcb_load(board, "U1.6", "U1.2", 0.00025, "CAN1 receive line switching current"), pcb_load(board, "U5.5", "U5.2", 0.0005, "MII transmit switching current"), pcb_load(board, "U1.8", "U1.2", 0.0005, "MII receive switching current"), pcb_load(board, "U8.2", "U8.4", 0.00025, "eFuse 1 command input"), pcb_load(board, "U9.2", "U9.4", 0.00025, "eFuse 2 command input"), pcb_load(board, "U10.2", "U10.4", 0.00025, "eFuse 3 command input"), pcb_load(board, "U1.10", "U1.2", 0.0012, "current-sense feedback into the ADC"), ]
def front_zone_board() !{}: return assemble_board("front-zone-controller", "Front zone controller board", "front-zone-controller", "front-left plenum, behind the wheel-arch liner", "FR-4 Tg170, 6 copper layers, dedicated ground plane", front_zone_stackup(), front_zone_components(), front_zone_pins(), front_zone_nets(), front_zone_plans(), front_zone_overrides(), front_zone_loads(), 145.0, 110.0)
def hv_junction_stackup(): layers = [ PcbLayer(id="hvj-bus", name="Bonded HV busbar", function=PcbLayerFunction.signal, copper_um=2000.0, dielectric_mm=0.0, dielectric_er=1.0, material="2.0 mm laminated copper busbar bonded to top copper"), PcbLayer(id="hvj-smt", name="Top soldermask", function=PcbLayerFunction.soldermask, copper_um=0.0, dielectric_mm=0.0254, dielectric_er=3.6, material="high-CTI LPI soldermask"), PcbLayer(id="hvj-l1", name="Top control and HV sense", function=PcbLayerFunction.signal, copper_um=105.0, dielectric_mm=0.0, dielectric_er=1.0, material="heavy copper 3 oz"), PcbLayer(id="hvj-d1", name="Interlayer dielectric", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.1016, dielectric_er=4.6, material="high-Tg thermal prepreg"), PcbLayer(id="hvj-l2", name="LV reference plane", function=PcbLayerFunction.plane, copper_um=105.0, dielectric_mm=0.0, dielectric_er=1.0, material="heavy copper 3 oz"), PcbLayer(id="hvj-d2", name="Thermally conductive dielectric", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.15, dielectric_er=8.0, material="ceramic-filled thermal dielectric"), PcbLayer(id="hvj-base", name="Aluminium base plate", function=PcbLayerFunction.core, copper_um=0.0, dielectric_mm=1.5, dielectric_er=1.0, material="5052 aluminium base plate"), ] return PcbStackup(id="stk-hvj", name="IMS heavy-copper 2-layer with bonded 2 mm busbars", layers=layers, total_thickness_mm=stackup_thickness_mm(layers), ipc_class=3, impedance_target_ohm=100.0)
def hv_junction_components(): return [ PcbComponent(refdes="TB1", part_number="BUSBAR-M8-2P", description="HV pack input terminal block", footprint="BUSBAR-M8-2P", pin_count=2, x_mm=172.0, y_mm=60.0, rotation_deg=0.0, power_dissipation_w=0.4, mass_g=48.0), PcbComponent(refdes="TB2", part_number="BUSBAR-M8-2P", description="HV output terminal block to the drive units", footprint="BUSBAR-M8-2P", pin_count=2, x_mm=52.0, y_mm=60.0, rotation_deg=0.0, power_dissipation_w=0.4, mass_g=48.0), PcbComponent(refdes="K1", part_number="GV12-800-1A", description="Main positive HV contactor, 800 V DC", footprint="CONTACTOR-GV12", pin_count=4, x_mm=140.0, y_mm=100.0, rotation_deg=0.0, power_dissipation_w=5.4, mass_g=340.0), PcbComponent(refdes="K2", part_number="GV12-800-1A", description="Main negative HV contactor, 800 V DC", footprint="CONTACTOR-GV12", pin_count=4, x_mm=140.0, y_mm=20.0, rotation_deg=0.0, power_dissipation_w=5.4, mass_g=340.0), PcbComponent(refdes="F1", part_number="PYROFUSE-PF800", description="Pyrotechnic disconnect in the positive rail", footprint="PYRO-M8", pin_count=4, x_mm=90.0, y_mm=100.0, rotation_deg=0.0, power_dissipation_w=2.1, mass_g=180.0), PcbComponent(refdes="RS1", part_number="FL-100U-800", description="100 uOhm coaxial current shunt in the negative rail", footprint="SHUNT-4T", pin_count=4, x_mm=90.0, y_mm=20.0, rotation_deg=0.0, power_dissipation_w=2.56, mass_g=95.0), PcbComponent(refdes="U1", part_number="AMC1311-Q1", description="Reinforced isolation amplifier for shunt current sense", footprint="SOIC-8W", pin_count=8, x_mm=30.0, y_mm=20.0, rotation_deg=0.0, power_dissipation_w=0.05, mass_g=0.08), PcbComponent(refdes="U2", part_number="ISO224-Q1", description="Reinforced isolation amplifier, insulation-monitor front end", footprint="SOIC-8W", pin_count=8, x_mm=30.0, y_mm=60.0, rotation_deg=0.0, power_dissipation_w=0.05, mass_g=0.08), PcbComponent(refdes="T1", part_number="750313638", description="Isolated supply transformer for the HV-referenced side", footprint="SM-4P", pin_count=4, x_mm=30.0, y_mm=42.0, rotation_deg=0.0, power_dissipation_w=0.09, mass_g=1.8), PcbComponent(refdes="U3", part_number="TPS7B6950-Q1", description="12 V to 5 V low-dropout regulator", footprint="DPAK", pin_count=3, x_mm=12.0, y_mm=82.0, rotation_deg=0.0, power_dissipation_w=0.15, mass_g=0.4), PcbComponent(refdes="Q1", part_number="IPB017N10N5", description="Contactor 1 low-side coil driver", footprint="D2PAK", pin_count=3, x_mm=12.0, y_mm=92.0, rotation_deg=0.0, power_dissipation_w=0.26, mass_g=1.6), PcbComponent(refdes="Q2", part_number="IPB017N10N5", description="Contactor 2 low-side coil driver", footprint="D2PAK", pin_count=3, x_mm=12.0, y_mm=102.0, rotation_deg=0.0, power_dissipation_w=0.26, mass_g=1.6), PcbComponent(refdes="Q3", part_number="IAUC100N04S6L", description="Pyrotechnic squib trigger driver", footprint="DPAK", pin_count=3, x_mm=12.0, y_mm=112.0, rotation_deg=0.0, power_dissipation_w=0.02, mass_g=0.4), PcbComponent(refdes="R1", part_number="HVD-2M2-4S", description="Insulation-monitor divider, four series HV elements", footprint="RES-HV-2512x4", pin_count=2, x_mm=60.0, y_mm=74.0, rotation_deg=0.0, power_dissipation_w=0.27, mass_g=0.12), PcbComponent(refdes="R2", part_number="CRCW2512-100K", description="Insulation-monitor divider low-side element", footprint="2512", pin_count=2, x_mm=57.0, y_mm=66.0, rotation_deg=0.0, power_dissipation_w=0.01, mass_g=0.03), PcbComponent(refdes="J1", part_number="MX150-10", description="LV control and diagnostics header", footprint="MOLEX-MX150-10", pin_count=10, x_mm=6.0, y_mm=35.0, rotation_deg=270.0, power_dissipation_w=0.0, mass_g=8.0), PcbComponent(refdes="J2", part_number="HVIL-2", description="High-voltage interlock loop connector", footprint="HVIL-2", pin_count=2, x_mm=6.0, y_mm=77.0, rotation_deg=270.0, power_dissipation_w=0.0, mass_g=2.4), ]
def hv_junction_pins(): return [ pcb_pin("TB1", 1, "PACK+", "hvj-hv-pack-p", 172.0, 100.0, PcbPinKind.power), pcb_pin("TB1", 2, "PACK-", "hvj-hv-pack-n", 172.0, 20.0, PcbPinKind.power), pcb_pin("K1", 1, "COIL+", "hvj-12v", 132.0, 116.0, PcbPinKind.power), pcb_pin("K1", 2, "COIL-", "hvj-coil1-lo", 120.0, 116.0, PcbPinKind.power), pcb_pin("K1", 3, "HV_IN", "hvj-hv-pack-p", 156.0, 100.0, PcbPinKind.power), pcb_pin("K1", 4, "HV_OUT", "hvj-hv-bus-p", 124.0, 100.0, PcbPinKind.power), pcb_pin("K2", 1, "COIL+", "hvj-12v", 132.0, 4.0, PcbPinKind.power), pcb_pin("K2", 2, "COIL-", "hvj-coil2-lo", 120.0, 4.0, PcbPinKind.power), pcb_pin("K2", 3, "HV_IN", "hvj-hv-pack-n", 156.0, 20.0, PcbPinKind.power), pcb_pin("K2", 4, "HV_OUT", "hvj-hv-bus-n", 124.0, 20.0, PcbPinKind.power), pcb_pin("F1", 1, "HV_IN", "hvj-hv-bus-p", 108.0, 100.0, PcbPinKind.power), pcb_pin("F1", 2, "HV_OUT", "hvj-hv-out-p", 72.0, 100.0, PcbPinKind.power), pcb_pin("F1", 3, "TRIG+", "hvj-12v", 90.0, 116.0, PcbPinKind.power), pcb_pin("F1", 4, "TRIG-", "hvj-pyro-lo", 78.0, 116.0, PcbPinKind.power), pcb_pin("RS1", 1, "HV_IN", "hvj-hv-bus-n", 108.0, 20.0, PcbPinKind.power), pcb_pin("RS1", 2, "HV_OUT", "hvj-hv-out-n", 72.0, 20.0, PcbPinKind.power), pcb_pin("RS1", 3, "SENSE+", "hvj-shunt-p", 99.0, 9.0, PcbPinKind.analog), pcb_pin("RS1", 4, "SENSE-", "hvj-shunt-n", 81.0, 9.0, PcbPinKind.analog), pcb_pin("TB2", 1, "OUT+", "hvj-hv-out-p", 52.0, 100.0, PcbPinKind.power), pcb_pin("TB2", 2, "OUT-", "hvj-hv-out-n", 52.0, 20.0, PcbPinKind.power), pcb_pin("R1", 1, "A", "hvj-hv-out-p", 66.0, 74.0, PcbPinKind.analog), pcb_pin("R1", 2, "B", "hvj-imd-node", 54.0, 74.0, PcbPinKind.analog), pcb_pin("R2", 1, "A", "hvj-imd-node", 60.0, 66.0, PcbPinKind.analog), pcb_pin("R2", 2, "B", "hvj-iso-gnd", 54.0, 66.0, PcbPinKind.ground), pcb_pin("U1", 1, "VDD1", "hvj-iso-5v", 38.0, 28.0, PcbPinKind.power), pcb_pin("U1", 2, "GND1", "hvj-iso-gnd", 38.0, 24.0, PcbPinKind.ground), pcb_pin("U1", 3, "INP", "hvj-shunt-p", 38.0, 20.0, PcbPinKind.analog), pcb_pin("U1", 4, "INN", "hvj-shunt-n", 38.0, 16.0, PcbPinKind.analog), pcb_pin("U1", 5, "VDD2", "hvj-5v", 22.0, 22.0, PcbPinKind.power), pcb_pin("U1", 6, "OUTP", "hvj-isense-out", 22.0, 18.0, PcbPinKind.analog), pcb_pin("U1", 7, "GND2", "hvj-gnd", 22.0, 14.0, PcbPinKind.ground), pcb_pin("U2", 1, "VDD1", "hvj-iso-5v", 38.0, 66.0, PcbPinKind.power), pcb_pin("U2", 2, "GND1", "hvj-iso-gnd", 38.0, 62.0, PcbPinKind.ground), pcb_pin("U2", 3, "VIN", "hvj-imd-node", 38.0, 58.0, PcbPinKind.analog), pcb_pin("U2", 4, "VDD2", "hvj-5v", 22.0, 64.0, PcbPinKind.power), pcb_pin("U2", 5, "GND2", "hvj-gnd", 22.0, 60.0, PcbPinKind.ground), pcb_pin("U2", 6, "OUT", "hvj-imd-out", 22.0, 56.0, PcbPinKind.analog), pcb_pin("T1", 1, "P1", "hvj-5v", 21.0, 46.0, PcbPinKind.power), pcb_pin("T1", 2, "P2", "hvj-gnd", 21.0, 38.0, PcbPinKind.ground), pcb_pin("T1", 3, "S1", "hvj-iso-5v", 38.0, 46.0, PcbPinKind.power), pcb_pin("T1", 4, "S2", "hvj-iso-gnd", 38.0, 38.0, PcbPinKind.ground), pcb_pin("U3", 1, "IN", "hvj-12v", 9.0, 82.0, PcbPinKind.power), pcb_pin("U3", 2, "GND", "hvj-gnd", 12.0, 85.0, PcbPinKind.ground), pcb_pin("U3", 3, "OUT", "hvj-5v", 15.0, 82.0, PcbPinKind.power), pcb_pin("Q1", 1, "G", "hvj-coil1-drv", 9.0, 92.0, PcbPinKind.signal), pcb_pin("Q1", 2, "D", "hvj-coil1-lo", 15.0, 95.0, PcbPinKind.power), pcb_pin("Q1", 3, "S", "hvj-gnd", 15.0, 89.0, PcbPinKind.ground), pcb_pin("Q2", 1, "G", "hvj-coil2-drv", 9.0, 102.0, PcbPinKind.signal), pcb_pin("Q2", 2, "D", "hvj-coil2-lo", 15.0, 105.0, PcbPinKind.power), pcb_pin("Q2", 3, "S", "hvj-gnd", 15.0, 99.0, PcbPinKind.ground), pcb_pin("Q3", 1, "G", "hvj-pyro-drv", 9.0, 112.0, PcbPinKind.signal), pcb_pin("Q3", 2, "D", "hvj-pyro-lo", 15.0, 115.0, PcbPinKind.power), pcb_pin("Q3", 3, "S", "hvj-gnd", 15.0, 109.0, PcbPinKind.ground), pcb_pin("J1", 1, "12V", "hvj-12v", 3.0, 8.0, PcbPinKind.power), pcb_pin("J1", 2, "GND", "hvj-gnd", 3.0, 14.0, PcbPinKind.ground), pcb_pin("J1", 3, "COIL1_DRV", "hvj-coil1-drv", 3.0, 20.0, PcbPinKind.signal), pcb_pin("J1", 4, "COIL2_DRV", "hvj-coil2-drv", 3.0, 26.0, PcbPinKind.signal), pcb_pin("J1", 5, "PYRO_DRV", "hvj-pyro-drv", 3.0, 32.0, PcbPinKind.signal), pcb_pin("J1", 6, "ISENSE", "hvj-isense-out", 3.0, 38.0, PcbPinKind.analog), pcb_pin("J1", 7, "IMD", "hvj-imd-out", 3.0, 44.0, PcbPinKind.analog), pcb_pin("J1", 8, "HVIL_OUT", "hvj-hvil-out", 3.0, 50.0, PcbPinKind.signal), pcb_pin("J1", 9, "HVIL_IN", "hvj-hvil-in", 3.0, 56.0, PcbPinKind.signal), pcb_pin("J1", 10, "GND_RTN", "hvj-gnd", 3.0, 62.0, PcbPinKind.ground), pcb_pin("J2", 1, "LOOP_OUT", "hvj-hvil-out", 3.0, 74.0, PcbPinKind.signal), pcb_pin("J2", 2, "LOOP_IN", "hvj-hvil-in", 3.0, 80.0, PcbPinKind.signal), ]
def hv_junction_12v_load_a(): return 2.0 * HVJ_COIL_A + HVJ_PYRO_MONITOR_A + hv_junction_5v_load_a()
def hv_junction_5v_load_a(): return HVJ_ISO_SUPPLY_A + HVJ_ISENSE_A + HVJ_IMD_A
def hv_junction_nets(): return [ PcbNet(id="hvj-hv-pack-p", name="Pack positive to contactor", net_class=PcbNetClass.hv, pin_ids=["TB1.1", "K1.3"], nominal_voltage_v=800.0, current_a=HV_BUS_CURRENT_A, is_reference=false), PcbNet(id="hvj-hv-bus-p", name="Contactor to pyrofuse positive bus", net_class=PcbNetClass.hv, pin_ids=["K1.4", "F1.1"], nominal_voltage_v=800.0, current_a=HV_BUS_CURRENT_A, is_reference=false), PcbNet(id="hvj-hv-out-p", name="Protected HV output positive", net_class=PcbNetClass.hv, pin_ids=["F1.2", "TB2.1", "R1.1"], nominal_voltage_v=800.0, current_a=HV_BUS_CURRENT_A, is_reference=false), PcbNet(id="hvj-hv-out-n", name="Protected HV output negative", net_class=PcbNetClass.hv, pin_ids=["TB2.2", "RS1.2"], nominal_voltage_v=0.0, current_a=HV_BUS_CURRENT_A, is_reference=false), PcbNet(id="hvj-hv-bus-n", name="Shunt to contactor negative bus", net_class=PcbNetClass.hv, pin_ids=["RS1.1", "K2.4"], nominal_voltage_v=0.0, current_a=HV_BUS_CURRENT_A, is_reference=false), PcbNet(id="hvj-hv-pack-n", name="Contactor to pack negative", net_class=PcbNetClass.hv, pin_ids=["K2.3", "TB1.2"], nominal_voltage_v=0.0, current_a=HV_BUS_CURRENT_A, is_reference=false), PcbNet(id="hvj-shunt-p", name="Shunt sense positive", net_class=PcbNetClass.analog, pin_ids=["RS1.3", "U1.3"], nominal_voltage_v=0.0, current_a=0.0, is_reference=false), PcbNet(id="hvj-shunt-n", name="Shunt sense negative", net_class=PcbNetClass.analog, pin_ids=["RS1.4", "U1.4"], nominal_voltage_v=0.0, current_a=0.0, is_reference=false), PcbNet(id="hvj-imd-node", name="Insulation-monitor divider midpoint", net_class=PcbNetClass.analog, pin_ids=["R1.2", "R2.1", "U2.3"], nominal_voltage_v=34.8, current_a=0.00035, is_reference=false), PcbNet(id="hvj-iso-5v", name="HV-referenced isolated 5 V rail", net_class=PcbNetClass.power, pin_ids=["T1.3", "U1.1", "U2.1"], nominal_voltage_v=5.0, current_a=0.0075, is_reference=false), PcbNet(id="hvj-iso-gnd", name="HV-referenced isolated return", net_class=PcbNetClass.ground, pin_ids=["T1.4", "U1.2", "U2.2", "R2.2"], nominal_voltage_v=0.0, current_a=0.0079, is_reference=false), PcbNet(id="hvj-5v", name="LV 5 V rail", net_class=PcbNetClass.power, pin_ids=["U3.3", "T1.1", "U1.5", "U2.4"], nominal_voltage_v=5.0, current_a=hv_junction_5v_load_a(), is_reference=false), PcbNet(id="hvj-12v", name="LV 12 V supply and contactor coil feed", net_class=PcbNetClass.power, pin_ids=["J1.1", "U3.1", "K1.1", "K2.1", "F1.3"], nominal_voltage_v=12.0, current_a=hv_junction_12v_load_a(), is_reference=false), PcbNet(id="hvj-gnd", name="LV chassis-referenced ground", net_class=PcbNetClass.ground, pin_ids=["J1.2", "J1.10", "U3.2", "Q1.3", "Q2.3", "Q3.3", "T1.2", "U1.7", "U2.5"], nominal_voltage_v=0.0, current_a=hv_junction_12v_load_a(), is_reference=true), PcbNet(id="hvj-coil1-lo", name="Contactor 1 coil low side", net_class=PcbNetClass.power, pin_ids=["K1.2", "Q1.2"], nominal_voltage_v=0.6, current_a=HVJ_COIL_A, is_reference=false), PcbNet(id="hvj-coil2-lo", name="Contactor 2 coil low side", net_class=PcbNetClass.power, pin_ids=["K2.2", "Q2.2"], nominal_voltage_v=0.6, current_a=HVJ_COIL_A, is_reference=false), PcbNet(id="hvj-pyro-lo", name="Pyrotechnic squib low side", net_class=PcbNetClass.power, pin_ids=["F1.4", "Q3.2"], nominal_voltage_v=0.2, current_a=HVJ_PYRO_MONITOR_A, is_reference=false), PcbNet(id="hvj-coil1-drv", name="Contactor 1 gate command", net_class=PcbNetClass.signal, pin_ids=["J1.3", "Q1.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="hvj-coil2-drv", name="Contactor 2 gate command", net_class=PcbNetClass.signal, pin_ids=["J1.4", "Q2.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="hvj-pyro-drv", name="Pyrotechnic trigger command", net_class=PcbNetClass.signal, pin_ids=["J1.5", "Q3.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="hvj-isense-out", name="Isolated current-sense output", net_class=PcbNetClass.analog, pin_ids=["U1.6", "J1.6"], nominal_voltage_v=2.5, current_a=0.0002, is_reference=false), PcbNet(id="hvj-imd-out", name="Insulation-monitor output", net_class=PcbNetClass.analog, pin_ids=["U2.6", "J1.7"], nominal_voltage_v=2.5, current_a=0.0002, is_reference=false), PcbNet(id="hvj-hvil-out", name="Interlock loop drive", net_class=PcbNetClass.signal, pin_ids=["J1.8", "J2.1"], nominal_voltage_v=5.0, current_a=0.008, is_reference=false), PcbNet(id="hvj-hvil-in", name="Interlock loop return", net_class=PcbNetClass.signal, pin_ids=["J2.2", "J1.9"], nominal_voltage_v=5.0, current_a=0.008, is_reference=false), ]
def hv_junction_plans(): return [ PcbRoutePlan(net_id="hv", layer_id="hvj-bus", width_mm=16.0, topology="chain", via_count=0, via_drill_mm=0.5, via_pad_mm=1.0, via_layer_id="hvj-bus"), PcbRoutePlan(net_id="power", layer_id="hvj-l1", width_mm=1.5, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="ground", layer_id="hvj-l2", width_mm=6.0, topology="star", via_count=2, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="signal", layer_id="hvj-l1", width_mm=0.5, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="analog", layer_id="hvj-l1", width_mm=0.5, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="can", layer_id="hvj-l1", width_mm=0.4, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="ethernet", layer_id="hvj-l1", width_mm=0.4, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), ]
def hv_junction_overrides(): return [ # The isolated return must stay off the chassis-referenced plane. PcbRoutePlan(net_id="hvj-iso-gnd", layer_id="hvj-l1", width_mm=2.0, topology="star", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="hvj-coil1-lo", layer_id="hvj-l1", width_mm=2.0, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="hvj-coil2-lo", layer_id="hvj-l1", width_mm=2.0, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), PcbRoutePlan(net_id="hvj-12v", layer_id="hvj-l1", width_mm=2.5, topology="chain", via_count=0, via_drill_mm=0.4, via_pad_mm=0.8, via_layer_id="hvj-l1"), ]
def hv_junction_loads(): board = "hv-junction-board" return [ pcb_load(board, "K1.3", "K1.4", HV_BUS_CURRENT_A, "positive contactor main pole"), pcb_load(board, "F1.1", "F1.2", HV_BUS_CURRENT_A, "pyrotechnic fuse conductor"), pcb_load(board, "TB2.1", "TB2.2", HV_BUS_CURRENT_A, "drive-unit HV load"), pcb_load(board, "RS1.2", "RS1.1", HV_BUS_CURRENT_A, "current shunt element"), pcb_load(board, "K2.4", "K2.3", HV_BUS_CURRENT_A, "negative contactor main pole"), pcb_load(board, "TB1.2", "TB1.1", HV_BUS_CURRENT_A, "traction pack source pair"), pcb_load(board, "R1.1", "R1.2", HVJ_IMD_DIVIDER_A, "insulation-monitor divider, HV element"), pcb_load(board, "R2.1", "R2.2", HVJ_IMD_DIVIDER_A, "insulation-monitor divider, low element"), pcb_load(board, "U3.1", "U3.3", hv_junction_5v_load_a(), "12 V to 5 V regulator pass element"), pcb_load(board, "T1.1", "T1.2", HVJ_ISO_SUPPLY_A, "isolated supply transformer primary"), pcb_load(board, "U1.1", "U1.2", 0.0035, "isolation amplifier HV-side supply"), pcb_load(board, "U2.1", "U2.2", 0.004, "insulation-monitor HV-side supply"), pcb_load(board, "U1.5", "U1.7", HVJ_ISENSE_A, "isolation amplifier LV-side supply"), pcb_load(board, "U2.4", "U2.5", HVJ_IMD_A, "insulation-monitor LV-side supply"), pcb_load(board, "K1.1", "K1.2", HVJ_COIL_A, "contactor 1 coil"), pcb_load(board, "Q1.2", "Q1.3", HVJ_COIL_A, "contactor 1 driver channel"), pcb_load(board, "K2.1", "K2.2", HVJ_COIL_A, "contactor 2 coil"), pcb_load(board, "Q2.2", "Q2.3", HVJ_COIL_A, "contactor 2 driver channel"), pcb_load(board, "F1.3", "F1.4", HVJ_PYRO_MONITOR_A, "squib continuity monitor current"), pcb_load(board, "Q3.2", "Q3.3", HVJ_PYRO_MONITOR_A, "squib driver channel"), pcb_load(board, "J1.6", "J1.10", 0.0002, "current-sense output into the zone controller"), pcb_load(board, "J1.7", "J1.10", 0.0002, "insulation-monitor output into the zone controller"), pcb_load(board, "J2.1", "J2.2", 0.008, "high-voltage interlock loop"), pcb_load(board, "Q1.1", "Q1.3", 0.00025, "contactor 1 gate command"), pcb_load(board, "Q2.1", "Q2.3", 0.00025, "contactor 2 gate command"), pcb_load(board, "Q3.1", "Q3.3", 0.00025, "pyrotechnic gate command"), ]
def hv_junction_board() !{}: return assemble_board("hv-junction-board", "HV junction, protection and isolation board", "hv-junction", "HV junction box above the pack front cross-member", "IMS aluminium-core, 105 um heavy copper, bonded 2 mm busbars", hv_junction_stackup(), hv_junction_components(), hv_junction_pins(), hv_junction_nets(), hv_junction_plans(), hv_junction_overrides(), hv_junction_loads(), 180.0, 120.0)
def sill_panel_stackup(): layers = [ PcbLayer(id="sill-top", name="Abrasion topcoat", function=PcbLayerFunction.soldermask, copper_um=0.0, dielectric_mm=0.05, dielectric_er=3.6, material="two-component abrasion-resistant topcoat"), PcbLayer(id="sill-shield", name="Overprinted shield", function=PcbLayerFunction.plane, copper_um=35.0, dielectric_mm=0.0, dielectric_er=1.0, material="screen-printed silver-copper shield ink"), PcbLayer(id="sill-cov", name="Printed coverlay", function=PcbLayerFunction.coverlay, copper_um=0.0, dielectric_mm=0.2, dielectric_er=3.4, material="printed polymer coverlay"), PcbLayer(id="sill-data", name="Printed data conductors", function=PcbLayerFunction.signal, copper_um=70.0, dielectric_mm=0.0, dielectric_er=1.0, material="printed and sintered copper paste"), PcbLayer(id="sill-iso", name="Interlayer dielectric", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.15, dielectric_er=4.2, material="printed interlayer dielectric"), PcbLayer(id="sill-pwr", name="Printed power conductors", function=PcbLayerFunction.signal, copper_um=400.0, dielectric_mm=0.0, dielectric_er=1.0, material="thick screen-printed and sintered copper"), PcbLayer(id="sill-diel", name="Dielectric barrier", function=PcbLayerFunction.prepreg, copper_um=0.0, dielectric_mm=0.25, dielectric_er=4.8, material="ceramic-filled epoxy dielectric barrier"), PcbLayer(id="sill-base", name="Structural sill extrusion", function=PcbLayerFunction.core, copper_um=0.0, dielectric_mm=3.5, dielectric_er=1.0, material="anodised 6082-T6 aluminium sill extrusion"), ] return PcbStackup(id="stk-sill", name="Structural printed conductor panel on the aluminium rocker", layers=layers, total_thickness_mm=stackup_thickness_mm(layers), ipc_class=3, impedance_target_ohm=100.0)
def sill_panel_components(): return [ PcbComponent(refdes="TB2", part_number="BONDPAD-M8-4P", description="Rear bond terminal block, pack and junction end", footprint="BONDPAD-M8x4", pin_count=4, x_mm=2560.0, y_mm=98.0, rotation_deg=0.0, power_dissipation_w=0.6, mass_g=96.0), PcbComponent(refdes="TB1", part_number="BONDPAD-M8-4P", description="Front bond terminal block, drive-unit end", footprint="BONDPAD-M8x4", pin_count=4, x_mm=40.0, y_mm=98.0, rotation_deg=0.0, power_dissipation_w=0.6, mass_g=96.0), PcbComponent(refdes="J2", part_number="MX150-6", description="Rear data and service connector", footprint="MOLEX-MX150-6", pin_count=6, x_mm=2480.0, y_mm=60.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=5.0), PcbComponent(refdes="J1", part_number="MX150-6", description="Front data and service connector", footprint="MOLEX-MX150-6", pin_count=6, x_mm=120.0, y_mm=60.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=5.0), PcbComponent(refdes="J3", part_number="MX150-4", description="B-pillar branch tap", footprint="MOLEX-MX150-4", pin_count=4, x_mm=1300.0, y_mm=40.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=3.4), PcbComponent(refdes="FS1", part_number="PRINTLINK-200A", description="Printed fusible link in the 48 V branch", footprint="PRINTLINK-60x25", pin_count=2, x_mm=1400.0, y_mm=62.0, rotation_deg=0.0, power_dissipation_w=1.9, mass_g=2.2), PcbComponent(refdes="TP1", part_number="NTC-PRINTED-10K", description="Printed thermistor, front conductor run", footprint="PRINTNTC-6x4", pin_count=2, x_mm=900.0, y_mm=25.0, rotation_deg=0.0, power_dissipation_w=0.001, mass_g=0.2), PcbComponent(refdes="TP2", part_number="NTC-PRINTED-10K", description="Printed thermistor, rear conductor run", footprint="PRINTNTC-6x4", pin_count=2, x_mm=1900.0, y_mm=27.0, rotation_deg=0.0, power_dissipation_w=0.001, mass_g=0.2), PcbComponent(refdes="SH1", part_number="SHIELDBOND-4P", description="Shield bond pad group at the B-pillar node", footprint="BONDPAD-6x4", pin_count=4, x_mm=1330.0, y_mm=121.0, rotation_deg=0.0, power_dissipation_w=0.0, mass_g=1.1), ]
def sill_panel_pins(): return [ pcb_pin("TB2", 1, "HV+", "sill-hv-p", 2560.0, 185.0, PcbPinKind.power), pcb_pin("TB2", 2, "HV-", "sill-hv-n", 2560.0, 115.0, PcbPinKind.power), pcb_pin("TB2", 3, "48V", "sill-48v", 2560.0, 62.0, PcbPinKind.power), pcb_pin("TB2", 4, "RTN", "sill-gnd", 2560.0, 32.0, PcbPinKind.ground), pcb_pin("TB1", 1, "HV+", "sill-hv-p", 40.0, 185.0, PcbPinKind.power), pcb_pin("TB1", 2, "HV-", "sill-hv-n", 40.0, 115.0, PcbPinKind.power), pcb_pin("TB1", 3, "48V", "sill-48v", 40.0, 62.0, PcbPinKind.power), pcb_pin("TB1", 4, "RTN", "sill-gnd", 40.0, 32.0, PcbPinKind.ground), pcb_pin("J2", 1, "DATA_P", "sill-data-p", 2480.0, 8.0, PcbPinKind.signal), pcb_pin("J2", 2, "DATA_N", "sill-data-n", 2480.0, 12.0, PcbPinKind.signal), pcb_pin("J2", 3, "SHIELD", "sill-shield", 2480.0, 210.0, PcbPinKind.ground), pcb_pin("J2", 4, "48V", "sill-48v", 2480.0, 62.0, PcbPinKind.power), pcb_pin("J2", 5, "RTN_A", "sill-gnd", 2480.0, 32.0, PcbPinKind.ground), pcb_pin("J2", 6, "RTN_B", "sill-gnd", 2480.0, 28.0, PcbPinKind.ground), pcb_pin("J1", 1, "DATA_P", "sill-data-p", 120.0, 8.0, PcbPinKind.signal), pcb_pin("J1", 2, "DATA_N", "sill-data-n", 120.0, 12.0, PcbPinKind.signal), pcb_pin("J1", 3, "SHIELD", "sill-shield", 120.0, 210.0, PcbPinKind.ground), pcb_pin("J1", 4, "48V", "sill-48v", 120.0, 62.0, PcbPinKind.power), pcb_pin("J1", 5, "RTN_A", "sill-gnd", 120.0, 32.0, PcbPinKind.ground), pcb_pin("J1", 6, "RTN_B", "sill-gnd", 120.0, 28.0, PcbPinKind.ground), pcb_pin("J3", 1, "48V_B", "sill-48v-b", 1300.0, 62.0, PcbPinKind.power), pcb_pin("J3", 2, "RTN", "sill-gnd", 1300.0, 32.0, PcbPinKind.ground), pcb_pin("J3", 3, "TEMP1", "sill-temp", 1300.0, 18.0, PcbPinKind.thermal), pcb_pin("J3", 4, "TEMP2", "sill-temp2", 1300.0, 22.0, PcbPinKind.thermal), pcb_pin("FS1", 1, "IN", "sill-48v", 1370.0, 62.0, PcbPinKind.power), pcb_pin("FS1", 2, "OUT", "sill-48v-b", 1430.0, 62.0, PcbPinKind.power), pcb_pin("TP1", 1, "A", "sill-temp", 900.0, 18.0, PcbPinKind.thermal), pcb_pin("TP1", 2, "B", "sill-gnd", 900.0, 32.0, PcbPinKind.ground), pcb_pin("TP2", 1, "A", "sill-temp2", 1900.0, 22.0, PcbPinKind.thermal), pcb_pin("TP2", 2, "B", "sill-gnd", 1900.0, 32.0, PcbPinKind.ground), pcb_pin("SH1", 1, "SH_A", "sill-shield", 1300.0, 210.0, PcbPinKind.ground), pcb_pin("SH1", 2, "SH_B", "sill-shield", 1340.0, 210.0, PcbPinKind.ground), pcb_pin("SH1", 3, "BOND_A", "sill-gnd", 1320.0, 32.0, PcbPinKind.ground), pcb_pin("SH1", 4, "BOND_B", "sill-gnd", 1360.0, 32.0, PcbPinKind.ground), ]
def sill_panel_nets(): return [ PcbNet(id="sill-hv-p", name="Structural HV positive conductor", net_class=PcbNetClass.hv, pin_ids=["TB2.1", "TB1.1"], nominal_voltage_v=800.0, current_a=SILL_HV_CURRENT_A, is_reference=false), PcbNet(id="sill-hv-n", name="Structural HV negative conductor", net_class=PcbNetClass.hv, pin_ids=["TB2.2", "TB1.2"], nominal_voltage_v=0.0, current_a=SILL_HV_CURRENT_A, is_reference=false), PcbNet(id="sill-48v", name="Structural 48 V conductor", net_class=PcbNetClass.power, pin_ids=["TB2.3", "J2.4", "FS1.1", "J1.4", "TB1.3"], nominal_voltage_v=48.0, current_a=SILL_ZONE_A + SILL_SERVICE_A + SILL_BPILLAR_A, is_reference=false), PcbNet(id="sill-48v-b", name="B-pillar protected branch", net_class=PcbNetClass.power, pin_ids=["FS1.2", "J3.1"], nominal_voltage_v=48.0, current_a=SILL_BPILLAR_A, is_reference=false), PcbNet(id="sill-gnd", name="Structural return conductor", net_class=PcbNetClass.ground, pin_ids=["TB2.4", "J2.5", "J2.6", "TP2.2", "SH1.3", "SH1.4", "J3.2", "TP1.2", "J1.5", "J1.6", "TB1.4"], nominal_voltage_v=0.0, current_a=SILL_ZONE_A + SILL_SERVICE_A + SILL_BPILLAR_A, is_reference=true), PcbNet(id="sill-data-p", name="Printed differential pair positive", net_class=PcbNetClass.ethernet, pin_ids=["J2.1", "J1.1"], nominal_voltage_v=1.1, current_a=0.011, is_reference=false), PcbNet(id="sill-data-n", name="Printed differential pair negative", net_class=PcbNetClass.ethernet, pin_ids=["J1.2", "J2.2"], nominal_voltage_v=0.0, current_a=0.011, is_reference=false), PcbNet(id="sill-shield", name="Overprinted shield conductor", net_class=PcbNetClass.signal, pin_ids=["SH1.1", "SH1.2", "J2.3", "J1.3"], nominal_voltage_v=0.0, current_a=0.0, is_reference=false), PcbNet(id="sill-temp", name="Printed thermistor channel, front run", net_class=PcbNetClass.analog, pin_ids=["J3.3", "TP1.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), PcbNet(id="sill-temp2", name="Printed thermistor channel, rear run", net_class=PcbNetClass.analog, pin_ids=["J3.4", "TP2.1"], nominal_voltage_v=3.3, current_a=0.00025, is_reference=false), ]
def sill_panel_plans(): return [ PcbRoutePlan(net_id="hv", layer_id="sill-pwr", width_mm=60.0, topology="chain", via_count=0, via_drill_mm=1.0, via_pad_mm=2.0, via_layer_id="sill-pwr"), PcbRoutePlan(net_id="power", layer_id="sill-pwr", width_mm=25.0, topology="chain", via_count=0, via_drill_mm=1.0, via_pad_mm=2.0, via_layer_id="sill-pwr"), PcbRoutePlan(net_id="ground", layer_id="sill-pwr", width_mm=25.0, topology="chain", via_count=0, via_drill_mm=1.0, via_pad_mm=2.0, via_layer_id="sill-pwr"), PcbRoutePlan(net_id="ethernet", layer_id="sill-data", width_mm=0.8, topology="chain", via_count=0, via_drill_mm=0.6, via_pad_mm=1.2, via_layer_id="sill-data"), PcbRoutePlan(net_id="analog", layer_id="sill-data", width_mm=0.5, topology="chain", via_count=0, via_drill_mm=0.6, via_pad_mm=1.2, via_layer_id="sill-data"), PcbRoutePlan(net_id="signal", layer_id="sill-shield", width_mm=12.0, topology="chain", via_count=0, via_drill_mm=0.6, via_pad_mm=1.2, via_layer_id="sill-shield"), PcbRoutePlan(net_id="can", layer_id="sill-data", width_mm=0.8, topology="chain", via_count=0, via_drill_mm=0.6, via_pad_mm=1.2, via_layer_id="sill-data"), ]
def sill_panel_overrides(): return [ PcbRoutePlan(net_id="sill-48v-b", layer_id="sill-pwr", width_mm=12.0, topology="chain", via_count=0, via_drill_mm=1.0, via_pad_mm=2.0, via_layer_id="sill-pwr"), ]
def sill_panel_loads(): board = "sill-conductor-panel" return [ pcb_load(board, "TB1.1", "TB1.2", SILL_HV_CURRENT_A, "front drive-unit traction feed, continuous rating"), pcb_load(board, "TB1.3", "TB1.4", SILL_ZONE_A, "front zone controller 48 V supply"), pcb_load(board, "J1.4", "J1.5", SILL_SERVICE_A, "front service branch"), pcb_load(board, "FS1.1", "FS1.2", SILL_BPILLAR_A, "printed fusible link element"), pcb_load(board, "J3.1", "J3.2", SILL_BPILLAR_A, "B-pillar branch load"), pcb_load(board, "J1.1", "J1.2", 0.011, "100BASE-T1 structural pair"), pcb_load(board, "TP1.1", "TP1.2", 0.00025, "front printed thermistor bias"), pcb_load(board, "TP2.1", "TP2.2", 0.00025, "rear printed thermistor bias"), ]
def sill_panel_board() !{}: return assemble_board("sill-conductor-panel", "Structural sill printed conductor panel", "sill-panels", "left structural rocker, A-pillar to C-pillar", "printed conductor panel on an anodised 6082-T6 aluminium sill extrusion", sill_panel_stackup(), sill_panel_components(), sill_panel_pins(), sill_panel_nets(), sill_panel_plans(), sill_panel_overrides(), sill_panel_loads(), 2600.0, 220.0)
def board_loads(board_id: str): if board_id == "bms-slave": return bms_slave_loads() if board_id == "front-zone-controller": return front_zone_loads() if board_id == "hv-junction-board": return hv_junction_loads() return sill_panel_loads()
def trace_branch_resistance_ohm(board: PcbBoard, trace: PcbTrace): # Vias are emitted as two parallel groups of n barrels around one buried run, # so the series contribution is 2 R1 / n = 4 * sum / count^2. mut group_sum = 0.0 mut group_count = 0 prefix = trace.id + "-v" for via in board.vias: if via.id.startswith(prefix): group_sum = group_sum + via.resistance_mohm group_count = group_count + 1 series_mohm = 0.0 if group_count == 0 else 4.0 * group_sum / float(group_count * group_count) return (trace.resistance_mohm + series_mohm) / 1000.0
def board_solution(board: PcbBoard): mut solve_segments = [] mut solve_resistances = [] for trace in board.traces: solve_segments.append(PcbSegment(id=trace.id, net_id=trace.net_id, layer_id=trace.layer_id, width_mm=trace.width_mm, from_pin_id=trace.from_pin_id, to_pin_id=trace.to_pin_id, via_count=0, via_drill_mm=0.3, via_pad_mm=0.6, via_layer_id=trace.layer_id)) solve_resistances.append(trace_branch_resistance_ohm(board, trace)) return solve_network(board.pins, board.nets, solve_segments, solve_resistances, board_loads(board.id))
def node_voltage(solution: PcbSolution, pin_id: str): for index in range(0, len(solution.node_ids)): if solution.node_ids[index] == pin_id: return solution.node_voltages[index] return 0.0
def net_drop_v(board: PcbBoard, solution: PcbSolution, net: PcbNet): feed = node_voltage(solution, net.pin_ids[0]) mut spread = 0.0 for pin_id in net.pin_ids: spread = max(spread, abs(feed - node_voltage(solution, pin_id))) return spread
def board_drc_violations(board: PcbBoard): mut findings = [] mut seen_pins = {} for pin in board.pins: if pin.x_mm > board.outline_width_mm or pin.y_mm > board.outline_height_mm: findings.append("pad outside outline: " + pin.id) seen_pins[pin.id] = seen_pins.get(pin.id, 0) + 1 if seen_pins[pin.id] > 1: findings.append("duplicate pad identifier: " + pin.id) for trace in board.traces: if trace.width_mm < MIN_TRACK_WIDTH_MM: findings.append("track below the minimum manufacturable width: " + trace.id) for via in board.vias: if (via.pad_mm - via.drill_mm) / 2.0 < MIN_ANNULAR_RING_MM: findings.append("annular ring below the class 3 minimum: " + via.id) return findings
def isolated_domain_nets(board_id: str): # Nets that float at traction-battery potential relative to the LV chassis domain. if board_id == "hv-junction-board": return ["hvj-hv-pack-p", "hvj-hv-pack-n", "hvj-hv-bus-p", "hvj-hv-bus-n", "hvj-hv-out-p", "hvj-hv-out-n", "hvj-shunt-p", "hvj-shunt-n", "hvj-imd-node", "hvj-iso-5v", "hvj-iso-gnd"] if board_id == "sill-conductor-panel": return ["sill-hv-p", "sill-hv-n"] return []
def isolation_required_mm(same_domain: bool, potential_difference_v: f64): # Creepage and clearance must both hold, so the binding requirement is the larger. # A barrier crossing gets reinforced insulation (twice the basic creepage) at the # full 800 V working voltage; inside one domain the local difference sets creepage # while the system impulse withstand still sets the clearance floor. if not same_domain: return max(2.0 * iec60664_creepage_mm(HV_WORKING_VOLTAGE_V), 2.0 * HV_CLEARANCE_MM) if potential_difference_v <= HAZARDOUS_VOLTAGE_V: return 0.0 return max(iec60664_creepage_mm(potential_difference_v), HV_CLEARANCE_MM)
def net_lookup(nets: list[PcbNet]) -> dict[str, int]: mut net_index = {} for index in range(0, len(nets)): net_index[nets[index].id] = index return net_index
def pcb_isolation_checks(board: PcbBoard): isolated = isolated_domain_nets(board.id) if len(isolated) == 0: return [] net_index = net_lookup(board.nets) mut in_domain = {} for net_id in isolated: in_domain[net_id] = 1 mut checks = [] mut tightest_barrier = 1000000.0 mut barrier_pair = ["", ""] mut tightest_basic = 1000000.0 mut basic_pair = ["", ""] mut basic_delta = 0.0 for left in range(0, len(board.pins)): for right in range(left + 1, len(board.pins)): first = board.pins[left] second = board.pins[right] if first.net_id == second.net_id: continue first_hv = in_domain.get(first.net_id, 0) == 1 second_hv = in_domain.get(second.net_id, 0) == 1 if not first_hv and not second_hv: continue delta = abs(board.nets[net_index.get(first.net_id, 0)].nominal_voltage_v - board.nets[net_index.get(second.net_id, 0)].nominal_voltage_v) required = isolation_required_mm(first_hv == second_hv, delta) if required <= 0.0: continue gap = math.sqrt((first.x_mm - second.x_mm) * (first.x_mm - second.x_mm) + (first.y_mm - second.y_mm) * (first.y_mm - second.y_mm)) if first_hv != second_hv: if gap < tightest_barrier: tightest_barrier = gap barrier_pair = [first.id, second.id] elif gap < tightest_basic: tightest_basic = gap basic_pair = [first.id, second.id] basic_delta = delta if gap < required: checks.append(PcbIsolationCheck(id=board.id + ":" + first.id + "/" + second.id, board_id=board.id, category="violation", from_pin_id=first.id, to_pin_id=second.id, working_voltage_v=delta, required_mm=required, measured_mm=gap, reinforced=first_hv != second_hv, compliant=false)) if len(barrier_pair[0]) > 0: barrier_required = isolation_required_mm(false, HV_WORKING_VOLTAGE_V) checks.append(PcbIsolationCheck(id=board.id + ":tightest-barrier", board_id=board.id, category="reinforced barrier, IEC 60664-1 PD2 group IIIa", from_pin_id=barrier_pair[0], to_pin_id=barrier_pair[1], working_voltage_v=HV_WORKING_VOLTAGE_V, required_mm=barrier_required, measured_mm=tightest_barrier, reinforced=true, compliant=tightest_barrier >= barrier_required)) if len(basic_pair[0]) > 0: basic_required = isolation_required_mm(true, basic_delta) checks.append(PcbIsolationCheck(id=board.id + ":tightest-basic", board_id=board.id, category="basic creepage inside the HV domain", from_pin_id=basic_pair[0], to_pin_id=basic_pair[1], working_voltage_v=basic_delta, required_mm=basic_required, measured_mm=tightest_basic, reinforced=false, compliant=tightest_basic >= basic_required)) return checks
def analyze_solved(board: PcbBoard, field: PcbSolution, gates: list[PcbIsolationCheck]): mut copper_loss_w = 0.0 mut max_rise = 0.0 mut ipc_violations = 0 for trace in board.traces: copper_loss_w = copper_loss_w + trace.power_loss_mw / 1000.0 max_rise = max(max_rise, trace.temperature_rise_c) if not trace.compliant: ipc_violations = ipc_violations + 1 for via in board.vias: copper_loss_w = copper_loss_w + via.current_a * via.current_a * via.resistance_mohm / 1000.0 mut worst_net = board.nets[0].id mut worst_drop = 0.0 mut worst_fraction = 0.0 mut source_node = board.nets[0].pin_ids[0] mut source_voltage = 0.0 for net in board.nets: drop = net_drop_v(board, field, net) if net.nominal_voltage_v > source_voltage: source_voltage = net.nominal_voltage_v source_node = net.pin_ids[0] if net.nominal_voltage_v >= 1.0: fraction = drop / net.nominal_voltage_v if fraction > worst_fraction: worst_fraction = fraction worst_drop = drop worst_net = net.id mut isolation_violations = 0 for check in gates: if not check.compliant: isolation_violations = isolation_violations + 1 return PcbAnalysis( board_id=board.id, node_count=len(field.node_ids), branch_count=len(board.traces), source_node=source_node, worst_net_id=worst_net, worst_voltage_drop_mv=worst_drop * 1000.0, worst_drop_fraction=worst_fraction, total_copper_loss_w=copper_loss_w, max_temperature_rise_c=max_rise, ipc_violations=ipc_violations, drc_violations=len(board_drc_violations(board)), isolation_violations=isolation_violations, solver_residual=field.solver_residual, kcl_residual_a=field.kcl_residual_a, )
def pcb_analyze(board: PcbBoard): return analyze_solved(board, board_solution(board), pcb_isolation_checks(board))
def pcb_netlist_text(board: PcbBoard): mut netlist_rows = ["(export (version D)"] netlist_rows.append(" (design (source \"" + board.id + "\") (tool \"Sema pcb.sema engineered reconstruction\") (substrate \"" + board.substrate + "\"))") netlist_rows.append(" (components") for component in board.components: netlist_rows.append(" (comp (ref " + component.refdes + ") (value " + component.part_number + ") (footprint " + component.footprint + "))") netlist_rows.append(" )") netlist_rows.append(" (nets") mut code = 0 for net in board.nets: code = code + 1 netlist_rows.append(" (net (code " + str(code) + ") (name \"/" + net.id + "\")") for pin in board.pins: if pin.net_id == net.id: netlist_rows.append(" (node (ref " + pin.refdes + ") (pin " + str(pin.number) + "))") netlist_rows.append(" )") netlist_rows.append(" )") netlist_rows.append(")") return "\n".join(netlist_rows)
def pcb_boards() !{}: return [bms_slave_board(), front_zone_board(), hv_junction_board(), sill_panel_board()]
def component_kind(refdes: str): if refdes.startswith("RT") or refdes.startswith("TP"): return "sensor" if refdes.startswith("RS"): return "shunt" if refdes.startswith("SH"): return "shield" if refdes.startswith("FS") or refdes.startswith("F"): return "fuse" if refdes.startswith("TB") or refdes.startswith("J"): return "connector" if refdes.startswith("U"): return "ic" if refdes.startswith("Q"): return "transistor" if refdes.startswith("R"): return "resistor" if refdes.startswith("C"): return "capacitor" if refdes.startswith("L"): return "inductor" if refdes.startswith("K"): return "relay" if refdes.startswith("D"): return "diode" return "magnetics"
def component_extent(board: PcbBoard, refdes: str): mut low_x = board.outline_width_mm mut low_y = board.outline_height_mm mut high_x = 0.0 mut high_y = 0.0 mut found = false for pin in board.pins: if pin.refdes == refdes: found = true low_x = min(low_x, pin.x_mm) low_y = min(low_y, pin.y_mm) high_x = max(high_x, pin.x_mm) high_y = max(high_y, pin.y_mm) if not found: return [1.6, 0.8] return [max(1.6, high_x - low_x + 1.6), max(0.8, high_y - low_y + 1.6)]
def pad_extent(footprint: str): if footprint.startswith("BUSBAR") or footprint.startswith("BONDPAD"): return [9.0, 9.0] if footprint.startswith("CONTACTOR") or footprint.startswith("PYRO") or footprint.startswith("SHUNT") or footprint.startswith("PRINTLINK"): return [6.0, 6.0] if footprint.startswith("MOLEX") or footprint.startswith("JST") or footprint.startswith("HVIL"): return [1.6, 2.4] if footprint.startswith("D2PAK") or footprint.startswith("DPAK") or footprint.startswith("IND") or footprint.startswith("PQFN"): return [2.2, 2.2] if footprint.startswith("1206") or footprint.startswith("2512") or footprint.startswith("RES-HV") or footprint.startswith("PRINTNTC"): return [1.4, 1.6] return [0.9, 0.5]
def layer_kind(layer: PcbLayer): if layer.copper_um <= 0.0: return "base" return "plane" if layer.function == PcbLayerFunction.plane else "signal"
def layer_color(index: int, layer: PcbLayer): if layer.copper_um <= 0.0: return "#2b2f36" palette = ["#e0803a", "#3f8fd0", "#6fbf5a", "#c05fc0", "#d8c04a", "#5ac4c0", "#c85050", "#8f7ad8"] return palette[index % len(palette)]
def substrate_kind(board_id: str): if board_id == "hv-junction-board": return "ims" if board_id == "sill-conductor-panel": return "structural-sill" return "fr4"
def viewer_net_class(net_class: PcbNetClass): if net_class == PcbNetClass.can or net_class == PcbNetClass.ethernet: return "data" if net_class == PcbNetClass.analog: return "sensor" return net_class_key(net_class)
def vehicle_net_for(net_id: str): if net_id == "sill-hv-p" or net_id == "sill-hv-n": return "hv-pack-front" if net_id == "hvj-hv-out-p" or net_id == "hvj-hv-out-n" or net_id == "hvj-hv-pack-p" or net_id == "hvj-hv-pack-n" or net_id == "hvj-hv-bus-p" or net_id == "hvj-hv-bus-n": return "hv-pack-rear" if net_id == "sill-48v" or net_id == "sill-48v-b" or net_id == "fzc-48v": return "lv-zones" if net_id == "sill-data-p" or net_id == "sill-data-n" or net_id == "fzc-cable-eth-p" or net_id == "fzc-cable-eth-n": return "data-backbone" if net_id == "fzc-can0-h" or net_id == "fzc-can0-l" or net_id == "fzc-can1-h" or net_id == "fzc-can1-l": return "front-drive-control" if net_id == "bmsA-ntc1" or net_id == "bmsA-ntc2" or net_id == "sill-temp" or net_id == "sill-temp2": return "thermal-sensors" return ""
def board_pours(board: PcbBoard): inset = min(2.0, board.outline_width_mm * 0.02) mut pours = [] for layer in board.stackup.layers: if layer.function != PcbLayerFunction.plane or layer.copper_um <= 0.0: continue mut owner = "" mut best = 0 mut counts = {} for trace in board.traces: if trace.layer_id != layer.id: continue tally = counts.get(trace.net_id, 0) + 1 counts[trace.net_id] = tally if tally > best: best = tally owner = trace.net_id if len(owner) == 0: owner = board.nets[0].id pours.append({ "id": board.id + ":" + layer.id + ":pour", "layer": layer.id, "net_id": owner, "label": layer.name, "points": [[inset, inset], [board.outline_width_mm - inset, inset], [board.outline_width_mm - inset, board.outline_height_mm - inset], [inset, board.outline_height_mm - inset]], }) return pours
def board_export(board: PcbBoard): index_of = pin_index_map(board.pins) solution = board_solution(board) gates = pcb_isolation_checks(board) report = analyze_solved(board, solution, gates) netlist = pcb_netlist_text(board) mut layers = [] for index in range(0, len(board.stackup.layers)): layer = board.stackup.layers[index] layers.append({ "id": layer.id, "name": layer.name, "index": index, "kind": layer_kind(layer), "function": layer_function_key(layer.function), "copper_oz": layer.copper_um / 34.79, "copper_um": layer.copper_um, "dielectric_mm": layer.dielectric_mm, "dielectric_er": layer.dielectric_er, "material": layer.material, "color": layer_color(index, layer), "internal": index > 0 and index < len(board.stackup.layers) - 1, }) mut components = [] mut pins = [] for component in board.components: extent = component_extent(board, component.refdes) pad = pad_extent(component.footprint) mut component_pins = [] for pin in board.pins: if pin.refdes != component.refdes: continue record = { "id": pin.id, "refdes": pin.refdes, "number": pin.number, "name": pin.name, "kind": pin_kind_key(pin.kind), "net_id": pin.net_id, "at": [pin.x_mm, pin.y_mm], "size": pad, "voltage_v": solution.node_voltages[index_of.get(pin.id, 0)], } component_pins.append(record) pins.append(record) components.append({ "refdes": component.refdes, "part_number": component.part_number, "description": component.description, "footprint": component.footprint, "kind": component_kind(component.refdes), "center": [component.x_mm, component.y_mm], "rotation": component.rotation_deg, "size": extent, "layer": board.stackup.layers[0].id, "pin_count": component.pin_count, "modelled_pin_count": len(component_pins), "dissipation_w": component.power_dissipation_w, "mass_g": component.mass_g, "pins": component_pins, }) mut traces = [] mut violations = [] mut routed_length_mm = 0.0 mut copper_area_mm2 = 0.0 mut midpoint_of = {} for trace in board.traces: origin = board.pins[index_of.get(trace.from_pin_id, 0)] target = board.pins[index_of.get(trace.to_pin_id, 0)] points = route_points(origin.x_mm, origin.y_mm, target.x_mm, target.y_mm) routed_length_mm = routed_length_mm + trace.length_mm copper_area_mm2 = copper_area_mm2 + trace.length_mm * trace.width_mm midpoint_of[trace.id] = len(traces) traces.append({ "id": trace.id, "net_id": trace.net_id, "layer": trace.layer_id, "width_mm": trace.width_mm, "points": points, "current_a": trace.current_a, "delta_t_c": trace.temperature_rise_c, "length_mm": trace.length_mm, "resistance_mohm": trace.resistance_mohm, "drop_mv": trace.voltage_drop_mv, "power_loss_mw": trace.power_loss_mw, "required_width_mm": trace.ipc_min_width_mm, "compliant": trace.compliant, "from_pin_id": trace.from_pin_id, "to_pin_id": trace.to_pin_id, }) if not trace.compliant: violations.append({ "trace_id": trace.id, "net_id": trace.net_id, "layer": trace.layer_id, "width_mm": trace.width_mm, "required_width_mm": trace.ipc_min_width_mm, "current_a": trace.current_a, "delta_t_c": trace.temperature_rise_c, "deficit_mm": trace.ipc_min_width_mm - trace.width_mm, "severity": "major" if trace.ipc_min_width_mm > trace.width_mm * 1.25 else "marginal", }) mut vias = [] for via in board.vias: parent = via.id.substring(0, via.id.rfind("-v")) anchor = traces[midpoint_of.get(parent, 0)]["points"][1] vias.append({ "id": via.id, "net_id": via.net_id, "at": anchor, "drill_mm": via.drill_mm, "pad_mm": via.pad_mm, "from_layer": via.from_layer_id, "to_layer": via.to_layer_id, "current_a": via.current_a, "resistance_mohm": via.resistance_mohm, "count": 1, }) mut nets = [] mut worst_drop_mv = 0.0 for net in board.nets: mut trace_count = 0 for trace in board.traces: if trace.net_id == net.id: trace_count = trace_count + 1 drop_mv = net_drop_v(board, solution, net) * 1000.0 worst_drop_mv = max(worst_drop_mv, drop_mv) nets.append({ "id": net.id, "name": net.name, "net_class": viewer_net_class(net.net_class), "net_class_raw": net_class_key(net.net_class), "nominal_voltage_v": net.nominal_voltage_v, "current_a": net.current_a, "is_reference": net.is_reference, "feed_pin_id": net.pin_ids[0], "pin_ids": net.pin_ids, "note": "feed pin held at the declared operating potential; every other pad solved", "worst_drop_mv": drop_mv, "trace_count": trace_count, "vehicle_net_id": vehicle_net_for(net.id), }) mut isolation = [] for check in gates: isolation.append({"id": check.id, "category": check.category, "from_pin_id": check.from_pin_id, "to_pin_id": check.to_pin_id, "working_voltage_v": check.working_voltage_v, "required_mm": check.required_mm, "measured_mm": check.measured_mm, "reinforced": check.reinforced, "compliant": check.compliant}) mut dissipation_w = report.total_copper_loss_w for component in board.components: dissipation_w = dissipation_w + component.power_dissipation_w analysis = { "board_id": report.board_id, "layer_count": len(board.stackup.layers), "component_count": len(board.components), "pin_count": len(board.pins), "net_count": len(board.nets), "trace_count": len(board.traces), "via_count": len(board.vias), "node_count": report.node_count, "branch_count": report.branch_count, "source_node": report.source_node, "routed_length_mm": routed_length_mm, "copper_area_mm2": copper_area_mm2, "total_dissipation_w": dissipation_w, "total_copper_loss_w": report.total_copper_loss_w, "worst_drop_mv": report.worst_voltage_drop_mv, "worst_drop_fraction": report.worst_drop_fraction, "worst_drop_net_id": report.worst_net_id, "max_temperature_rise_c": report.max_temperature_rise_c, "ipc_violations": report.ipc_violations, "drc_violations": report.drc_violations, "isolation_violations": report.isolation_violations, "solver_residual": report.solver_residual, "kcl_residual_a": report.kcl_residual_a, "rule": "IPC-2221B conductor sizing at " + str(IPC_ALLOWED_RISE_C) + " K rise, IPC-6012 class " + str(board.stackup.ipc_class), "violations": violations, "isolation": isolation, "verdict": "pass" if report.ipc_violations + report.drc_violations + report.isolation_violations == 0 else "review", } return { "id": board.id, "name": board.name, "host_part_id": board.host_part_id, "part_id": board.host_part_id, "zone": board.zone, "substrate": substrate_kind(board.id), "substrate_label": board.substrate, "outline_width_mm": board.outline_width_mm, "outline_height_mm": board.outline_height_mm, "size": [board.outline_width_mm, board.outline_height_mm], "corner_radius": min(4.0, board.outline_width_mm * 0.02), "stackup": {"id": board.stackup.id, "name": board.stackup.name, "total_thickness_mm": board.stackup.total_thickness_mm, "ipc_class": board.stackup.ipc_class, "impedance_target_ohm": board.stackup.impedance_target_ohm}, "summary": board.name + " on " + board.substrate, "layers": layers, "components": components, "pins": pins, "traces": traces, "vias": vias, "pours": board_pours(board), "nets": nets, "analysis": analysis, "netlist": netlist, "netlist_sha_free_text_length": len(netlist), "board_worst_drop_mv": worst_drop_mv, }
pub def pcb_export() -> dict[str, any] !{}: mut boards = [] mut component_count = 0 mut pin_count = 0 mut net_count = 0 mut trace_count = 0 mut via_count = 0 mut copper_loss_w = 0.0 mut violation_count = 0 mut peak_drop_mv = 0.0 for item in pcb_boards(): exported = board_export(item) boards.append(exported) summary = exported["analysis"] component_count = component_count + summary["component_count"] pin_count = pin_count + summary["pin_count"] net_count = net_count + summary["net_count"] trace_count = trace_count + summary["trace_count"] via_count = via_count + summary["via_count"] copper_loss_w = copper_loss_w + summary["total_copper_loss_w"] violation_count = violation_count + summary["ipc_violations"] + summary["drc_violations"] + summary["isolation_violations"] peak_drop_mv = max(peak_drop_mv, exported["board_worst_drop_mv"]) totals = { "board_count": len(boards), "component_count": component_count, "pin_count": pin_count, "net_count": net_count, "trace_count": trace_count, "via_count": via_count, "copper_loss_w": copper_loss_w, "violations": violation_count, "peak_conductor_drop_mv": peak_drop_mv, } return { "schema": "sema.circuitframe-pcb/v1", "units": "mm, A, V, W, degC", "standard": "IPC-2221B conductor sizing, IPC-6012 class 3 fabrication, IEC 60664-1 creepage screening", "provenance": "engineered reconstruction; not an OEM design, netlist or manufacturing data set", "conductor_model": "copper resistivity 1.724e-8 ohm.m at 20 degC, temperature coefficient 0.00393 /K, evaluated at " + str(CONDUCTOR_OPERATING_TEMP_C) + " degC", "boards": boards, "totals": totals, "summary": totals, "equations": pcb_equations(), }
pub def pcb_equations() -> list[dict[str, str]] !{}: return [ {"id": "pcb-resistivity", "source": "conductor_resistance_ohm", "expression": "R = rho_20 (1 + alpha (T - 20)) L / (w t), rho_20 = 1.724e-8 ohm.m, alpha = 0.00393 /K", "unit": "ohm", "owner": "Sema printed-conductor model"}, {"id": "pcb-nodal-analysis", "source": "solve_network", "expression": "G v = i, feed pin of every net held at its declared potential, loads injected at sink and returned at the return pad", "unit": "V", "owner": "Sema DC modified nodal analysis"}, {"id": "pcb-joule-loss", "source": "joule_loss_w", "expression": "P = I^2 R with I = (v_a - v_b) / R from the solved node voltages", "unit": "W", "owner": "Sema DC loss model"}, {"id": "pcb-via-barrel", "source": "via_barrel_resistance_ohm", "expression": "R_via = rho h / (pi (d + t) t), t = 25 um plating", "unit": "ohm", "owner": "Sema plated-barrel model"}, {"id": "pcb-ipc2221-width", "source": "ipc2221_cross_section_mils2", "expression": "A_mils2 = (I / (k dT^0.44))^(1/0.725), k = 0.048 external and 0.024 internal; w_mm = A_mils2 / (t_um / 25.4) * 0.0254; the published curve is fitted to I <= 35 A, so busbar and printed-sill currents are extrapolated", "unit": "mm", "owner": "IPC-2221B conductor sizing"}, {"id": "pcb-temperature-rise", "source": "ipc2221_temperature_rise_c", "expression": "dT = (I / (k A^0.725))^(1/0.44), the same relation inverted for the declared cross-section", "unit": "degC", "owner": "IPC-2221B conductor sizing"}, {"id": "pcb-creepage", "source": "iec60664_creepage_mm", "expression": "creepage = table4(V_working) at pollution degree 2 and material group IIIa; reinforced insulation = 2 x basic", "unit": "mm", "owner": "IEC 60664-1 screening"}, ]
test "every pad resolves to a declared component and net, and every trace stays on its net": mut orphan_pads = 0 mut thin_nets = 0 mut stray_endpoints = 0 mut missing_members = 0 for subject in pcb_boards(): mut known_nets = {} for subject_net in subject.nets: known_nets[subject_net.id] = len(subject_net.pin_ids) if len(subject_net.pin_ids) < 2: thin_nets = thin_nets + 1 mut known_refdes = {} for subject_component in subject.components: known_refdes[subject_component.refdes] = 1 mut owner_of = {} for subject_pad in subject.pins: owner_of[subject_pad.id] = subject_pad.net_id if known_nets.get(subject_pad.net_id, 0) == 0 or known_refdes.get(subject_pad.refdes, 0) == 0: orphan_pads = orphan_pads + 1 for subject_net in subject.nets: for member in subject_net.pin_ids: if owner_of.get(member, "") != subject_net.id: missing_members = missing_members + 1 for subject_wire in subject.traces: if owner_of.get(subject_wire.from_pin_id, "") != subject_wire.net_id: stray_endpoints = stray_endpoints + 1 if owner_of.get(subject_wire.to_pin_id, "") != subject_wire.net_id: stray_endpoints = stray_endpoints + 1 ensure orphan_pads == 0 ensure thin_nets == 0 ensure stray_endpoints == 0 ensure missing_members == 0
test "the solved rail falls monotonically away from its feed and closes Kirchhoff": panel = sill_panel_board() field = board_solution(panel) ladder = ["TB2.3", "J2.4", "FS1.1", "J1.4", "TB1.3"] mut upstream = node_voltage(field, ladder[0]) for rung in range(1, len(ladder)): downstream = node_voltage(field, ladder[rung]) ensure downstream < upstream upstream = downstream ensure abs(node_voltage(field, ladder[0]) - 48.0) < 0.000000001 ensure node_voltage(field, ladder[0]) - node_voltage(field, ladder[4]) > 0.05 ensure field.kcl_residual_a < 0.000000001 ensure field.solver_residual < 0.000000001 ensure pcb_analyze(front_zone_board()).kcl_residual_a < 0.000000001 ensure pcb_analyze(hv_junction_board()).kcl_residual_a < 0.000000001
test "copper loss equals I squared R over every routed conductor": mut reported_mw = 0.0 mut recomputed_mw = 0.0 for subject in pcb_boards(): for subject_wire in subject.traces: reported_mw = reported_mw + subject_wire.power_loss_mw recomputed_mw = recomputed_mw + subject_wire.current_a * subject_wire.current_a * subject_wire.resistance_mohm ensure reported_mw > 60000.0 ensure abs(reported_mw - recomputed_mw) / reported_mw < 0.000000001
test "node-wise and branch-wise power sums agree on the solved zone controller": card = front_zone_board() field = board_solution(card) mut node_side = 0.0 mut branch_side = 0.0 for rung in range(0, len(card.traces)): subject_wire = card.traces[rung] drive = field.branch_currents[rung] branch_side = branch_side + drive * drive * trace_branch_resistance_ohm(card, subject_wire) node_side = node_side + (node_voltage(field, subject_wire.from_pin_id) - node_voltage(field, subject_wire.to_pin_id)) * drive ensure branch_side > 0.0 ensure abs(node_side - branch_side) / branch_side < 0.000000001
test "IPC-2221 sizing clears the sound eFuse channel and rejects the marginal one": card = front_zone_board() mut marginal = 0 mut sound = 0 for subject_wire in card.traces: if subject_wire.id == "fzc-out1-s1": marginal = marginal + 1 ensure not subject_wire.compliant ensure subject_wire.ipc_min_width_mm > subject_wire.width_mm ensure subject_wire.ipc_min_width_mm < subject_wire.width_mm * 1.1 ensure subject_wire.temperature_rise_c > IPC_ALLOWED_RISE_C if subject_wire.id == "fzc-out2-s1": sound = sound + 1 ensure subject_wire.compliant ensure subject_wire.ipc_min_width_mm < subject_wire.width_mm ensure subject_wire.temperature_rise_c < IPC_ALLOWED_RISE_C ensure marginal == 1 and sound == 1 ensure pcb_analyze(card).ipc_violations == 1 ensure ipc_min_width_mm(3.1, 35.0, true) < ipc_min_width_mm(3.1, 35.0, false) ensure abs(ipc_temperature_rise_c(3.1, ipc_min_width_mm(3.1, 35.0, false), 35.0, false) - IPC_ALLOWED_RISE_C) < 0.00000001 ensure ipc_min_width_mm(0.0, 35.0, true) == 0.0
test "the generated netlist enumerates exactly one node per connected pad": for subject in pcb_boards(): rendered = pcb_netlist_text(subject) mut connected = 0 for subject_pad in subject.pins: if len(subject_pad.net_id) > 0: connected = connected + 1 ensure rendered.startswith("(export (version D)") ensure rendered.count("(node ") == connected ensure rendered.count("(comp (ref ") == len(subject.components) ensure rendered.count("(net (code ") == len(subject.nets)
test "the 800 V board holds IEC 60664-1 creepage and an under-spec gap is rejected": card = hv_junction_board() mut barrier_mm = 0.0 mut basic_mm = 0.0 for gate in pcb_isolation_checks(card): ensure gate.compliant if gate.reinforced: barrier_mm = gate.measured_mm else: basic_mm = gate.measured_mm ensure abs(iec60664_creepage_mm(800.0) - 8.0) < 0.000001 ensure abs(isolation_required_mm(false, 800.0) - 16.0) < 0.000001 ensure isolation_required_mm(true, 12.0) == 0.0 ensure barrier_mm >= isolation_required_mm(false, 800.0) ensure basic_mm >= isolation_required_mm(true, 800.0) understated = PcbIsolationCheck(id="synthetic-under-spec", board_id=card.id, category="synthetic barrier gap", from_pin_id="U1.4", to_pin_id="U1.5", working_voltage_v=800.0, required_mm=isolation_required_mm(false, 800.0), measured_mm=6.0, reinforced=true, compliant=6.0 >= isolation_required_mm(false, 800.0)) ensure not understated.compliant ensure pcb_analyze(card).isolation_violations == 0
test "conductor resistance follows the temperature-corrected copper law": rho = operating_resistivity() ensure abs(rho - COPPER_RESISTIVITY_OHM_M * (1.0 + COPPER_TEMPCO_PER_K * 25.0)) < 0.0000000000000001 ensure abs(conductor_resistance_ohm(rho, 1.0, 0.001, 0.000035) - rho / 0.000000035) < 0.000000001 corner = route_points(0.0, 0.0, 10.0, 4.0) ensure len(corner) == 3 ensure abs(polyline_length_mm(corner) - (6.0 + math.sqrt(32.0))) < 0.000000001
test "the exported payload carries four boards with drawable geometry and analysis": payload = pcb_export() encoded = encode_json(payload) ensure len(encoded) > 200000 and len(encoded) < 400000 ensure payload["schema"] == "sema.circuitframe-pcb/v1" ensure payload["totals"]["board_count"] == 4 ensure payload["totals"]["component_count"] == 64 ensure payload["totals"]["pin_count"] == 260 ensure payload["totals"]["net_count"] == 84 ensure payload["totals"]["trace_count"] == 176 ensure payload["totals"]["via_count"] == 230 ensure payload["totals"]["violations"] == 1 ensure payload["totals"]["copper_loss_w"] > 69.0 and payload["totals"]["copper_loss_w"] < 70.0 ensure payload["boards"][0]["id"] == "bms-slave" ensure payload["boards"][1]["id"] == "front-zone-controller" ensure payload["boards"][2]["id"] == "hv-junction-board" ensure payload["boards"][3]["id"] == "sill-conductor-panel" ensure payload["boards"][3]["substrate"] == "structural-sill" ensure payload["boards"][2]["substrate"] == "ims" mut drawable = 0 for card in payload["boards"]: ensure card["size"][0] > 0.0 and card["size"][1] > 0.0 ensure card["layers"][0]["index"] == 0 ensure len(card["netlist"]) == card["netlist_sha_free_text_length"] for subject_wire in card["traces"]: ensure len(subject_wire["points"]) >= 2 if subject_wire["compliant"]: ensure subject_wire["width_mm"] >= subject_wire["required_width_mm"] else: ensure subject_wire["width_mm"] < subject_wire["required_width_mm"] drawable = drawable + 1 ensure drawable == 176
test "the equation manifest publishes the same row shape as the dynamics manifest": rows = pcb_equations() ensure len(rows) == 7 mut described = 0 for row in rows: ensure len(row["id"]) > 0 and len(row["source"]) > 0 ensure len(row["expression"]) > 0 and len(row["unit"]) > 0 and len(row["owner"]) > 0 described = described + 1 ensure described == 7src/physics.sema
Section titled “src/physics.sema”"""Coupled electric-vehicle dynamics over the panel-solved aerodynamic field.
Aerodynamic loads come from `aero.sema`: a constant-strength source-panel solve with an imageground plane, closed by an empirical viscous and base-pressure model. Mass, centre of gravity andyaw inertia come from `assembly.sema`, summed from the declared part placements rather than typedin as literals. The thermal state is the eight-node coolant network from the same module, so heatmoves between components instead of each one sinking independently to ambient.
Two reduced-order surrogates are fitted to the panel solve at startup and are continuously scoredagainst it while the vehicle runs; that residual is what drives the model-adaptation lifecycle.The dynamics themselves always integrate the solved loads, never the surrogate."""
import math
from magna_ev_digital_twin.aero import AeroBasis, AeroForces, aero_basis, aero_solvefrom magna_ev_digital_twin.assembly import MassProperties, SuspensionState, air_spring_reference_pressure, arm_rotation_limits, corner_pose, mass_properties, suspension_travel, thermal_initial_temps, thermal_step, wheel_forcefrom magna_ev_digital_twin.hardware import AxleHardware, HardwareLimits, brake_fade_factor, hardware_limits, rolling_coefficient, tire_friction
assure silver
CALM_MODEL_ID = "aero-rom-calm-v1"CROSSWIND_MODEL_ID = "aero-rom-crosswind-v2"THERMAL_NODE_COUNT = 8MAX_WIND_MPS = 60.0
pub struct ControlInput: throttle: f64 brake: f64 steering: f64 wind_mps: f64 wind_yaw_deg: f64 invariant throttle >= 0.0 and throttle <= 1.0 invariant brake >= 0.0 and brake <= 1.0 invariant steering >= -1.0 and steering <= 1.0 invariant wind_mps >= 0.0 and wind_mps <= MAX_WIND_MPS invariant wind_yaw_deg >= -180.0 and wind_yaw_deg <= 180.0
pub struct VehicleState: time_s: f64 distance_m: f64 speed_mps: f64 lateral_speed_mps: f64 acceleration_mps2: f64 lateral_acceleration_mps2: f64 yaw_rate_rps: f64 yaw_angle_rad: f64 steering_angle_rad: f64 battery_soc: f64 thermal_temps_c: list[f64] battery_temp_c: f64 motor_temp_c: f64 inverter_temp_c: f64 coolant_temp_c: f64 cabin_temp_c: f64 tire_temp_c: f64 front_disc_temp_c: f64 rear_disc_temp_c: f64 motor_rpm: f64 model_id: str invariant time_s >= 0.0 invariant distance_m >= 0.0 invariant speed_mps >= 0.0 and speed_mps <= 90.0 invariant lateral_speed_mps >= -40.0 and lateral_speed_mps <= 40.0 invariant yaw_rate_rps >= -3.0 and yaw_rate_rps <= 3.0 invariant battery_soc >= 0.0 and battery_soc <= 1.0 invariant len(thermal_temps_c) == THERMAL_NODE_COUNT invariant battery_temp_c >= -40.0 and battery_temp_c <= 120.0 invariant motor_temp_c >= -40.0 and motor_temp_c <= 220.0 invariant inverter_temp_c >= -40.0 and inverter_temp_c <= 180.0 invariant coolant_temp_c >= -40.0 and coolant_temp_c <= 140.0 invariant cabin_temp_c >= -40.0 and cabin_temp_c <= 90.0 invariant tire_temp_c >= -40.0 and tire_temp_c <= 180.0 invariant front_disc_temp_c >= -40.0 and front_disc_temp_c <= MAX_DISC_TEMP_C invariant rear_disc_temp_c >= -40.0 and rear_disc_temp_c <= MAX_DISC_TEMP_C invariant len(model_id) > 0
pub struct AeroLoads: """Body-axis aerodynamic loads. Positive drag resists forward travel; positive lift is upward.""" air_speed_mps: f64 sideslip_rad: f64 dynamic_pressure_pa: f64 cd: f64 cy: f64 cl: f64 cmz: f64 drag_n: f64 side_n: f64 lift_n: f64 front_lift_n: f64 rear_lift_n: f64 yaw_moment_nm: f64 invariant air_speed_mps >= 0.0 invariant dynamic_pressure_pa >= 0.0
pub struct AeroSurrogate: """Even-in-yaw drag and lift, odd-in-yaw side force and yaw moment, fitted to the panel solve.""" model_id: str cd0: f64 cd2: f64 cd4: f64 cy1: f64 cy3: f64 cl0: f64 cl2: f64 cl4: f64 cmz1: f64 cmz3: f64 fit_max_yaw_rad: f64 fit_nodes: int fit_rms_cd: f64 invariant len(model_id) > 0 invariant cd0 > 0.0 invariant fit_max_yaw_rad > 0.0 invariant fit_nodes >= 2 invariant fit_rms_cd >= 0.0
pub struct VehicleParameters: """Inertial and geometric parameters derived from the declared assembly, not typed in.""" mass_kg: f64 cg_to_front_axle_m: f64 cg_to_rear_axle_m: f64 cg_height_m: f64 yaw_inertia_kg_m2: f64 wheelbase_m: f64 front_mass_fraction: f64 track_m: f64 invariant mass_kg > 0.0 invariant cg_to_front_axle_m > 0.0 invariant cg_to_rear_axle_m > 0.0 invariant cg_height_m > 0.0 invariant yaw_inertia_kg_m2 > 0.0 invariant wheelbase_m > 0.0 invariant front_mass_fraction > 0.0 and front_mass_fraction < 1.0 invariant track_m > 0.0
pub struct AeroHoldout: active_training_error: f64 candidate_training_error: f64 active_validation_error: f64 candidate_validation_error: f64 invariant active_training_error >= 0.0 invariant candidate_training_error >= 0.0 invariant active_validation_error >= 0.0 invariant candidate_validation_error >= 0.0
pub struct CornerSuspension: """One wheel station read out of the declared double-wishbone closure.""" vertical_load_n: f64 wheel_travel_m: f64 camber_deg: f64 toe_deg: f64 damper_length_m: f64 spring_force_n: f64 travel_utilisation: f64 invariant vertical_load_n >= 0.0 invariant damper_length_m > 0.0 invariant travel_utilisation >= 0.0 and travel_utilisation <= 1.0
pub struct SuspensionCorners: """The four wheel stations, each solved at its own vertical load.
Lateral load transfer moves normal force onto the outside pair, so the closure returns different travel, camber and toe per side. With no lateral acceleration the two sides are identical. """ front_left: CornerSuspension front_right: CornerSuspension rear_left: CornerSuspension rear_right: CornerSuspension
pub struct SuspensionTable: """Corner closures pre-solved on a load grid, in strictly increasing load order.""" loads_n: list[f64] corners: list[CornerSuspension] invariant len(loads_n) == len(corners) invariant len(loads_n) >= 2
pub struct AxleLongitudinal: """One axle's longitudinal state: what its hardware could do, and what the tyres allowed.
Both the brake and the drive side report which declared component set the number. At the tyre limit a stop is mass-independent; the moment the caliper is the binding constraint it is not, and the difference is visible here rather than folded into one force. """ axle: str normal_force_n: f64 friction_coefficient: f64 tire_limit_n: f64 brake_capability_n: f64 brake_force_n: f64 brake_constraint: str motor_force_n: f64 power_force_n: f64 drive_force_n: f64 drive_constraint: str cornering_stiffness_n_rad: f64 disc_temp_c: f64 fade_fraction: f64 invariant len(axle) > 0 invariant normal_force_n >= 0.0 invariant friction_coefficient > 0.0 invariant tire_limit_n >= 0.0 invariant brake_capability_n >= 0.0 invariant brake_force_n >= 0.0 invariant drive_force_n >= 0.0 invariant cornering_stiffness_n_rad >= 0.0 invariant fade_fraction > 0.0 and fade_fraction <= 1.0 invariant len(brake_constraint) > 0 and len(drive_constraint) > 0
struct LongitudinalDemand: """The live inputs one longitudinal solve needs, flattened out of the state and the aero solve.
Built once per step and handed to the four axle solves. Everything on it is a scalar so that the per-call copy the interpreter makes stays cheap. """ throttle: f64 brake: f64 power_limit: f64 speed_mps: f64 previous_acceleration_mps2: f64 front_disc_temp_c: f64 rear_disc_temp_c: f64 front_lift_n: f64 rear_lift_n: f64 lift_n: f64 drag_n: f64 invariant throttle >= 0.0 and throttle <= 1.0 invariant brake >= 0.0 and brake <= 1.0 invariant power_limit >= 0.0 and power_limit <= 1.0 invariant speed_mps >= 0.0
struct LongitudinalSolution: """Both axles solved together under one load transfer, with the resulting body acceleration.""" front: AxleLongitudinal rear: AxleLongitudinal rolling_force_n: f64 brake_line_pressure_pa: f64 brake_force_n: f64 drive_force_n: f64 motor_torque_nm: f64 acceleration_mps2: f64 limiting_constraint: str invariant rolling_force_n >= 0.0 invariant brake_line_pressure_pa >= 0.0 invariant brake_force_n >= 0.0 invariant drive_force_n >= 0.0 invariant motor_torque_nm >= 0.0 invariant len(limiting_constraint) > 0
pub struct PhysicsContext: """Everything expensive is built once here: the panel basis, the mass budget and both surrogates.""" basis: AeroBasis parameters: VehicleParameters calm: AeroSurrogate crosswind: AeroSurrogate holdout: AeroHoldout suspension: SuspensionTable hardware: HardwareLimits
pub struct DynamicsFrame: state: VehicleState controls: ControlInput sequence: int apparent_air_speed_mps: f64 sideslip_rad: f64 dynamic_pressure_pa: f64 solved: AeroLoads surrogate: AeroLoads model_cd: f64 reference_cd: f64 cd_pressure: f64 cd_base: f64 cd_friction: f64 cd_wheels: f64 drag_force_n: f64 lift_force_n: f64 side_force_n: f64 aero_yaw_moment_nm: f64 rolling_force_n: f64 brake_force_n: f64 regenerative_force_n: f64 drive_force_n: f64 wheel_torque_nm: f64 tire_slip_ratio: f64 front_axle_normal_force_n: f64 rear_axle_normal_force_n: f64 tire_utilization_fraction: f64 front_axle: AxleLongitudinal rear_axle: AxleLongitudinal limiting_constraint: str brake_line_pressure_pa: f64 brake_fade_fraction: f64 front_disc_temp_c: f64 rear_disc_temp_c: f64 suspension: SuspensionCorners mechanical_power_kw: f64 electrical_power_kw: f64 regenerative_power_kw: f64 battery_power_kw: f64 battery_loss_kw: f64 motor_loss_kw: f64 inverter_loss_kw: f64 pack_open_circuit_voltage_v: f64 pack_voltage_v: f64 pack_current_a: f64 pack_resistance_ohm: f64 power_limit_fraction: f64 power_residual_kw: f64 aero_residual_fraction: f64 reynolds_number: f64 separation_x_m: f64 cp_min: f64 cp_max: f64 base_pressure_coefficient: f64 wake_width_m: f64 wake_shedding_hz: f64 turbulence_intensity: f64 wake_velocity_deficit_fraction: f64 wake_recirculation_length_m: f64 dalembert_residual_cd: f64 aero_solver_residual: f64 aero_force_closure_residual: f64 thermal_energy_residual_w: f64 energy_residual_fraction: f64 invariant sequence >= 0 invariant apparent_air_speed_mps >= 0.0 invariant dynamic_pressure_pa >= 0.0 invariant rolling_force_n >= 0.0 invariant brake_force_n >= 0.0 invariant regenerative_force_n >= 0.0 invariant drive_force_n >= 0.0 invariant tire_slip_ratio >= 0.0 invariant front_axle_normal_force_n >= 0.0 invariant rear_axle_normal_force_n >= 0.0 invariant tire_utilization_fraction >= 0.0 invariant len(limiting_constraint) > 0 invariant brake_line_pressure_pa >= 0.0 invariant brake_fade_fraction > 0.0 and brake_fade_fraction <= 1.0 invariant front_disc_temp_c >= -40.0 and front_disc_temp_c <= MAX_DISC_TEMP_C invariant rear_disc_temp_c >= -40.0 and rear_disc_temp_c <= MAX_DISC_TEMP_C invariant regenerative_power_kw >= 0.0 invariant battery_loss_kw >= 0.0 invariant motor_loss_kw >= 0.0 invariant inverter_loss_kw >= 0.0 invariant pack_open_circuit_voltage_v > 0.0 invariant pack_voltage_v > 0.0 invariant pack_resistance_ohm > 0.0 invariant power_limit_fraction >= 0.0 and power_limit_fraction <= 1.0 invariant aero_residual_fraction >= 0.0 invariant reynolds_number >= 0.0 invariant wake_shedding_hz >= 0.0 invariant turbulence_intensity >= 0.0 and turbulence_intensity <= 1.0 invariant wake_velocity_deficit_fraction >= 0.0 and wake_velocity_deficit_fraction <= 1.0 invariant wake_recirculation_length_m >= 0.0 invariant energy_residual_fraction >= 0.0
AIR_DENSITY_KG_M3 = 1.225FRONTAL_AREA_M2 = 2.48SIDE_AREA_M2 = 4.32PLANFORM_AREA_M2 = 7.95VEHICLE_LENGTH_M = 4.751GRAVITY_MPS2 = 9.80665MOTOR_EFFICIENCY = 0.95INVERTER_EFFICIENCY = 0.975AUXILIARY_POWER_KW = 0.9MAX_DISC_TEMP_C = 1200.0FRONT_MOTOR_LOSS_SHARE = 0.42AMBIENT_TEMP_C = 24.0TIRE_THERMAL_CAPACITY_KJ_K = 28.0TIRE_COOLING_KW_PER_K = 0.08CALM_FIT_MAX_DEG = 8.0CROSSWIND_FIT_MAX_DEG = 35.0CALM_FIT_NODES = 5CROSSWIND_FIT_NODES = 8SURROGATE_FIT_SPEED_MPS = 27.8SUSPENSION_TABLE_NODES = 48SUSPENSION_MAX_LOAD_N = 40000.0
equation aerodynamic_force(air_density, coefficient, reference_area, relative_speed) -> any: return 0.5 * air_density * coefficient * reference_area * relative_speed^2
equation dynamic_pressure(air_density, speed) -> any: return 0.5 * air_density * speed^2
equation traction_force(motor_torque, gear_ratio, efficiency, wheel_radius) -> any: return motor_torque * gear_ratio * efficiency / wheel_radius
equation longitudinal_acceleration(drive_force, drag_force, rolling_force, brake_force, mass) -> any: return (drive_force - drag_force - rolling_force - brake_force) / mass
equation battery_soc_step(soc, electrical_power_kw, duration_s, capacity_kwh) -> any: return soc - electrical_power_kw * duration_s / (capacity_kwh * 3600.0)
equation battery_open_circuit_voltage(soc, temperature_c) -> any: return 700.0 + 150.0 * soc - max(0.0, 15.0 - temperature_c) * 0.7
equation battery_internal_resistance(base_resistance, soc, temperature_c) -> any: return base_resistance * (1.0 + max(0.0, 0.2 - soc) * 2.5 + max(0.0, 15.0 - temperature_c) * 0.018)
equation lumped_thermal_step(temperature_c, heat_kw, cooling_kw_per_k, ambient_c, thermal_capacity_kj_k, duration_s) -> any: return temperature_c + (heat_kw - cooling_kw_per_k * (temperature_c - ambient_c)) * duration_s / thermal_capacity_kj_k
equation lateral_acceleration(front_force, rear_force, aero_side_force, speed, yaw_rate, mass) -> any: return (front_force + rear_force + aero_side_force) / mass - speed * yaw_rate
equation yaw_acceleration(front_force, rear_force, aero_yaw_moment, front_arm, rear_arm, yaw_inertia) -> any: return (front_force * front_arm - rear_force * rear_arm + aero_yaw_moment) / yaw_inertia
equation normal_equations_2x2(s11, s12, s22, r1, r2) -> any: return solve([[s11, s12], [s12, s22]], [r1, r2])
equation axle_load_transfer(mass, acceleration, cg_height, wheelbase) -> any: return mass * acceleration * cg_height / wheelbase
equation disc_cooling_conductance(base_w_per_k, speed_w_per_k_mps, speed) -> any: return base_w_per_k + speed_w_per_k_mps * speed
def vehicle_parameters(): """Inertia and geometry summed from the declared part placements in `assembly.sema`.""" mass = mass_properties() return VehicleParameters( mass_kg=mass.total_mass_kg, cg_to_front_axle_m=0.5 * mass.wheelbase_m - mass.cg_x_m, cg_to_rear_axle_m=0.5 * mass.wheelbase_m + mass.cg_x_m, cg_height_m=mass.cg_height_m, yaw_inertia_kg_m2=mass.izz_kg_m2, wheelbase_m=mass.wheelbase_m, front_mass_fraction=mass.front_mass_fraction, track_m=2.0 * corner_pose(0.0)[5], )
def least_squares_pair(u: list[f64], y: list[f64]) !{}: """Fit y ~ a*u + b*u^2 through the origin by the 2x2 normal equations.""" require len(u) == len(y) and len(u) >= 2 mut s11 = 0.0 mut s12 = 0.0 mut s22 = 0.0 mut r1 = 0.0 mut r2 = 0.0 for i in range(0, len(u)): a = u[i] b = a * a s11 = s11 + a * a s12 = s12 + a * b s22 = s22 + b * b r1 = r1 + a * y[i] r2 = r2 + b * y[i] if s11 * s22 - s12 * s12 <= 0.0: return [0.0, 0.0] coefficients = normal_equations_2x2(s11, s12, s22, r1, r2) return [coefficients[0], coefficients[1]]
def least_squares_single(u: list[f64], y: list[f64]): """Fit y ~ a*u through the origin.""" require len(u) == len(y) and len(u) >= 1 mut numerator = 0.0 mut denominator = 0.0 for i in range(0, len(u)): numerator = numerator + u[i] * y[i] denominator = denominator + u[i] * u[i] if denominator <= 0.0: return 0.0 return numerator / denominator
def yaw_fit_nodes(count: int, maximum_deg: f64): """Yaw stations in radians, evenly spaced from zero to the declared fit boundary.""" require count >= 2 mut nodes = [] for i in range(0, count): nodes = nodes + [maximum_deg * float(i) / float(count - 1) * math.pi / 180.0] return nodes
def fit_surrogate(basis: AeroBasis, model_id: str, count: int, maximum_deg: f64, quartic: bool) !{}: """Least-squares fit of the panel solve over a bounded yaw window.
Drag and lift are even in sideslip, side force and yaw moment are odd, so the basis functions are beta^2/beta^4 and beta/beta^3 respectively. The zero-yaw intercepts are taken exactly from the solve so the calibrated `C_d(0)` is preserved without relying on the fit. """ require count >= 2 nodes = yaw_fit_nodes(count, maximum_deg) zero = aero_solve(basis, SURROGATE_FIT_SPEED_MPS, 0.0) mut even = [] mut odd = [] mut drag_residual = [] mut lift_residual = [] mut side = [] mut moment = [] for beta in nodes: forces = aero_solve(basis, SURROGATE_FIT_SPEED_MPS, beta) even = even + [beta * beta] odd = odd + [beta] drag_residual = drag_residual + [forces.cd - zero.cd] lift_residual = lift_residual + [forces.cl - zero.cl] side = side + [forces.cy] moment = moment + [forces.cmz] mut cd2 = 0.0 mut cd4 = 0.0 mut cl2 = 0.0 mut cl4 = 0.0 mut cy1 = 0.0 mut cy3 = 0.0 mut cmz1 = 0.0 mut cmz3 = 0.0 if quartic: drag = least_squares_pair(even, drag_residual) lift = least_squares_pair(even, lift_residual) lateral = least_squares_pair(odd, side) yaw = least_squares_pair(odd, moment) cd2 = drag[0] cd4 = drag[1] cl2 = lift[0] cl4 = lift[1] cy1 = lateral[0] cy3 = lateral[1] cmz1 = yaw[0] cmz3 = yaw[1] else: cd2 = least_squares_single(even, drag_residual) cl2 = least_squares_single(even, lift_residual) cy1 = least_squares_single(odd, side) cmz1 = least_squares_single(odd, moment) mut squared = 0.0 for i in range(0, len(nodes)): u = even[i] predicted = cd2 * u + cd4 * u * u error = predicted - drag_residual[i] squared = squared + error * error return AeroSurrogate( model_id=model_id, cd0=zero.cd, cd2=cd2, cd4=cd4, cy1=cy1, cy3=cy3, cl0=zero.cl, cl2=cl2, cl4=cl4, cmz1=cmz1, cmz3=cmz3, fit_max_yaw_rad=maximum_deg * math.pi / 180.0, fit_nodes=count, fit_rms_cd=math.sqrt(squared / float(len(nodes))), )
def surrogate_loads(surrogate: AeroSurrogate, air_speed_mps: f64, sideslip_rad: f64) !{}: """Evaluate the reduced-order coefficients. Cheap enough to run inside any control loop.""" speed = max(0.0, air_speed_mps) q = dynamic_pressure(AIR_DENSITY_KG_M3, speed) even = sideslip_rad * sideslip_rad cd = surrogate.cd0 + surrogate.cd2 * even + surrogate.cd4 * even * even cl = surrogate.cl0 + surrogate.cl2 * even + surrogate.cl4 * even * even cy = surrogate.cy1 * sideslip_rad + surrogate.cy3 * sideslip_rad * even cmz = surrogate.cmz1 * sideslip_rad + surrogate.cmz3 * sideslip_rad * even lift = cl * q * PLANFORM_AREA_M2 return AeroLoads( air_speed_mps=speed, sideslip_rad=sideslip_rad, dynamic_pressure_pa=q, cd=cd, cy=cy, cl=cl, cmz=cmz, drag_n=cd * q * FRONTAL_AREA_M2, side_n=cy * q * SIDE_AREA_M2, lift_n=lift, front_lift_n=0.5 * lift, rear_lift_n=0.5 * lift, yaw_moment_nm=cmz * q * SIDE_AREA_M2 * VEHICLE_LENGTH_M, )
def solved_loads(forces: AeroForces): return AeroLoads( air_speed_mps=forces.air_speed_mps, sideslip_rad=forces.yaw_rad, dynamic_pressure_pa=forces.dynamic_pressure_pa, cd=forces.cd, cy=forces.cy, cl=forces.cl, cmz=forces.cmz, drag_n=forces.drag_n, side_n=forces.side_n, lift_n=forces.lift_n, front_lift_n=forces.front_lift_n, rear_lift_n=forces.rear_lift_n, yaw_moment_nm=forces.yaw_moment_nm, )
def surrogate_for(context: PhysicsContext, model_id: str): return context.crosswind if model_id == CROSSWIND_MODEL_ID else context.calm
def relative_drag_error(basis: AeroBasis, surrogate: AeroSurrogate, degrees: list[f64]) !{}: """Mean relative drag-coefficient error of a surrogate against the panel solve.""" require len(degrees) >= 1 mut total = 0.0 for value in degrees: beta = value * math.pi / 180.0 reference = aero_solve(basis, SURROGATE_FIT_SPEED_MPS, beta) predicted = surrogate_loads(surrogate, SURROGATE_FIT_SPEED_MPS, beta) total = total + abs(reference.cd - predicted.cd) / max(0.001, abs(reference.cd)) return total / float(len(degrees))
pub def physics_context() -> PhysicsContext !{}: """Build the panel basis, the mass budget and both surrogates. Call once at startup.""" basis = aero_basis() calm = fit_surrogate(basis, CALM_MODEL_ID, CALM_FIT_NODES, CALM_FIT_MAX_DEG, false) crosswind = fit_surrogate(basis, CROSSWIND_MODEL_ID, CROSSWIND_FIT_NODES, CROSSWIND_FIT_MAX_DEG, true) training = [4.0, 8.0] validation = [22.0, 32.0] return PhysicsContext( basis=basis, parameters=vehicle_parameters(), calm=calm, crosswind=crosswind, holdout=AeroHoldout( active_training_error=relative_drag_error(basis, calm, training), candidate_training_error=relative_drag_error(basis, crosswind, training), active_validation_error=relative_drag_error(basis, calm, validation), candidate_validation_error=relative_drag_error(basis, crosswind, validation), ), suspension=suspension_table(), hardware=hardware_limits(), )
pub def aero_holdout_metrics(context: PhysicsContext) -> dict[str, f64] !{}: """Training and held-out surrogate errors measured against the live panel solve.""" return { "active_training_error": context.holdout.active_training_error, "candidate_training_error": context.holdout.candidate_training_error, "active_validation_error": context.holdout.active_validation_error, "candidate_validation_error": context.holdout.candidate_validation_error, "invariant_violations": 0.0, }
pub def initial_vehicle_state() -> VehicleState !{}: temps = thermal_initial_temps() return VehicleState( time_s=0.0, distance_m=0.0, speed_mps=0.0, lateral_speed_mps=0.0, acceleration_mps2=0.0, lateral_acceleration_mps2=0.0, yaw_rate_rps=0.0, yaw_angle_rad=0.0, steering_angle_rad=0.0, battery_soc=0.82, thermal_temps_c=temps, battery_temp_c=temps[0], motor_temp_c=max(temps[1], temps[2]), inverter_temp_c=max(temps[3], temps[4]), coolant_temp_c=temps[6], cabin_temp_c=temps[5], tire_temp_c=AMBIENT_TEMP_C, front_disc_temp_c=AMBIENT_TEMP_C, rear_disc_temp_c=AMBIENT_TEMP_C, motor_rpm=0.0, model_id=CALM_MODEL_ID, )
def state_at(speed_mps: f64, battery_soc: f64): """A settled state at one road speed and state of charge, with cold discs and cold tyres.""" base = initial_vehicle_state() return VehicleState( time_s=base.time_s, distance_m=base.distance_m, speed_mps=speed_mps, lateral_speed_mps=base.lateral_speed_mps, acceleration_mps2=base.acceleration_mps2, lateral_acceleration_mps2=base.lateral_acceleration_mps2, yaw_rate_rps=base.yaw_rate_rps, yaw_angle_rad=base.yaw_angle_rad, steering_angle_rad=base.steering_angle_rad, battery_soc=battery_soc, thermal_temps_c=base.thermal_temps_c, battery_temp_c=base.battery_temp_c, motor_temp_c=base.motor_temp_c, inverter_temp_c=base.inverter_temp_c, coolant_temp_c=base.coolant_temp_c, cabin_temp_c=base.cabin_temp_c, tire_temp_c=base.tire_temp_c, front_disc_temp_c=base.front_disc_temp_c, rear_disc_temp_c=base.rear_disc_temp_c, motor_rpm=base.motor_rpm, model_id=base.model_id, )
def state_carrying(state: VehicleState, speed_mps: f64): """The same thermal and electrical state brought back to speed, for a repeated-stop sequence.
Only the motion resets. Disc, tyre, pack and coolant temperatures carry over, which is the whole point: the heat a stop leaves behind is what the next one starts from. """ return VehicleState( time_s=state.time_s, distance_m=state.distance_m, speed_mps=speed_mps, lateral_speed_mps=0.0, acceleration_mps2=0.0, lateral_acceleration_mps2=0.0, yaw_rate_rps=0.0, yaw_angle_rad=state.yaw_angle_rad, steering_angle_rad=0.0, battery_soc=state.battery_soc, thermal_temps_c=state.thermal_temps_c, battery_temp_c=state.battery_temp_c, motor_temp_c=state.motor_temp_c, inverter_temp_c=state.inverter_temp_c, coolant_temp_c=state.coolant_temp_c, cabin_temp_c=state.cabin_temp_c, tire_temp_c=state.tire_temp_c, front_disc_temp_c=state.front_disc_temp_c, rear_disc_temp_c=state.rear_disc_temp_c, motor_rpm=state.motor_rpm, model_id=state.model_id, )
def with_added_mass(context: PhysicsContext, added_kg: f64): """The same vehicle carrying more mass: everything else declared stays exactly as it is.
Only the inertial parameters move. The motors, calipers, discs and tyres are the ones the parts list declares, which is the whole point of the comparison: what does the declared hardware do when it has more mass to move and to stop. """ parameters = context.parameters return PhysicsContext( basis=context.basis, parameters=VehicleParameters( mass_kg=parameters.mass_kg + added_kg, cg_to_front_axle_m=parameters.cg_to_front_axle_m, cg_to_rear_axle_m=parameters.cg_to_rear_axle_m, cg_height_m=parameters.cg_height_m, yaw_inertia_kg_m2=parameters.yaw_inertia_kg_m2 * (parameters.mass_kg + added_kg) / parameters.mass_kg, wheelbase_m=parameters.wheelbase_m, front_mass_fraction=parameters.front_mass_fraction, track_m=parameters.track_m, ), calm=context.calm, crosswind=context.crosswind, holdout=context.holdout, suspension=context.suspension, hardware=context.hardware, )
def apparent_wind(state: VehicleState, controls: ControlInput): """Apparent-wind magnitude and sideslip in the vehicle frame.
A head-on tunnel wind and forward road speed add; `wind_yaw_deg` rotates the tunnel wind around the vehicle, so a parked car in a 27.8 m/s stream reports 27.8 m/s of apparent wind at that yaw. """ yaw = controls.wind_yaw_deg * math.pi / 180.0 axial = state.speed_mps + controls.wind_mps * math.cos(yaw) lateral = state.lateral_speed_mps + controls.wind_mps * math.sin(yaw) speed = math.sqrt(axial * axial + lateral * lateral) sideslip = math.atan2(lateral, axial) if speed > 0.01 else 0.0 return [speed, sideslip]
def corner_state(state: SuspensionState): """Narrow a solved corner to the fields the dynamics frame publishes.""" return CornerSuspension( vertical_load_n=state.vertical_load_n, wheel_travel_m=state.wheel_travel_m, camber_deg=state.camber_deg, toe_deg=state.toe_deg, damper_length_m=state.damper_length_m, spring_force_n=state.spring_force_n, travel_utilisation=state.travel_utilisation, )
def suspension_table(): """Pre-solve the declared corner closure on a grid uniform in lower-arm rotation.
`suspension_travel` bisects the arm rotation against the polytropic air spring, which costs far more than the whole rest of a 20 Hz step. Sampling it once at startup and interpolating keeps the loop inside its budget without inventing a second suspension model. Rotation-uniform spacing puts the nodes where the linkage actually moves: the air spring stiffens so sharply that a load-uniform grid would collapse the working range into a couple of intervals. Rotations that clamp to the same load at a stop are dropped, so the lookup never meets a zero-width interval. """ limits = arm_rotation_limits() pressure = air_spring_reference_pressure() mut loads = [] mut corners = [] for index in range(0, SUSPENSION_TABLE_NODES): rotation = limits[0] + (limits[1] - limits[0]) * f64(index) / f64(SUSPENSION_TABLE_NODES - 1) load = max(0.0, min(SUSPENSION_MAX_LOAD_N, wheel_force(rotation, pressure))) if len(loads) == 0 or load > loads[len(loads) - 1]: loads.append(load) corners.append(corner_state(suspension_travel(load))) return SuspensionTable(loads_n=loads, corners=corners)
def interpolate_corner(table: SuspensionTable, vertical_load_n: f64): """Read the pre-solved corner grid at one vertical wheel load, linearly between nodes.""" load = max(0.0, min(SUSPENSION_MAX_LOAD_N, vertical_load_n)) mut index = 1 while index < len(table.loads_n) - 1 and table.loads_n[index] < load: index = index + 1 lower = table.corners[index - 1] upper = table.corners[index] span = table.loads_n[index] - table.loads_n[index - 1] blend = max(0.0, min(1.0, (load - table.loads_n[index - 1]) / span)) return CornerSuspension( vertical_load_n=load, wheel_travel_m=lower.wheel_travel_m + (upper.wheel_travel_m - lower.wheel_travel_m) * blend, camber_deg=lower.camber_deg + (upper.camber_deg - lower.camber_deg) * blend, toe_deg=lower.toe_deg + (upper.toe_deg - lower.toe_deg) * blend, damper_length_m=lower.damper_length_m + (upper.damper_length_m - lower.damper_length_m) * blend, spring_force_n=lower.spring_force_n + (upper.spring_force_n - lower.spring_force_n) * blend, travel_utilisation=lower.travel_utilisation + (upper.travel_utilisation - lower.travel_utilisation) * blend, )
def clamped_disc_temp(value: f64): """Hold an integrated disc temperature inside the range the state invariant declares.""" return max(-40.0, min(MAX_DISC_TEMP_C, value))
def axle_longitudinal(hardware: AxleHardware, demand: LongitudinalDemand, normal_force_n: f64, disc_temp_c: f64) !{}: """One axle solved against its own hardware and its own live vertical load.
The brake side walks the real chain: pedal demand sets line pressure, pressure and piston area set clamp force, pad friction and the effective radius set corner torque, and the rolling radius turns that into a wheel force. All of that is fixed by the declared components, so `hardware` arrives with it already solved and only the pedal fraction and the fade factor are applied here. The tyre side is the friction circle on this axle's live load, with the declared load sensitivity, so a heavier corner returns less grip per newton. Whichever of the two is smaller is the force the axle makes, and which one it was is published. """ load = max(0.0, normal_force_n) friction = tire_friction(hardware.peak_friction, hardware.slide_friction, hardware.load_sensitivity, hardware.friction_reference_load_n, 0.5 * load) grip = friction * load fade = brake_fade_factor(disc_temp_c, hardware.fade_onset_temp_c, hardware.fade_rate_per_k, hardware.fade_floor_fraction) capability = hardware.brake_axle_force_n * demand.brake * fade braking = min(capability, grip) motor_force = hardware.drive_force_per_throttle_n * demand.throttle * demand.power_limit # The power term is the machine's shaft corner power carried through the same driveline # efficiency as the torque term, so the two meet exactly at the base speed. Which component set # the current behind that envelope is already resolved into `power_constraint`. power_force = hardware.power_force_numerator_w * demand.power_limit / max(2.0, demand.speed_mps) headroom = max(0.0, grip - braking) drive = min(motor_force, min(power_force, headroom)) mut drive_constraint = "coasting" if demand.throttle > 0.0: drive_constraint = "tyre-grip" if motor_force <= power_force and motor_force <= headroom: drive_constraint = "motor-torque" elif power_force <= headroom: drive_constraint = hardware.power_constraint return AxleLongitudinal( axle=hardware.axle, normal_force_n=load, friction_coefficient=friction, tire_limit_n=grip, brake_capability_n=capability, brake_force_n=braking, brake_constraint="released" if demand.brake <= 0.0 else ("brake-torque" if capability < grip else "tyre-grip"), motor_force_n=motor_force, power_force_n=power_force, drive_force_n=drive, drive_constraint=drive_constraint, cornering_stiffness_n_rad=hardware.cornering_stiffness_per_n * load, disc_temp_c=disc_temp_c, fade_fraction=fade, )
def axle_pass(front_hardware: AxleHardware, rear_hardware: AxleHardware, demand: LongitudinalDemand, static_front_n: f64, static_rear_n: f64, transfer_n: f64) !{}: """Both axles solved at the load transfer one candidate acceleration implies.""" return [ axle_longitudinal(front_hardware, demand, static_front_n - transfer_n, demand.front_disc_temp_c), axle_longitudinal(rear_hardware, demand, static_rear_n + transfer_n, demand.rear_disc_temp_c), ]
def longitudinal_solution(parameters: VehicleParameters, hardware: HardwareLimits, demand: LongitudinalDemand) !{}: """Solve both axles and the body acceleration together, since each one sets the other.
Load transfer depends on the acceleration, and the acceleration depends on the per-axle limits that the transferred loads produce. One predictor from the previous step's acceleration and one corrector from this step's is enough at 20 Hz: the transfer moves by well under a percent over the second pass, and a fixed point is not worth the extra arithmetic inside the loop.
Nothing larger than a flat scalar struct crosses these call boundaries. The interpreter copies a struct argument by value, and `PhysicsContext` carries the whole panel basis, so handing it to a helper that runs four times a step costs milliseconds out of a fifty-millisecond budget. """ front_hardware = hardware.front_axle rear_hardware = hardware.rear_axle mass = parameters.mass_kg weight = mass * GRAVITY_MPS2 static_front = weight * parameters.cg_to_rear_axle_m / parameters.wheelbase_m - demand.front_lift_n static_rear = weight * parameters.cg_to_front_axle_m / parameters.wheelbase_m - demand.rear_lift_n coefficient = rolling_coefficient(hardware.tire.rolling_coefficient, hardware.tire.rolling_speed_coefficient, demand.speed_mps) rolling = coefficient * max(0.0, weight - demand.lift_n) if demand.speed_mps > 0.05 or demand.throttle > 0.0 else 0.0 predicted = axle_pass(front_hardware, rear_hardware, demand, static_front, static_rear, axle_load_transfer(mass, demand.previous_acceleration_mps2, parameters.cg_height_m, parameters.wheelbase_m)) trial = longitudinal_acceleration(predicted[0].drive_force_n + predicted[1].drive_force_n, demand.drag_n, rolling, predicted[0].brake_force_n + predicted[1].brake_force_n, mass) axles = axle_pass(front_hardware, rear_hardware, demand, static_front, static_rear, axle_load_transfer(mass, trial, parameters.cg_height_m, parameters.wheelbase_m)) front = axles[0] rear = axles[1] braking = front.brake_force_n + rear.brake_force_n drive = front.drive_force_n + rear.drive_force_n mut limiting = front.drive_constraint if front.drive_force_n >= rear.drive_force_n else rear.drive_constraint if braking > 0.0: limiting = front.brake_constraint if front.brake_force_n >= rear.brake_force_n else rear.brake_constraint return LongitudinalSolution( front=front, rear=rear, rolling_force_n=rolling, brake_line_pressure_pa=hardware.max_line_pressure_pa * demand.brake, brake_force_n=braking, drive_force_n=drive, motor_torque_nm=hardware.max_motor_torque_nm * demand.throttle * demand.power_limit, acceleration_mps2=longitudinal_acceleration(drive, demand.drag_n, rolling, braking, mass), limiting_constraint=limiting, )
pub def step_vehicle(state: VehicleState, controls: ControlInput, duration_s: f64, sequence: int, model_id: str, context: PhysicsContext) -> DynamicsFrame !{}: require duration_s == 0.05 require sequence >= 1 parameters = context.parameters mass = parameters.mass_kg wheelbase = parameters.wheelbase_m wind = apparent_wind(state, controls) apparent_air_speed = wind[0] sideslip = wind[1] forces = aero_solve(context.basis, apparent_air_speed, sideslip) solved = solved_loads(forces) surrogate = surrogate_loads(surrogate_for(context, model_id), apparent_air_speed, sideslip) pressure = forces.dynamic_pressure_pa drag = solved.drag_n lift = solved.lift_n aero_side_force = 0.0 - solved.side_n aero_yaw_moment = 0.0 - solved.yaw_moment_nm battery_derate = max(0.25, 1.0 - max(0.0, state.battery_temp_c - 55.0) / 55.0) motor_derate = max(0.30, 1.0 - max(0.0, state.motor_temp_c - 120.0) / 100.0) soc_derate = min(1.0, max(0.0, state.battery_soc / 0.12)) power_limit = min(battery_derate, min(motor_derate, soc_derate)) longitudinal = longitudinal_solution(parameters, context.hardware, LongitudinalDemand( throttle=controls.throttle * (1.0 - controls.brake), brake=controls.brake, power_limit=power_limit, speed_mps=state.speed_mps, previous_acceleration_mps2=state.acceleration_mps2, front_disc_temp_c=state.front_disc_temp_c, rear_disc_temp_c=state.rear_disc_temp_c, front_lift_n=solved.front_lift_n, rear_lift_n=solved.rear_lift_n, lift_n=lift, drag_n=drag, )) front_axle = longitudinal.front rear_axle = longitudinal.rear rolling = longitudinal.rolling_force_n brake_force = longitudinal.brake_force_n drive_force = longitudinal.drive_force_n motor_torque = longitudinal.motor_torque_nm requested_drive_force = front_axle.motor_force_n + rear_axle.motor_force_n tire_force_limit = front_axle.tire_limit_n + rear_axle.tire_limit_n acceleration = longitudinal.acceleration_mps2 next_speed = max(0.0, min(90.0, state.speed_mps + acceleration * duration_s)) average_speed = (state.speed_mps + next_speed) * 0.5 front_axle_load = front_axle.normal_force_n rear_axle_load = rear_axle.normal_force_n # Regeneration is a share of the same pedal demand, split across the axles by the continuous # torque each machine can generate and clamped to what that axle is actually braking with. The # discs only take the friction remainder, so a stop with a warm, receptive pack heats them less. charge_acceptance = min(1.0, max(0.0, (1.0 - state.battery_soc) / 0.02)) limits = context.hardware regenerative_request = min(brake_force, min(limits.max_regenerative_force_n, limits.max_regenerative_power_kw * 1000.0 / max(2.0, state.speed_mps))) * charge_acceptance front_regenerative = min(front_axle.brake_force_n, regenerative_request * limits.regenerative_front_share) rear_regenerative = min(rear_axle.brake_force_n, regenerative_request * (1.0 - limits.regenerative_front_share)) regenerative_force = front_regenerative + rear_regenerative disc_cooling_kw_per_k = disc_cooling_conductance(limits.disc_cooling_base_w_per_k, limits.disc_cooling_speed_w_per_k_mps, average_speed) / 1000.0 next_front_disc_temp = clamped_disc_temp(lumped_thermal_step(state.front_disc_temp_c, 0.5 * (front_axle.brake_force_n - front_regenerative) * average_speed / 1000.0, disc_cooling_kw_per_k, AMBIENT_TEMP_C, limits.front_brake.disc_thermal_capacity_j_k / 1000.0, duration_s)) next_rear_disc_temp = clamped_disc_temp(lumped_thermal_step(state.rear_disc_temp_c, 0.5 * (rear_axle.brake_force_n - rear_regenerative) * average_speed / 1000.0, disc_cooling_kw_per_k, AMBIENT_TEMP_C, limits.rear_brake.disc_thermal_capacity_j_k / 1000.0, duration_s)) mechanical_power = max(0.0, drive_force * average_speed / 1000.0) regenerative_mechanical_power = regenerative_force * average_speed / 1000.0 regenerative_power = regenerative_mechanical_power * limits.motor_efficiency * limits.inverter_efficiency traction_electrical_power = mechanical_power / (limits.motor_efficiency * limits.inverter_efficiency) auxiliary_power = AUXILIARY_POWER_KW * min(1.0, max(0.0, state.battery_soc / 0.01)) electric_power = traction_electrical_power + auxiliary_power - regenerative_power open_circuit_voltage = battery_open_circuit_voltage(state.battery_soc, state.battery_temp_c) pack_resistance = battery_internal_resistance(limits.pack.resistance_ohm, state.battery_soc, state.battery_temp_c) current_discriminant = max(1.0, open_circuit_voltage * open_circuit_voltage - 4.0 * pack_resistance * electric_power * 1000.0) pack_current = (open_circuit_voltage - math.sqrt(current_discriminant)) / (2.0 * pack_resistance) pack_voltage = open_circuit_voltage - pack_current * pack_resistance battery_loss = pack_current * pack_current * pack_resistance / 1000.0 battery_power = open_circuit_voltage * pack_current / 1000.0 next_soc = max(0.0, min(1.0, battery_soc_step(state.battery_soc, battery_power, duration_s, limits.pack.energy_kwh))) motor_loss = mechanical_power * (1.0 / limits.motor_efficiency - 1.0) + regenerative_mechanical_power * (1.0 - limits.motor_efficiency) inverter_loss = mechanical_power / limits.motor_efficiency * (1.0 / limits.inverter_efficiency - 1.0) + regenerative_mechanical_power * limits.motor_efficiency * (1.0 - limits.inverter_efficiency) heat_w = [ battery_loss * 1000.0, motor_loss * 1000.0 * FRONT_MOTOR_LOSS_SHARE, motor_loss * 1000.0 * (1.0 - FRONT_MOTOR_LOSS_SHARE), inverter_loss * 1000.0 * FRONT_MOTOR_LOSS_SHARE, inverter_loss * 1000.0 * (1.0 - FRONT_MOTOR_LOSS_SHARE), auxiliary_power * 1000.0 * 0.35, 0.0, 0.0, ] next_temps = thermal_step(state.thermal_temps_c, heat_w, AMBIENT_TEMP_C, duration_s) mut injected = 0.0 for value in heat_w: injected = injected + value mut stored = 0.0 for index in range(0, THERMAL_NODE_COUNT): stored = stored + (next_temps[index] - state.thermal_temps_c[index]) steering_angle = controls.steering * 0.42 speed_floor = max(2.0, next_speed) front_slip_angle = steering_angle - (state.lateral_speed_mps + parameters.cg_to_front_axle_m * state.yaw_rate_rps) / speed_floor rear_slip_angle = -(state.lateral_speed_mps - parameters.cg_to_rear_axle_m * state.yaw_rate_rps) / speed_floor total_axle_load = max(1.0, front_axle_load + rear_axle_load) front_longitudinal_force = front_axle.drive_force_n - front_axle.brake_force_n rear_longitudinal_force = rear_axle.drive_force_n - rear_axle.brake_force_n front_grip = front_axle.tire_limit_n rear_grip = rear_axle.tire_limit_n front_lateral_limit = math.sqrt(max(0.0, front_grip * front_grip - front_longitudinal_force * front_longitudinal_force)) rear_lateral_limit = math.sqrt(max(0.0, rear_grip * rear_grip - rear_longitudinal_force * rear_longitudinal_force)) front_tire_force = max(-front_lateral_limit, min(front_lateral_limit, front_axle.cornering_stiffness_n_rad * front_slip_angle)) rear_tire_force = max(-rear_lateral_limit, min(rear_lateral_limit, rear_axle.cornering_stiffness_n_rad * rear_slip_angle)) lateral_acceleration_value = lateral_acceleration(front_tire_force, rear_tire_force, aero_side_force, next_speed, state.yaw_rate_rps, mass) # The load transfer is driven by the total lateral force the contact patches react, not by the # body-frame velocity derivative: in a settled corner that derivative goes to zero while the car # is still leaning on its outside wheels. lateral_force_n = front_tire_force + rear_tire_force + aero_side_force lateral_transfer_n = lateral_force_n * parameters.cg_height_m / parameters.track_m front_transfer_n = lateral_transfer_n * parameters.front_mass_fraction rear_transfer_n = lateral_transfer_n * (1.0 - parameters.front_mass_fraction) corners = SuspensionCorners( front_left=interpolate_corner(context.suspension, front_axle_load * 0.5 - front_transfer_n), front_right=interpolate_corner(context.suspension, front_axle_load * 0.5 + front_transfer_n), rear_left=interpolate_corner(context.suspension, rear_axle_load * 0.5 - rear_transfer_n), rear_right=interpolate_corner(context.suspension, rear_axle_load * 0.5 + rear_transfer_n), ) next_lateral_speed = max(-40.0, min(40.0, state.lateral_speed_mps + lateral_acceleration_value * duration_s)) yaw_acceleration_value = yaw_acceleration(front_tire_force, rear_tire_force, aero_yaw_moment, parameters.cg_to_front_axle_m, parameters.cg_to_rear_axle_m, parameters.yaw_inertia_kg_m2) grip_yaw_rate_limit = min(3.0, 0.5 * (front_axle.friction_coefficient + rear_axle.friction_coefficient) * GRAVITY_MPS2 * total_axle_load / (mass * GRAVITY_MPS2 * max(2.0, next_speed))) next_yaw_rate = max(0.0 - grip_yaw_rate_limit, min(grip_yaw_rate_limit, state.yaw_rate_rps + yaw_acceleration_value * duration_s)) next_yaw_angle = state.yaw_angle_rad + (state.yaw_rate_rps + next_yaw_rate) * 0.5 * duration_s tire_slip = min(1.0, abs(front_slip_angle) + abs(rear_slip_angle) + max(0.0, requested_drive_force - tire_force_limit) / max(1.0, tire_force_limit)) front_tire_utilization = math.sqrt(front_longitudinal_force * front_longitudinal_force + front_tire_force * front_tire_force) / max(1.0, front_grip) rear_tire_utilization = math.sqrt(rear_longitudinal_force * rear_longitudinal_force + rear_tire_force * rear_tire_force) / max(1.0, rear_grip) tire_utilization = max(front_tire_utilization, rear_tire_utilization) tire_heat = rolling * average_speed / 1000.0 * 0.12 + brake_force * average_speed / 1000.0 * 0.08 + abs(lateral_acceleration_value) * average_speed * 0.03 next_tire_temp = lumped_thermal_step(state.tire_temp_c, tire_heat, TIRE_COOLING_KW_PER_K, AMBIENT_TEMP_C, TIRE_THERMAL_CAPACITY_KJ_K, duration_s) wheel_rps = next_speed / (2.0 * math.pi * limits.tire.rolling_radius_m) next_state = VehicleState( time_s=state.time_s + duration_s, distance_m=state.distance_m + average_speed * duration_s, speed_mps=next_speed, lateral_speed_mps=next_lateral_speed, acceleration_mps2=acceleration, lateral_acceleration_mps2=lateral_acceleration_value, yaw_rate_rps=next_yaw_rate, yaw_angle_rad=next_yaw_angle, steering_angle_rad=steering_angle, battery_soc=next_soc, thermal_temps_c=next_temps, battery_temp_c=next_temps[0], motor_temp_c=max(next_temps[1], next_temps[2]), inverter_temp_c=max(next_temps[3], next_temps[4]), coolant_temp_c=next_temps[6], cabin_temp_c=next_temps[5], tire_temp_c=next_tire_temp, front_disc_temp_c=next_front_disc_temp, rear_disc_temp_c=next_rear_disc_temp, motor_rpm=wheel_rps * 60.0 * limits.final_drive_ratio, model_id=model_id, ) aero_residual = abs(solved.cd - surrogate.cd) / max(0.001, abs(solved.cd)) power_residual = electric_power - pack_voltage * pack_current / 1000.0 energy_residual = abs(power_residual) / max(1.0, abs(electric_power)) return DynamicsFrame( state=next_state, controls=controls, sequence=sequence, apparent_air_speed_mps=apparent_air_speed, sideslip_rad=sideslip, dynamic_pressure_pa=pressure, solved=solved, surrogate=surrogate, model_cd=surrogate.cd, reference_cd=solved.cd, cd_pressure=forces.cd_pressure, cd_base=forces.cd_base, cd_friction=forces.cd_friction, cd_wheels=forces.cd_wheels, drag_force_n=drag, lift_force_n=lift, side_force_n=solved.side_n, aero_yaw_moment_nm=solved.yaw_moment_nm, rolling_force_n=rolling, brake_force_n=brake_force, regenerative_force_n=regenerative_force, drive_force_n=drive_force, wheel_torque_nm=motor_torque, tire_slip_ratio=tire_slip, front_axle_normal_force_n=front_axle_load, rear_axle_normal_force_n=rear_axle_load, tire_utilization_fraction=tire_utilization, front_axle=front_axle, rear_axle=rear_axle, limiting_constraint=longitudinal.limiting_constraint, brake_line_pressure_pa=longitudinal.brake_line_pressure_pa, brake_fade_fraction=min(front_axle.fade_fraction, rear_axle.fade_fraction), front_disc_temp_c=next_front_disc_temp, rear_disc_temp_c=next_rear_disc_temp, suspension=corners, mechanical_power_kw=mechanical_power, electrical_power_kw=electric_power, regenerative_power_kw=regenerative_power, battery_power_kw=battery_power, battery_loss_kw=battery_loss, motor_loss_kw=motor_loss, inverter_loss_kw=inverter_loss, pack_open_circuit_voltage_v=open_circuit_voltage, pack_voltage_v=pack_voltage, pack_current_a=pack_current, pack_resistance_ohm=pack_resistance, power_limit_fraction=power_limit, power_residual_kw=power_residual, aero_residual_fraction=aero_residual, reynolds_number=forces.reynolds_number, separation_x_m=forces.separation_x_m, cp_min=forces.cp_min, cp_max=forces.cp_max, base_pressure_coefficient=forces.base_pressure_coefficient, wake_width_m=forces.wake_width_m, wake_shedding_hz=forces.wake_shedding_hz, turbulence_intensity=forces.turbulence_intensity, wake_velocity_deficit_fraction=forces.wake_deficit_fraction, wake_recirculation_length_m=forces.wake_recirculation_length_m, dalembert_residual_cd=forces.dalembert_residual_cd, aero_solver_residual=forces.solver_residual, aero_force_closure_residual=forces.force_closure_residual, thermal_energy_residual_w=injected, energy_residual_fraction=energy_residual, )
struct StopMeasurement: """One full-pedal stop, integrated at the same 20 Hz the live loop runs at.""" distance_m: f64 duration_s: f64 mean_deceleration_mps2: f64 front_constraint: str rear_constraint: str front_disc_temp_c: f64 rear_disc_temp_c: f64 peak_brake_force_n: f64 invariant distance_m > 0.0 invariant duration_s > 0.0 invariant mean_deceleration_mps2 > 0.0
struct LaunchMeasurement: """One full-throttle launch to a target speed, integrated at the same 20 Hz.""" duration_s: f64 distance_m: f64 launch_constraint: str final_constraint: str peak_drive_force_n: f64 invariant duration_s > 0.0 invariant distance_m > 0.0
def measured_stop(context: PhysicsContext, initial_speed_mps: f64): """Brake from one speed to rest at full pedal and report what the vehicle actually did.""" require initial_speed_mps > 1.0 controls = ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0) start = state_at(initial_speed_mps, 0.82) mut frame = step_vehicle(start, controls, 0.05, 1, CALM_MODEL_ID, context) first = frame mut sequence = 1 while frame.state.speed_mps > 0.0 and sequence < 400: sequence = sequence + 1 frame = step_vehicle(frame.state, controls, 0.05, sequence, CALM_MODEL_ID, context) return StopMeasurement( distance_m=frame.state.distance_m, duration_s=frame.state.time_s, mean_deceleration_mps2=initial_speed_mps * initial_speed_mps / (2.0 * frame.state.distance_m), front_constraint=first.front_axle.brake_constraint, rear_constraint=first.rear_axle.brake_constraint, front_disc_temp_c=frame.state.front_disc_temp_c, rear_disc_temp_c=frame.state.rear_disc_temp_c, peak_brake_force_n=first.brake_force_n, )
def measured_launch(context: PhysicsContext, target_speed_mps: f64): """Accelerate from rest to one speed at full throttle and report what the hardware allowed.""" require target_speed_mps > 1.0 controls = ControlInput(throttle=1.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0) mut frame = step_vehicle(initial_vehicle_state(), controls, 0.05, 1, CALM_MODEL_ID, context) first = frame mut sequence = 1 while frame.state.speed_mps < target_speed_mps and sequence < 600: sequence = sequence + 1 frame = step_vehicle(frame.state, controls, 0.05, sequence, CALM_MODEL_ID, context) return LaunchMeasurement( duration_s=frame.state.time_s, distance_m=frame.state.distance_m, launch_constraint=first.limiting_constraint, final_constraint=frame.limiting_constraint, peak_drive_force_n=first.drive_force_n, )
pub def dynamics_equations() -> list[dict[str, str]] !{}: return [ {"id": "apparent-wind", "source": "apparent_wind", "expression": "V = |v_road + v_wind|; beta = atan2(V_y, V_x)", "unit": "m/s", "owner": "Sema vehicle-frame apparent wind (exact)"}, {"id": "aero-pressure", "source": "dynamic_pressure", "expression": "q = 0.5 rho |V_rel|^2", "unit": "Pa", "owner": "Sema aero (exact)"}, {"id": "aero-drag", "source": "aerodynamic_force", "expression": "F_d = q C_d(beta) A_frontal", "unit": "N", "owner": "Sema panel solve + viscous closure (solved+correlated)"}, {"id": "aero-surrogate", "source": "surrogate_loads", "expression": "C_d ~= C_d0 + c_2 beta^2 + c_4 beta^4; C_y ~= c_1 beta + c_3 beta^3", "unit": "1", "owner": "Sema reduced-order surrogate fitted to the panel solve (correlated)"}, {"id": "surrogate-fit", "source": "fit_surrogate", "expression": "min_c ||Phi c - (C(beta) - C(0))||_2 via the 2x2 normal equations", "unit": "1", "owner": "Sema least-squares fit (solved)"}, {"id": "traction", "source": "traction_force", "expression": "F_x = T_m i_g eta / r_w, with T_m from the declared machines and i_g from the declared reduction stages", "unit": "N", "owner": "Sema longitudinal dynamics over hardware.sema limits (exact on solved inputs)"}, {"id": "axle-drive-limit", "source": "axle_longitudinal", "expression": "F_x,axle = min(T_peak i_g eta / r_w, P_corner eta / v, mu(F_z) F_z - F_brake)", "unit": "N", "owner": "Sema per-axle drive limit; the binding term is published per axle (solved)"}, {"id": "axle-brake-chain", "source": "axle_longitudinal", "expression": "F_b,axle = min(2 T_corner(p_line, mu_pad(T_disc)) / r_w, mu(F_z) F_z)", "unit": "N", "owner": "Sema pedal-to-pressure-to-clamp-to-torque chain against the tyre friction circle (solved on correlated pad and tyre closures)"}, {"id": "brake-thermal", "source": "step_vehicle", "expression": "C_disc dT/dt = (F_b - F_regen) v / 2 - (h_0 + h_1 v)(T - T_amb)", "unit": "degC", "owner": "Sema per-disc lumped node; half m v squared less the recovered share (solved on a correlated cooling law)"}, {"id": "load-transfer-closure", "source": "longitudinal_solution", "expression": "one predictor at a_x(k-1) and one corrector at a_x(k) over Delta F_z = m a_x h_cg / L", "unit": "N", "owner": "Sema two-pass longitudinal closure; the axle limits and the acceleration each set the other (exact)"}, {"id": "rolling-resistance", "source": "rolling_coefficient", "expression": "F_rr = (f_0 + f_2 v^2) max(0, m g - F_lift)", "unit": "N", "owner": "Sema tyre rolling resistance on the live normal load, not on kerb weight (correlated)"}, {"id": "cornering-stiffness", "source": "axle_longitudinal", "expression": "C_alpha,axle = c_z F_z,axle", "unit": "N/rad", "owner": "Sema tyre slip stiffness per unit vertical load, so an axle stiffens as it is loaded (correlated)"}, {"id": "longitudinal", "source": "longitudinal_acceleration", "expression": "a_x = (F_x - F_d - F_rr - F_b) / m", "unit": "m/s^2", "owner": "Sema longitudinal dynamics (exact)"}, {"id": "mass-budget", "source": "vehicle_parameters", "expression": "m, r_cg, I_zz summed from the declared part placements", "unit": "kg, m, kg m^2", "owner": "Sema assembly mass budget (solved)"}, {"id": "axle-load-transfer", "source": "step_vehicle", "expression": "Delta F_z = m a_x h_cg / L; F_z,front = F_z,static - Delta F_z - F_lift,front", "unit": "N", "owner": "Sema reduced load-path model (exact)"}, {"id": "corner-load-split", "source": "step_vehicle", "expression": "Delta F_z,lat = (F_yf + F_yr + F_aero,y) h_cg / t, split front/rear by the static axle share; F_z,corner = F_z,axle / 2 -+ Delta F_z,lat", "unit": "N", "owner": "Sema steady-state algebraic lateral transfer at centre-of-gravity height; no roll-stiffness distribution and no transient (exact, uncorrelated)"}, {"id": "corner-closure", "source": "interpolate_corner", "expression": "travel, camber, toe and damper length read from the declared four-bar closure at F_z,corner", "unit": "m, deg", "owner": "Sema declared double-wishbone closure, startup-sampled and linearly interpolated (solved+interpolated)"}, {"id": "tire-friction-circle", "source": "step_vehicle", "expression": "F_y,max = sqrt((mu F_z)^2 - F_x^2)", "unit": "N", "owner": "Sema coupled tire limit (correlated)"}, {"id": "lateral", "source": "lateral_acceleration", "expression": "a_y = (F_yf + F_yr + F_aero,y) / m - u r", "unit": "m/s^2", "owner": "Sema reduced bicycle model (exact)"}, {"id": "yaw", "source": "yaw_acceleration", "expression": "yaw_accel = (a F_yf - b F_yr + N_aero) / I_z", "unit": "rad/s^2", "owner": "Sema reduced bicycle model with solved aero moment (exact+solved)"}, {"id": "battery-energy", "source": "battery_soc_step", "expression": "SoC_(k+1) = SoC_k - P_batt dt / E_pack", "unit": "fraction", "owner": "Sema energy model (exact)"}, {"id": "battery-voltage", "source": "battery_open_circuit_voltage", "expression": "V_terminal = V_oc(SoC,T) - I R(SoC,T)", "unit": "V", "owner": "Sema equivalent-circuit model (correlated)"}, {"id": "battery-terminal-power", "source": "step_vehicle", "expression": "P_terminal = I (V_oc - I R)", "unit": "kW", "owner": "Sema equivalent-circuit model (exact)"}, {"id": "thermal-network", "source": "thermal_step", "expression": "C_i dT_i/dt = Q_i + sum_j k_ij (T_j - T_i) - h_i (T_i - T_amb)", "unit": "degC", "owner": "Sema coupled eight-node coolant network (exact)"}, {"id": "thermal-tire", "source": "lumped_thermal_step", "expression": "T_(k+1) = T_k + (Q - h(T-T_amb)) dt / C_th", "unit": "degC", "owner": "Sema lumped tire node (correlated)"}, ]
test "a stationary vehicle in a wind tunnel still measures drag": context = physics_context() state = initial_vehicle_state() controls = ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=27.8, wind_yaw_deg=0.0) frame = step_vehicle(state, controls, 0.05, 1, CALM_MODEL_ID, context) ensure frame.state.speed_mps == 0.0 ensure frame.apparent_air_speed_mps > 27.7 and frame.apparent_air_speed_mps < 27.9 ensure frame.drag_force_n > 200.0 ensure frame.dynamic_pressure_pa > 400.0
test "wind yaw drives a signed side force and yaw moment that vanish head-on": context = physics_context() state = initial_vehicle_state() head_on = step_vehicle(state, ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=27.8, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) from_left = step_vehicle(state, ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=27.8, wind_yaw_deg=20.0), 0.05, 1, CALM_MODEL_ID, context) from_right = step_vehicle(state, ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=27.8, wind_yaw_deg=-20.0), 0.05, 1, CALM_MODEL_ID, context) ensure abs(head_on.side_force_n) < 0.001 * head_on.drag_force_n ensure abs(head_on.aero_yaw_moment_nm) < 0.001 * head_on.drag_force_n ensure from_left.side_force_n > 0.0 ensure abs(from_left.side_force_n + from_right.side_force_n) < 0.000001 * abs(from_left.side_force_n) ensure from_left.drag_force_n > head_on.drag_force_n ensure abs(from_left.drag_force_n - from_right.drag_force_n) < 0.000001 * from_left.drag_force_n ensure from_left.state.lateral_acceleration_mps2 < 0.0
test "vehicle step conserves the declared power balance and advances causal state": context = physics_context() state = initial_vehicle_state() controls = ControlInput(throttle=0.7, brake=0.0, steering=0.1, wind_mps=0.0, wind_yaw_deg=0.0) frame = step_vehicle(state, controls, 0.05, 1, CALM_MODEL_ID, context) ensure frame.state.time_s == 0.05 ensure frame.state.speed_mps > 0.0 ensure frame.drive_force_n > frame.drag_force_n ensure frame.energy_residual_fraction < 0.000000001 ensure frame.state.battery_soc < state.battery_soc
test "inertial parameters come from the declared assembly, not from literals": parameters = vehicle_parameters() ensure parameters.mass_kg > 1900.0 and parameters.mass_kg < 2300.0 ensure abs(parameters.cg_to_front_axle_m + parameters.cg_to_rear_axle_m - parameters.wheelbase_m) < 0.000000001 ensure parameters.cg_height_m > 0.45 and parameters.cg_height_m < 0.60 ensure parameters.yaw_inertia_kg_m2 > 2800.0 and parameters.yaw_inertia_kg_m2 < 4500.0 ensure parameters.front_mass_fraction > 0.47 and parameters.front_mass_fraction < 0.53 ensure parameters.track_m > 1.5 and parameters.track_m < 1.8 ensure parameters.track_m < parameters.wheelbase_m
test "the crosswind surrogate beats the calm surrogate outside the calm fit window": context = physics_context() holdout = aero_holdout_metrics(context) ensure context.calm.fit_rms_cd < 0.03 * context.calm.cd0 ensure context.crosswind.fit_rms_cd < 0.03 * context.crosswind.cd0 ensure holdout["active_validation_error"] > 0.10 ensure holdout["candidate_validation_error"] < 0.04 ensure holdout["candidate_validation_error"] < 0.25 * holdout["active_validation_error"]
test "surrogate coefficients respect the symmetry of the solved field": context = physics_context() left = surrogate_loads(context.crosswind, 27.8, 0.35) right = surrogate_loads(context.crosswind, 27.8, -0.35) ensure abs(left.cd - right.cd) < 0.000000001 ensure abs(left.cy + right.cy) < 0.000000001 ensure abs(left.cmz + right.cmz) < 0.000000001
test "aerodynamic loads scale with the square of the apparent wind": context = physics_context() state = initial_vehicle_state() slow = step_vehicle(state, ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=15.0, wind_yaw_deg=10.0), 0.05, 1, CALM_MODEL_ID, context) fast = step_vehicle(state, ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=30.0, wind_yaw_deg=10.0), 0.05, 1, CALM_MODEL_ID, context) ensure abs(fast.side_force_n - 4.0 * slow.side_force_n) < 0.000001 * abs(fast.side_force_n) ensure abs(fast.cd_pressure - slow.cd_pressure) < 0.000000000001 ensure abs(fast.cd_base - slow.cd_base) < 0.000000000001 ensure fast.cd_friction < slow.cd_friction ensure abs(fast.drag_force_n - 4.0 * slow.drag_force_n) < 0.02 * fast.drag_force_n
test "the coupled thermal network moves heat into the coolant loop": context = physics_context() mut state = initial_vehicle_state() mut frame = step_vehicle(state, ControlInput(throttle=1.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) for sequence in range(2, 60): state = frame.state frame = step_vehicle(state, ControlInput(throttle=1.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, sequence, CALM_MODEL_ID, context) ensure frame.state.battery_temp_c > 24.0 ensure frame.state.coolant_temp_c > 24.0 ensure frame.state.motor_temp_c > frame.state.coolant_temp_c ensure len(frame.state.thermal_temps_c) == THERMAL_NODE_COUNT ensure frame.energy_residual_fraction < 0.000000001
test "equivalent circuit exposes voltage sag losses and thermal coupling": context = physics_context() state = initial_vehicle_state() frame = step_vehicle(state, ControlInput(throttle=0.9, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) ensure frame.pack_voltage_v < frame.pack_open_circuit_voltage_v ensure frame.battery_loss_kw > 0.0 ensure frame.battery_power_kw > frame.electrical_power_kw coasting = step_vehicle(state, ControlInput(throttle=0.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) ensure frame.state.battery_temp_c > coasting.state.battery_temp_c ensure frame.energy_residual_fraction < 0.000000001
test "friction and regenerative braking share one bounded request": context = physics_context() mut state = initial_vehicle_state() mut frame = step_vehicle(state, ControlInput(throttle=0.8, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) for sequence in range(2, 22): state = frame.state frame = step_vehicle(state, ControlInput(throttle=0.8, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, sequence, CALM_MODEL_ID, context) braking = step_vehicle(frame.state, ControlInput(throttle=0.0, brake=0.8, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 22, CALM_MODEL_ID, context) ensure braking.regenerative_force_n > 0.0 ensure braking.regenerative_force_n <= braking.brake_force_n ensure braking.regenerative_power_kw > 0.0 ensure braking.state.acceleration_mps2 < 0.0
test "depleted battery cannot provide traction or auxiliary power": context = physics_context() depleted = state_at(0.0, 0.0) frame = step_vehicle(depleted, ControlInput(throttle=1.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) ensure frame.drive_force_n == 0.0 ensure frame.electrical_power_kw == 0.0 ensure frame.state.battery_soc == 0.0
test "the pre-sampled corner table reproduces the bisected suspension closure": context = physics_context() mut previous = -1.0 for load in context.suspension.loads_n: ensure load > previous previous = load for load in [1500.0, 3800.0, 4820.0, 5680.0, 7200.0, 11000.0, 18000.0]: solved = suspension_travel(load) read = interpolate_corner(context.suspension, load) ensure abs(read.wheel_travel_m - solved.wheel_travel_m) < 0.0003 ensure abs(read.camber_deg - solved.camber_deg) < 0.01 ensure abs(read.toe_deg - solved.toe_deg) < 0.005 ensure abs(read.damper_length_m - solved.damper_length_m) < 0.0005 ensure abs(read.spring_force_n - solved.spring_force_n) < 0.01 * solved.spring_force_n ensure len(context.suspension.loads_n) >= 24
test "a straight-line step loads both sides of an axle identically": context = physics_context() state = initial_vehicle_state() frame = step_vehicle(state, ControlInput(throttle=0.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) corners = frame.suspension ensure abs(corners.front_left.vertical_load_n - frame.front_axle_normal_force_n * 0.5) < 0.000001 ensure abs(corners.rear_left.vertical_load_n - frame.rear_axle_normal_force_n * 0.5) < 0.000001 ensure corners.front_left.vertical_load_n == corners.front_right.vertical_load_n ensure corners.rear_left.vertical_load_n == corners.rear_right.vertical_load_n heavier = corners.rear_left if corners.rear_left.vertical_load_n > corners.front_left.vertical_load_n else corners.front_left lighter = corners.front_left if corners.rear_left.vertical_load_n > corners.front_left.vertical_load_n else corners.rear_left ensure heavier.wheel_travel_m > lighter.wheel_travel_m ensure heavier.camber_deg < lighter.camber_deg ensure heavier.damper_length_m < lighter.damper_length_m ensure heavier.spring_force_n > lighter.spring_force_n ensure corners.front_left.travel_utilisation >= 0.0 and corners.front_left.travel_utilisation <= 1.0 ensure corners.rear_left.travel_utilisation >= 0.0 and corners.rear_left.travel_utilisation <= 1.0
test "cornering moves normal force onto the outside corners": context = physics_context() mut frame = step_vehicle(initial_vehicle_state(), ControlInput(throttle=0.6, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) for sequence in range(2, 40): frame = step_vehicle(frame.state, ControlInput(throttle=0.35, brake=0.0, steering=0.8, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, sequence, CALM_MODEL_ID, context) corners = frame.suspension total = corners.front_left.vertical_load_n + corners.front_right.vertical_load_n + corners.rear_left.vertical_load_n + corners.rear_right.vertical_load_n ensure frame.state.yaw_rate_rps > 0.01 ensure abs(total - frame.front_axle_normal_force_n - frame.rear_axle_normal_force_n) < 0.000000001 ensure corners.front_right.vertical_load_n - corners.front_left.vertical_load_n > 200.0 ensure corners.rear_right.vertical_load_n - corners.rear_left.vertical_load_n > 200.0 ensure corners.front_right.wheel_travel_m > corners.front_left.wheel_travel_m ensure corners.front_right.camber_deg < corners.front_left.camber_deg ensure corners.front_right.damper_length_m < corners.front_left.damper_length_m ensure corners.rear_right.travel_utilisation > corners.rear_left.travel_utilisation
test "the brakes out-torque the tyres at kerb mass, so a cold stop is grip limited": context = physics_context() limits = context.hardware parameters = context.parameters grip = limits.tire.peak_friction * parameters.mass_kg * GRAVITY_MPS2 ensure limits.max_brake_force_n > grip stop = measured_stop(context, 27.7778) ensure stop.front_constraint == "tyre-grip" ensure stop.rear_constraint == "tyre-grip" ensure stop.peak_brake_force_n < limits.max_brake_force_n
test "a 100-0 km/h stop lands in a credible band at close to mu times g": context = physics_context() stop = measured_stop(context, 27.7778) ensure stop.distance_m > 32.0 and stop.distance_m < 42.0 ensure stop.duration_s > 2.3 and stop.duration_s < 3.3 friction = context.hardware.tire.peak_friction ensure abs(stop.mean_deceleration_mps2 - friction * GRAVITY_MPS2) < 0.10 * friction * GRAVITY_MPS2 ensure stop.front_disc_temp_c > AMBIENT_TEMP_C + 20.0 ensure stop.front_disc_temp_c > stop.rear_disc_temp_c
test "0-100 km/h lands in a credible band for the derived torque and power": context = physics_context() launch = measured_launch(context, 27.7778) ensure launch.duration_s > 3.2 and launch.duration_s < 5.5 ensure launch.distance_m > 30.0 and launch.distance_m < 90.0 ensure launch.launch_constraint == "motor-torque" ensure launch.final_constraint == "inverter-power" or launch.final_constraint == "motor-power" torque = context.hardware.max_motor_torque_nm ceiling = torque * context.hardware.final_drive_ratio / context.hardware.tire.rolling_radius_m ensure launch.peak_drive_force_n > 0.85 * ceiling and launch.peak_drive_force_n < ceiling
test "mass lengthens the stop, heats the discs and cuts acceleration in proportion": context = physics_context() heavy = with_added_mass(context, 300.0) light_stop = measured_stop(context, 27.7778) heavy_stop = measured_stop(heavy, 27.7778) light_launch = measured_launch(context, 27.7778) heavy_launch = measured_launch(heavy, 27.7778) # At kerb both axles are grip limited, so the stop is very nearly mass independent. Add 300 kg # and the rear tyre asks for more than the rear caliper can make: from there the extra mass is # no longer paid for by extra grip, and the stop gets longer. ensure light_stop.rear_constraint == "tyre-grip" ensure heavy_stop.rear_constraint == "brake-torque" ensure heavy_stop.distance_m > light_stop.distance_m ensure heavy_stop.mean_deceleration_mps2 < light_stop.mean_deceleration_mps2 ensure heavy_stop.front_disc_temp_c > light_stop.front_disc_temp_c ensure heavy_stop.rear_disc_temp_c > light_stop.rear_disc_temp_c # The launch is motor limited, so a = F/m and the time stretches with the mass ratio. ratio = heavy.parameters.mass_kg / context.parameters.mass_kg ensure heavy_launch.duration_s > light_launch.duration_s ensure abs(heavy_launch.duration_s / light_launch.duration_s - ratio) < 0.05 * ratio
test "repeated stops heat the discs until the declared pad compound fades": context = physics_context() limits = context.hardware controls = ControlInput(throttle=0.0, brake=1.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0) mut frame = step_vehicle(state_at(27.7778, 0.82), controls, 0.05, 1, CALM_MODEL_ID, context) cold = frame.front_axle.brake_capability_n mut sequence = 1 # Brake to rest, put the car straight back to 100 km/h with the discs exactly as hot as it left # them, and repeat. Nothing else changes, so the only thing accumulating is disc temperature. while sequence < 900: sequence = sequence + 1 relaunched = frame.state if frame.state.speed_mps > 0.1 else state_carrying(frame.state, 27.7778) frame = step_vehicle(relaunched, controls, 0.05, sequence, CALM_MODEL_ID, context) ensure frame.state.front_disc_temp_c > limits.fade_onset_temp_c ensure frame.brake_fade_fraction < 1.0 ensure frame.front_axle.brake_capability_n < cold ensure frame.front_axle.brake_constraint == "brake-torque" ensure frame.brake_fade_fraction >= limits.fade_floor_fraction
test "every published limit is read from the declared hardware, not from a literal": context = physics_context() limits = context.hardware frame = step_vehicle(state_at(20.0, 0.82), ControlInput(throttle=1.0, brake=0.0, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) ensure frame.front_axle.axle == "front" and frame.rear_axle.axle == "rear" ensure frame.rear_axle.motor_force_n > frame.front_axle.motor_force_n ensure abs(frame.front_axle.cornering_stiffness_n_rad - limits.tire.cornering_stiffness_per_n * frame.front_axle_normal_force_n) < 0.000000001 ensure abs(frame.brake_line_pressure_pa) < 0.000000001 ensure frame.front_axle.brake_constraint == "released" and frame.rear_axle.brake_constraint == "released" ensure frame.limiting_constraint == frame.rear_axle.drive_constraint braking = step_vehicle(state_at(20.0, 0.82), ControlInput(throttle=0.0, brake=0.5, steering=0.0, wind_mps=0.0, wind_yaw_deg=0.0), 0.05, 1, CALM_MODEL_ID, context) ensure abs(braking.brake_line_pressure_pa - 0.5 * limits.max_line_pressure_pa) < 0.000000001 ensure braking.front_axle.brake_constraint == "brake-torque" ensure braking.energy_residual_fraction < 0.000000001src/vehicle.sema
Section titled “src/vehicle.sema”"""Automotive-native major-assembly vehicle, E/E, and manufacturing definition."""
from magna_ev_digital_twin.candidate import conductor_mass, conductor_resistancefrom magna_ev_digital_twin.domain import AutomationClass, CompletenessReport, EvidenceClass, EvidenceRecord, Fidelity, NetDefinition, OperationDefinition, PartDefinition, PartGroup, QualificationStatus, SourceAuthority, SourceBinding, SourceRecord, ValidationCaseDefinition, VehicleIdentity
assure silver
CONFIGURATION_ID = "circuitframe_gt_01"CONFIGURATION_REVISION = 1EVIDENCE_EXTERIOR = "model-y-community-reference-exterior-v1"EVIDENCE_CONCEPT = "engineered-gap-reconstruction-v1"EVIDENCE_ELECTRICAL = "circuitframe-electrical-v1"EVIDENCE_PROCESS = "robot-process-kinematic-v1"EXTERIOR_SOURCE_ID = "printables-model-1111101-stl-4641564"ASSUMPTION_SOURCE_ID = "source-circuitframe-engineered-gap-v1"TESLA_SERVICE_SOURCE_ID = "tesla-model-y-sop3-berlin-public-docs"OPEN_BMS_SOURCE_ID = "ennoid-bms-daf72c9"TABBY_CAD_SOURCE_ID = "tabby-evo-2015-open-cad"EVIDENCE_TESLA_SERVICE = "model-y-official-service-reference-v1"EVIDENCE_OPEN_BMS = "ennoid-open-bms-reference-v1"EVIDENCE_TABBY_CAD = "tabby-open-cad-candidate-v1"COPPER_RESISTIVITY_OHM_M = 0.0000000168COPPER_DENSITY_KG_M3 = 8960.0CONNECTOR_MASS_KG = 0.045SPLICE_MASS_KG = 0.012HARNESS_JACKET_FACTOR = 1.85STRUCTURAL_OVERPRINT_FACTOR = 1.25STRUCTURAL_CURRENT_DENSITY_A_MM2 = 7.0MINIMUM_CONDUCTOR_AREA_MM2 = 0.35
def variant_value(configuration_variant: str, conventional: str, circuitframe: str): require configuration_variant == "conventional" or configuration_variant == "circuitframe" return conventional if configuration_variant == "conventional" else circuitframe
pub def vehicle_identity() -> VehicleIdentity !{}: return VehicleIdentity( id=CONFIGURATION_ID, revision=CONFIGURATION_REVISION, name="CircuitFrame Model Y reference reconstruction", category="Evidence-aware electric crossover reconstruction", seats=5, fidelity=Fidelity.derived, )
pub def vehicle_parts() -> list[PartDefinition] !{}: return [ PartDefinition(id="body-shell", name="Painted body shell", group=PartGroup.body, count=1, material="aluminium/composite", mass_kg=236.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_EXTERIOR]), PartDefinition(id="floor-pan", name="Structural floor pan", group=PartGroup.chassis, count=1, material="aluminium extrusion and sheet", mass_kg=82.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="front-subframe", name="Front subframe", group=PartGroup.chassis, count=1, material="aluminium", mass_kg=31.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="rear-subframe", name="Rear subframe", group=PartGroup.chassis, count=1, material="aluminium", mass_kg=34.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="sill-panels", name="Variant-specific structural sill panels", group=PartGroup.electrical, count=2, material="configuration-specific aluminium/dielectric/conductor stack", mass_kg=26.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT, EVIDENCE_ELECTRICAL]), PartDefinition(id="battery-enclosure", name="Structural battery enclosure", group=PartGroup.energy, count=1, material="aluminium", mass_kg=118.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="battery-modules", name="Battery modules", group=PartGroup.energy, count=24, material="cell/module supplier assembly", mass_kg=438.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="battery-management", name="Battery management system", group=PartGroup.control, count=1, material="PCB/electronics", mass_kg=6.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="hv-junction", name="HV junction and protection unit", group=PartGroup.electrical, count=1, material="copper/polymer/electronics", mass_kg=13.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="charge-port", name="CCS charge port", group=PartGroup.energy, count=1, material="supplier assembly", mass_kg=4.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="onboard-charger", name="On-board charger", group=PartGroup.energy, count=1, material="power electronics", mass_kg=14.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="dc-dc", name="High-to-low voltage converter", group=PartGroup.energy, count=1, material="power electronics", mass_kg=8.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="front-inverter", name="Front inverter", group=PartGroup.drive, count=1, material="power electronics", mass_kg=11.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="rear-inverter", name="Rear inverter", group=PartGroup.drive, count=1, material="power electronics", mass_kg=12.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="front-motor", name="Front electric drive motor", group=PartGroup.drive, count=1, material="steel/copper/magnets", mass_kg=58.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="rear-motor", name="Rear electric drive motor", group=PartGroup.drive, count=1, material="steel/copper/magnets", mass_kg=71.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="reduction-gears", name="Single-speed reduction gears", group=PartGroup.drive, count=2, material="steel/aluminium", mass_kg=42.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="wheels", name="Aerodynamic alloy wheels", group=PartGroup.motion, count=4, material="forged aluminium", mass_kg=52.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_EXTERIOR, EVIDENCE_CONCEPT]), PartDefinition(id="tires", name="Road tires", group=PartGroup.motion, count=4, material="supplier tire assembly", mass_kg=48.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_EXTERIOR, EVIDENCE_CONCEPT]), PartDefinition(id="suspension", name="Air spring and multi-link suspension", group=PartGroup.motion, count=4, material="steel/aluminium/elastomer", mass_kg=104.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="steering", name="Electric steering system", group=PartGroup.motion, count=1, material="supplier mechatronics", mass_kg=23.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="brakes", name="Brake-by-wire corner modules", group=PartGroup.motion, count=4, material="steel/aluminium/electronics", mass_kg=76.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="thermal-loop", name="Battery and powertrain thermal loop", group=PartGroup.thermal, count=1, material="coolant/aluminium/polymer", mass_kg=31.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="heat-pump", name="Cabin and battery heat pump", group=PartGroup.thermal, count=1, material="supplier thermal assembly", mass_kg=24.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="front-zone-controller", name="Front zone controller", group=PartGroup.control, count=1, material="PCB/electronics", mass_kg=2.2, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="rear-zone-controller", name="Rear zone controller", group=PartGroup.control, count=1, material="PCB/electronics", mass_kg=2.2, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="cabin-zone-controller", name="Cabin zone controller", group=PartGroup.control, count=1, material="PCB/electronics", mass_kg=1.8, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="central-compute", name="Central vehicle compute", group=PartGroup.control, count=1, material="supplier compute assembly", mass_kg=5.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="lv-battery", name="Low-voltage battery", group=PartGroup.energy, count=1, material="lithium-ion assembly", mass_kg=12.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="sensors", name="Vehicle sensing suite", group=PartGroup.control, count=28, material="supplier sensors", mass_kg=18.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_ELECTRICAL]), PartDefinition(id="lamps", name="Exterior lighting", group=PartGroup.electrical, count=6, material="polymer/LED/electronics", mass_kg=16.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_EXTERIOR, EVIDENCE_CONCEPT]), PartDefinition(id="glazing", name="Laminated glazing", group=PartGroup.body, count=6, material="laminated glass", mass_kg=47.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_EXTERIOR, EVIDENCE_CONCEPT]), PartDefinition(id="closures", name="Doors and closures", group=PartGroup.body, count=6, material="aluminium/composite", mass_kg=112.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_EXTERIOR, EVIDENCE_CONCEPT]), PartDefinition(id="seats", name="Seats and occupant structures", group=PartGroup.cabin, count=5, material="steel/foam/textile", mass_kg=96.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="interior", name="Interior, HMI and trim", group=PartGroup.cabin, count=1, material="mixed interior system", mass_kg=118.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="restraints", name="Restraints and airbags", group=PartGroup.safety, count=1, material="supplier safety system", mass_kg=34.0, fidelity=Fidelity.derived, opaque=true, evidence_ids=[EVIDENCE_CONCEPT]), PartDefinition(id="fasteners-seals", name="Fasteners, adhesives and seals", group=PartGroup.manufacturing, count=620, material="mixed joining system", mass_kg=63.0, fidelity=Fidelity.reduced, opaque=false, evidence_ids=[EVIDENCE_PROCESS]), PartDefinition(id="service-links", name="Variant-specific harness and serviceable connector set", group=PartGroup.electrical, count=18, material="copper/polymer/connectors", mass_kg=19.0, fidelity=Fidelity.derived, opaque=false, evidence_ids=[EVIDENCE_ELECTRICAL]), ]
pub def vehicle_nets(configuration_variant: str) -> list[NetDefinition] !{}: require configuration_variant == "conventional" or configuration_variant == "circuitframe" return [ NetDefinition(id="hv-pack-front", name="Pack to front inverter", domain="HV", source_part_id="hv-junction", target_part_ids=["front-inverter"], nominal_voltage_v=800.0, maximum_current_a=420.0, route=variant_value(configuration_variant, "serviceable shielded HV cable", "left CircuitFrame sill protected busbar"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="hv-pack-rear", name="Pack to rear inverter", domain="HV", source_part_id="hv-junction", target_part_ids=["rear-inverter"], nominal_voltage_v=800.0, maximum_current_a=520.0, route=variant_value(configuration_variant, "serviceable shielded HV cable", "battery enclosure rear protected busbar"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="hv-charge", name="Charge path", domain="HV", source_part_id="charge-port", target_part_ids=["onboard-charger", "hv-junction"], nominal_voltage_v=800.0, maximum_current_a=500.0, route="serviceable shielded cable", fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="hv-dcdc", name="DC/DC supply", domain="HV", source_part_id="hv-junction", target_part_ids=["dc-dc"], nominal_voltage_v=800.0, maximum_current_a=25.0, route=variant_value(configuration_variant, "shielded HV cable", "protected panel trace"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="lv-zones", name="48 V zonal supply", domain="LV", source_part_id="dc-dc", target_part_ids=["front-zone-controller", "rear-zone-controller", "cabin-zone-controller", "central-compute"], nominal_voltage_v=48.0, maximum_current_a=180.0, route=variant_value(configuration_variant, "branched copper wiring harness", "dual protected structural conductors"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="data-backbone", name="Redundant Ethernet backbone", domain="DATA", source_part_id="central-compute", target_part_ids=["front-zone-controller", "rear-zone-controller", "cabin-zone-controller"], nominal_voltage_v=1.0, maximum_current_a=0.2, route=variant_value(configuration_variant, "shielded twisted-pair harness", "shielded differential structural links"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="front-drive-control", name="Front drive control", domain="DATA", source_part_id="front-zone-controller", target_part_ids=["front-inverter", "brakes", "steering"], nominal_voltage_v=1.0, maximum_current_a=0.2, route=variant_value(configuration_variant, "conventional branch harness", "short local links"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="rear-drive-control", name="Rear drive control", domain="DATA", source_part_id="rear-zone-controller", target_part_ids=["rear-inverter", "brakes"], nominal_voltage_v=1.0, maximum_current_a=0.2, route=variant_value(configuration_variant, "conventional branch harness", "short local links"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), NetDefinition(id="thermal-sensors", name="Thermal sensor network", domain="SENSOR", source_part_id="cabin-zone-controller", target_part_ids=["battery-management", "thermal-loop", "heat-pump"], nominal_voltage_v=5.0, maximum_current_a=2.0, route=variant_value(configuration_variant, "conventional sensor harness", "local printed links and service harness"), fidelity=Fidelity.derived, evidence_ids=[EVIDENCE_ELECTRICAL]), ]
def net_routing() -> list[dict[str, any]]: """Per-net conductor routing for both configurations.
Lengths, terminations and splice counts are bounded engineered assumptions derived from the packaging of the reconstructed vehicle; they are not measured production harness data. The CircuitFrame column routes through the structure, so it is shorter and needs fewer separable interfaces, which is the claim this comparison exists to quantify. """ return [ {"id": "hv-pack-front", "density": 5.0, "conventional": [3.10, 4.0, 1.0], "circuitframe": [2.35, 2.0, 0.0]}, {"id": "hv-pack-rear", "density": 5.0, "conventional": [1.90, 4.0, 1.0], "circuitframe": [1.35, 2.0, 0.0]}, {"id": "hv-charge", "density": 5.0, "conventional": [4.40, 6.0, 2.0], "circuitframe": [4.00, 4.0, 1.0]}, {"id": "hv-dcdc", "density": 5.0, "conventional": [1.20, 4.0, 0.0], "circuitframe": [0.90, 2.0, 0.0]}, {"id": "lv-zones", "density": 6.0, "conventional": [11.60, 14.0, 6.0], "circuitframe": [7.20, 6.0, 1.0]}, {"id": "data-backbone", "density": 6.0, "conventional": [9.80, 12.0, 4.0], "circuitframe": [6.40, 6.0, 0.0]}, {"id": "front-drive-control", "density": 6.0, "conventional": [5.30, 9.0, 3.0], "circuitframe": [2.90, 4.0, 0.0]}, {"id": "rear-drive-control", "density": 6.0, "conventional": [4.70, 8.0, 3.0], "circuitframe": [2.60, 4.0, 0.0]}, {"id": "thermal-sensors", "density": 6.0, "conventional": [7.90, 16.0, 5.0], "circuitframe": [4.80, 8.0, 1.0]}, ]
def conductor_variant(net: NetDefinition, route: list[f64], density_a_mm2: f64, jacket_factor: f64, conductor: str, route_text: str) !{}: """Size one conductor run from its declared current and report the resulting electrical loads.""" length_m = route[0] connectors = route[1] splices = route[2] area_mm2 = max(MINIMUM_CONDUCTOR_AREA_MM2, net.maximum_current_a / density_a_mm2) resistance_ohm = conductor_resistance(COPPER_RESISTIVITY_OHM_M, length_m, area_mm2, 1.0) copper_kg = conductor_mass(COPPER_DENSITY_KG_M3, length_m, area_mm2, 1.0) return { "conductor": conductor, "section_mm2": area_mm2, "length_m": length_m, "mass_kg": copper_kg * jacket_factor + connectors * CONNECTOR_MASS_KG + splices * SPLICE_MASS_KG, "resistance_mohm": resistance_ohm * 1000.0, "drop_v": net.maximum_current_a * resistance_ohm, "loss_w": net.maximum_current_a * net.maximum_current_a * resistance_ohm, "connectors": int(connectors), "splices": int(splices), "route": route_text, }
pub def vehicle_net_comparison() -> list[dict[str, any]] !{}: """Conventional harness versus CircuitFrame structural conductors over the same nine nets.""" conventional_nets = vehicle_nets("conventional") circuitframe_nets = vehicle_nets("circuitframe") routing = net_routing() mut rows = [] for index in range(0, len(routing)): entry = routing[index] harness_net = conventional_nets[index] structural_net = circuitframe_nets[index] ensure entry["id"] == harness_net.id and entry["id"] == structural_net.id harness = conductor_variant(harness_net, entry["conventional"], entry["density"], HARNESS_JACKET_FACTOR, "stranded copper cable with jacket and shield", harness_net.route) structural = conductor_variant(structural_net, entry["circuitframe"], STRUCTURAL_CURRENT_DENSITY_A_MM2, STRUCTURAL_OVERPRINT_FACTOR, "printed structural conductor with dielectric barrier and overprinted shield", structural_net.route) rows = rows + [{ "id": harness_net.id, "name": harness_net.name, "domain": harness_net.domain, "voltage_v": harness_net.nominal_voltage_v, "current_a": harness_net.maximum_current_a, "source_part_id": harness_net.source_part_id, "target_part_ids": harness_net.target_part_ids, "variants": {"conventional": harness, "circuitframe": structural}, "delta": { "mass_kg": structural["mass_kg"] - harness["mass_kg"], "connectors": structural["connectors"] - harness["connectors"], "drop_v": structural["drop_v"] - harness["drop_v"], "length_m": structural["length_m"] - harness["length_m"], }, }] return rows
def manufacturing_operations(configuration_variant: str): require configuration_variant == "conventional" or configuration_variant == "circuitframe" return [ OperationDefinition(id="op-010", name="Locate structural floor and subframes", station="body-01", resource="fixture-cell", consumed_part_ids=["floor-pan", "front-subframe", "rear-subframe"], created_features=["vehicle datum frame"], automation=AutomationClass.unknown, cycle_time_s=68.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-020", name=variant_value(configuration_variant, "Install and inspect conventional harnesses", "Print and inspect protected sill conductors"), station="circuit-01", resource=variant_value(configuration_variant, "harness routing and connector cell", "multi-material deposition cell"), consumed_part_ids=["sill-panels", "service-links"], created_features=[variant_value(configuration_variant, "routed HV/LV/data harnesses", "HV/LV/data conductors with dielectric and coating")], automation=AutomationClass.unknown, cycle_time_s=142.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-030", name="Place zone controllers and power terminals", station="circuit-02", resource="six-axis robot and vision tool", consumed_part_ids=["front-zone-controller", "rear-zone-controller", "cabin-zone-controller", "hv-junction"], created_features=["robotic placements", "bonded terminals"], automation=AutomationClass.unknown, cycle_time_s=94.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-040", name="Continuity and isolation test", station="test-01", resource="automated electrical tester", consumed_part_ids=["sill-panels", "hv-junction"], created_features=["signed continuity record", "signed isolation record"], automation=AutomationClass.unknown, cycle_time_s=52.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-050", name="Install and seal battery pack", station="battery-01", resource="AGV lift and fastening robots", consumed_part_ids=["battery-enclosure", "battery-modules", "battery-management"], created_features=["sealed structural pack joint"], automation=AutomationClass.unknown, cycle_time_s=126.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-060", name="Install drive units and thermal loop", station="drive-01", resource="dual robot cell", consumed_part_ids=["front-motor", "rear-motor", "front-inverter", "rear-inverter", "thermal-loop"], created_features=["drive mounts", "thermal connections"], automation=AutomationClass.assisted, cycle_time_s=155.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-070", name="Install suspension, steering, brakes, wheels and tires", station="chassis-01", resource="four-corner robot cell", consumed_part_ids=["suspension", "steering", "brakes", "wheels", "tires"], created_features=["aligned corner modules"], automation=AutomationClass.assisted, cycle_time_s=188.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-080", name="Install cabin, glazing and restraints", station="trim-01", resource="robots plus manual safety work", consumed_part_ids=["seats", "interior", "glazing", "restraints"], created_features=["trimmed cabin"], automation=AutomationClass.manual, cycle_time_s=420.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-090", name="Join and seal body shell", station="body-02", resource="joining and metrology cell", consumed_part_ids=["body-shell", "closures", "fasteners-seals"], created_features=["closed body", "measured gaps"], automation=AutomationClass.unknown, cycle_time_s=176.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-100", name="Connect serviceable exceptions", station="electrical-01", resource="robot-assisted operator", consumed_part_ids=["service-links", "lamps", "sensors"], created_features=["service disconnects", "sensor connections"], automation=AutomationClass.manual, cycle_time_s=255.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-110", name="Coat and paint exterior", station="paint-01", resource="robot paint line", consumed_part_ids=["body-shell"], created_features=["corrosion protection", "pearl exterior finish"], automation=AutomationClass.unknown, cycle_time_s=310.0, evidence_ids=[EVIDENCE_PROCESS]), OperationDefinition(id="op-120", name="Software, calibration and end-of-line test", station="eol-01", resource="automated dynamometer and diagnostics", consumed_part_ids=["central-compute", "battery-management"], created_features=["configuration record", "EOL evidence bundle"], automation=AutomationClass.assisted, cycle_time_s=360.0, evidence_ids=[EVIDENCE_PROCESS]), ]
def validation_cases(): return [ ValidationCaseDefinition(id="dvp-drive-energy", name="Longitudinal drive, brake, regeneration, and energy balance", domain="vehicle-dynamics", authority="Sema internal verification; not homologation evidence", method="20 Hz coupled reduced-model step and exact terminal-power residual", status=QualificationStatus.executable_reduced, fidelity=Fidelity.reduced, evidence_ids=[EVIDENCE_CONCEPT, EVIDENCE_ELECTRICAL]), ValidationCaseDefinition(id="dvp-lateral-yaw", name="Steering and crosswind lateral/yaw response", domain="vehicle-dynamics", authority="Sema internal verification; external maneuver correlation required", method="reduced bicycle model with axle load transfer and friction-circle limits", status=QualificationStatus.executable_reduced, fidelity=Fidelity.reduced, evidence_ids=[EVIDENCE_CONCEPT]), ValidationCaseDefinition(id="dvp-aero-crosswind", name="Apparent-wind aerodynamic loads", domain="aerodynamics", authority="Sema internal verification; governing CFD absent", method="analytic drag, lift, side force, pressure, and held-out coefficient residual", status=QualificationStatus.executable_reduced, fidelity=Fidelity.reduced, evidence_ids=[EVIDENCE_CONCEPT]), ValidationCaseDefinition(id="dvp-pack-electrothermal", name="Pack equivalent-circuit and component thermal duty", domain="battery", authority="Sema internal verification; cell and pack correlation required", method="open-circuit voltage, internal resistance, loss, SoC, and lumped thermal integration", status=QualificationStatus.executable_reduced, fidelity=Fidelity.reduced, evidence_ids=[EVIDENCE_ELECTRICAL, EVIDENCE_OPEN_BMS]), ValidationCaseDefinition(id="dvp-body-torsion", name="Body-in-white static torsional stiffness", domain="structure", authority="OEM target and solver practice must be supplied", method="implicit structural FEA with suspension hard-point reactions and mesh convergence", status=QualificationStatus.blocked_missing_evidence, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_CONCEPT]), ValidationCaseDefinition(id="dvp-modal-nvh", name="Body and full-vehicle modal/NVH response", domain="structure", authority="OEM target and correlation measurements must be supplied", method="normal modes, frequency response, road-input transfer paths, and test correlation", status=QualificationStatus.blocked_missing_evidence, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_CONCEPT]), ValidationCaseDefinition(id="dvp-frontal-crash", name="Frontal crash and restraint response", domain="crash", authority="Candidate baseline: UNECE frontal-impact requirements and Euro NCAP 2026 Crash Protection", method="explicit nonlinear FE vehicle, dummy, restraint, intrusion, and post-crash HV analysis", status=QualificationStatus.planned_external, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_TESLA_SERVICE]), ValidationCaseDefinition(id="dvp-side-pole-crash", name="Side and pole impact", domain="crash", authority="Candidate baseline: UNECE R95/R135 and Euro NCAP 2026 Crash Protection", method="explicit nonlinear FE side structure, occupant, battery intrusion, and HV isolation analysis", status=QualificationStatus.planned_external, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_TESLA_SERVICE]), ValidationCaseDefinition(id="dvp-roof-rollover", name="Roof strength and rollover protection", domain="crash", authority="Applicable market and vehicle-category requirements must be frozen", method="quasi-static roof load plus explicit rollover and restraint analysis", status=QualificationStatus.planned_external, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_CONCEPT]), ValidationCaseDefinition(id="dvp-post-crash-hv", name="Post-crash electrical safety and rescue", domain="electrical-safety", authority="Candidate baseline: FMVSS 305a/GTR 20 and Euro NCAP 2026 Post-Crash Safety", method="HV isolation, electrical shock, electrolyte, thermal-event warning, door access, and emergency-response evidence", status=QualificationStatus.planned_external, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_ELECTRICAL]), ValidationCaseDefinition(id="dvp-reess-safety", name="Rechargeable energy storage system safety", domain="battery", authority="Candidate baseline: UNECE R100; applicable revision and approval scope must be frozen", method="mechanical integrity, vibration, thermal shock, fire, external short, overcharge, over-discharge, over-temperature, and propagation evidence", status=QualificationStatus.planned_external, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_OPEN_BMS]), ValidationCaseDefinition(id="dvp-emc", name="Vehicle and component EMC", domain="electrical-safety", authority="Candidate baseline: UNECE R10/CISPR 25 plus customer limits", method="emissions, immunity, transient, grounding, shielding, and communication robustness tests", status=QualificationStatus.planned_external, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_ELECTRICAL]), ValidationCaseDefinition(id="dvp-durability", name="Road-load and environmental durability", domain="durability", authority="Customer duty cycle, proving-ground spectrum, and life targets must be supplied", method="multiaxial road-load acquisition, fatigue, thermal cycling, corrosion, ingress, and accelerated-life correlation", status=QualificationStatus.blocked_missing_evidence, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_CONCEPT]), ValidationCaseDefinition(id="dvp-manufacturing-cell", name="Robot reach, collision, joining, and process capability", domain="manufacturing", authority="Supplier cell, tooling, tolerance, and process-capability evidence must be supplied", method="offline robot programming, reach/collision, line balance, joining coupons, metrology, and capability study", status=QualificationStatus.blocked_missing_evidence, fidelity=Fidelity.unknown, evidence_ids=[EVIDENCE_PROCESS]), ValidationCaseDefinition(id="dvp-functional-safety", name="Functional-safety lifecycle and safety validation", domain="governance", authority="Candidate baseline: ISO 26262; item definition and HARA remain owner work", method="requirements traceability, HARA, ASIL decomposition, dependent-failure analysis, verification, and safety case", status=QualificationStatus.reference_only, fidelity=Fidelity.excluded, evidence_ids=[EVIDENCE_ELECTRICAL]), ]
def evidence_records(): return [ EvidenceRecord(id=EVIDENCE_EXTERIOR, kind=EvidenceClass.provenance, source="Printables model 1111101 / STL 4641564", summary="CC BY 4.0 community-reference exterior normalized into bounded presentation LODs; not OEM CAD", fidelity=Fidelity.derived, accepted=true), EvidenceRecord(id=EVIDENCE_CONCEPT, kind=EvidenceClass.provenance, source="CircuitFrame explicit engineered assumptions", summary="Visible subsystem reconstruction with non-authoritative geometry, materials, mass, interfaces, and placement", fidelity=Fidelity.reduced, accepted=true), EvidenceRecord(id=EVIDENCE_ELECTRICAL, kind=EvidenceClass.computational, source="Sema electrical equations", summary="Bounded concept topology without source-validated physical routing", fidelity=Fidelity.reduced, accepted=true), EvidenceRecord(id=EVIDENCE_PROCESS, kind=EvidenceClass.manufacturing, source="CircuitFrame deterministic process demonstrator", summary="Scripted operation graph without sourced production process or robot feasibility", fidelity=Fidelity.reduced, accepted=false), EvidenceRecord(id=EVIDENCE_TESLA_SERVICE, kind=EvidenceClass.provenance, source="Tesla Service Model Y SOP3 Berlin public documentation", summary="Official electrical, connector, collision-repair, and dimensional discovery source; remote reference only and not OEM CAD authority", fidelity=Fidelity.derived, accepted=false), EvidenceRecord(id=EVIDENCE_OPEN_BMS, kind=EvidenceClass.computational, source="ENNOID-BMS commit daf72c927d4134cad95bb57da6d85807adf948b0", summary="Ingested GPL-3.0 repository snapshot with modular up-to-400-V BMS datasheet and KiCad schematics; separate comparator, not an 800 V vehicle design", fidelity=Fidelity.derived, accepted=false), EvidenceRecord(id=EVIDENCE_TABBY_CAD, kind=EvidenceClass.provenance, source="Open Motors TABBY EVO 2015 open CAD release metadata", summary="CC-BY-SA-4.0 open-EV CAD candidate kept separate; historical binaries were unavailable and therefore were not ingested", fidelity=Fidelity.illustrative, accepted=false), ]
def source_records(): return [ SourceRecord( id=EXTERIOR_SOURCE_ID, authority=SourceAuthority.licensed_benchmark, provider="Spark Models and SueRoger via Printables", vehicle_model="Tesla Model Y community-reference exterior", source_revision="printables-model-1111101-stl-4641564", artifact_kind="attributed CC BY 4.0 binary STL and deterministic web GLB LODs", rights_scope="rendering, adaptation, and attributed visualization export under CC BY 4.0; no Tesla endorsement, OEM CAD, manufacturing, or safety authority", digest="aaf8a0da316b913b4559bcaa84f5a8cb9ae3e306b1297ee5d6e53b0c9ce0561b", fidelity=Fidelity.derived, render_allowed=true, simulation_allowed=false, export_allowed=true, ), SourceRecord( id=ASSUMPTION_SOURCE_ID, authority=SourceAuthority.engineered_assumption, provider="CircuitFrame Sema reconstruction", vehicle_model="CircuitFrame Model Y reference reconstruction", source_revision="configuration-revision-1", artifact_kind="explicit subsystem geometry, property, E/E, and process assumptions", rights_scope="evidence-labelled demo rendering, reduced simulation, and manifest export only; no production, homologation, or safety claim", digest="circuitframe-gt-01-revision-1", fidelity=Fidelity.reduced, render_allowed=true, simulation_allowed=true, export_allowed=true, ), SourceRecord( id=TESLA_SERVICE_SOURCE_ID, authority=SourceAuthority.open_research, provider="Tesla Service", vehicle_model="Model Y SOP3 Berlin public service reference", source_revision="non-structural-pack-2022-01-06--2024-05-06", artifact_kind="remote electrical, connector, collision-repair, and dimensional indexes", rights_scope="reference discovery only; documents are not redistributed and confer no OEM CAD, production, or homologation authority", digest="86c98870e71b10d06f481d24eab75201796d623b973a8395c7b3ccfded3ecb40", fidelity=Fidelity.derived, render_allowed=false, simulation_allowed=false, export_allowed=false, ), SourceRecord( id=OPEN_BMS_SOURCE_ID, authority=SourceAuthority.open_research, provider="ENNOID-BMS open-source project", vehicle_model="Modular BMS reference for packs up to 400 V", source_revision="daf72c927d4134cad95bb57da6d85807adf948b0", artifact_kind="metadata and digest manifest for an optional external KiCad source checkout", rights_scope="repository declares GPL-3.0; source artifacts are not redistributed here; explicit external source root plus legal and engineering review required", digest="b393ebda17485f3b123c64f6a081241a070665726e24864e152d8a8aef7e8cac", fidelity=Fidelity.derived, render_allowed=false, simulation_allowed=false, export_allowed=false, ), SourceRecord( id=TABBY_CAD_SOURCE_ID, authority=SourceAuthority.open_research, provider="Open Motors, formerly OSVehicle", vehicle_model="TABBY EVO open EV platform", source_revision="2-seat-v0.1-and-4-seat-v0.1-2015", artifact_kind="CC-BY-SA-4.0 STEP/2D CAD release metadata; binaries unavailable", rights_scope="separate CAD/process comparator only; no Model Y or CircuitFrame identity, geometry, or validation authority", digest="678563eec503b99ddfadd78253e82a24dd5d67c69031494601230dd113f7c9ae", fidelity=Fidelity.illustrative, render_allowed=false, simulation_allowed=false, export_allowed=false, ), ]
def source_bindings(): mut bindings = [] for part in vehicle_parts(): if part.id == "body-shell" or part.id == "wheels" or part.id == "tires" or part.id == "lamps" or part.id == "glazing" or part.id == "closures": bindings.append(SourceBinding(entity_id=part.id, source_record_id=EXTERIOR_SOURCE_ID, source_entity_id="monolithic-exterior-envelope", fidelity=Fidelity.derived)) else: bindings.append(SourceBinding(entity_id=part.id, source_record_id=ASSUMPTION_SOURCE_ID, source_entity_id=part.id, fidelity=Fidelity.reduced)) return bindings
pub def source_gate() -> dict[str, any] !{}: bindings = source_bindings() ensure len(bindings) == len(vehicle_parts()) mut accepted_source_records = 0 mut assumption_records = 0 mut reference_only_records = 0 for source in source_records(): if source.authority == SourceAuthority.licensed_benchmark: accepted_source_records = accepted_source_records + 1 elif source.authority == SourceAuthority.engineered_assumption: assumption_records = assumption_records + 1 else: reference_only_records = reference_only_records + 1 return { "status": "partial", "presentation_qualified": true, "physical_completeness": false, "reason": "attributed CC BY exterior and FreeCAD/OCCT mechanical assembly loaded; product authority, certified materials and mass, process capability, and validation remain assumption-tagged or incomplete", "current_asset_role": "community-reference exterior plus source-gated parametric mechanical reconstruction", "accepted_source_records": accepted_source_records, "assumption_records": assumption_records, "reference_only_records": reference_only_records, "bound_fixture_groups": 0, "bound_reconstruction_groups": len(bindings), "planned_configurations": ["conventional", "circuitframe"], "shared_reference_identity_required": true, "exterior_asset_manifest": "/assets/vehicle/manifest.json", "source_mesh_sha256": "aaf8a0da316b913b4559bcaa84f5a8cb9ae3e306b1297ee5d6e53b0c9ce0561b", "exterior_lods": [ {"id": "hero", "path": "/assets/vehicle/model-y-hero.glb", "sha256": "4d6657c313118c7978398ba2e8d6f5888208e03f626848ec5c6c3db90facd088"}, {"id": "medium", "path": "/assets/vehicle/model-y-medium.glb", "sha256": "c41f343f29a838f9bc11b494bfeb830b29d64794fe929349574a6f7b4ba9af3e"}, {"id": "compact", "path": "/assets/vehicle/model-y-compact.glb", "sha256": "c6070775ae904dd238457583e70f84b3514092cf6661fd3d2103419e625257c1"}, ], "mechanical_asset_manifest": "/assets/mechanical/manifest.json", "mechanical_authority": "FreeCAD/OCCT parametric engineering reconstruction from declared dimensional assumptions; not OEM manufacturing CAD", "mechanical_features": 1005, "mechanical_lods": [ {"id": "hero", "path": "/assets/mechanical/circuitframe-mechanical-hero.glb", "sha256": "47533dcfe24eaa7191e84b5064fc82163ac49fcf3c18eae6f768ca4d3d9e440f"}, {"id": "medium", "path": "/assets/mechanical/circuitframe-mechanical-medium.glb", "sha256": "121910ada7a5c05ab46b89be9ca597258b5342bd968d8464428e51f3203af189"}, {"id": "compact", "path": "/assets/mechanical/circuitframe-mechanical-compact.glb", "sha256": "b92d4bd80740c7cee2bf4b485dbb1398f850e03cd09019e2d99d1e182722b156"}, ], "attribution": "Tesla Model Y (high detail) by Spark Models, remixed from SueRoger, CC BY 4.0", "branding_review_status": "front and rear badge triangles removed and replaced by neutral surfaces; final legal review open", }
pub def completeness_report(configuration_variant: str) -> CompletenessReport !{}: parts = vehicle_parts() operations = manufacturing_operations(configuration_variant) mut instances = 0 mut unknown = 0 for part in parts: instances = instances + part.count for operation in operations: if operation.automation == AutomationClass.unknown: unknown = unknown + 1 return CompletenessReport( profile="major_assembly_incomplete", declared_parts=len(parts), declared_instances=instances, declared_nets=len(vehicle_nets(configuration_variant)), declared_operations=len(operations), unterminated_required_ports=0, required_port_inventory_complete=false, unknown_operations=unknown, major_assembly_complete=false, production_complete=false, exclusions=["required mechanical, electrical, thermal, fluid, control, and service ports are not yet inventoried; zero unterminated ports is therefore not claimed", "CC BY community exterior is dimensionally normalized presentation geometry, not OEM CAD", "all non-exterior geometry, mass properties, interfaces, and placements remain bounded engineered assumptions", "physical circuit routing and manufacturing feasibility are not validated", "fasteners represented as counted families", "airbag inflator internals opaque", "semiconductor die internals opaque", "tire compound formulation opaque", "manufacturing CAD authority not claimed"], )
pub def vehicle_manifest(configuration_variant: str) -> dict[str, any] !{}: identity = vehicle_identity() report = completeness_report(configuration_variant) ensure identity.id == CONFIGURATION_ID ensure not report.major_assembly_complete and not report.production_complete return { "schema": "sema.circuitframe-vehicle-manifest/v3", "configuration_variant": configuration_variant, "configuration_delta": { "shared_identity": CONFIGURATION_ID, "changed_part_ids": ["sill-panels", "service-links"], "changed_net_ids": ["hv-pack-front", "hv-pack-rear", "hv-dcdc", "lv-zones", "data-backbone", "front-drive-control", "rear-drive-control", "thermal-sensors"], "removed_features": ["structural conductors", "printed short links"] if configuration_variant == "conventional" else ["branched copper harness routes"], "added_features": ["branched copper harness routes"] if configuration_variant == "conventional" else ["protected structural conductors", "printed short links"], "authority": "configuration delta is a bounded engineered comparison, not production routing", }, "identity": identity, "parts": vehicle_parts(), "nets": vehicle_nets(configuration_variant), "operations": manufacturing_operations(configuration_variant), "validation_cases": validation_cases(), "evidence": evidence_records(), "sources": source_records(), "source_bindings": source_bindings(), "source_gate": source_gate(), "completeness": report, }
test "major assembly profile remains incomplete without required port inventory": report = completeness_report("circuitframe") ensure not report.major_assembly_complete ensure not report.required_port_inventory_complete ensure not report.production_complete ensure report.declared_parts >= 35 ensure report.declared_instances > report.declared_parts ensure report.declared_nets >= 8 ensure report.declared_operations >= 10 ensure report.unknown_operations > 0
test "sourced exterior qualifies presentation without physical authority": gate = source_gate() ensure gate["status"] == "partial" ensure gate["presentation_qualified"] == true ensure gate["physical_completeness"] == false ensure gate["accepted_source_records"] == 1 ensure gate["bound_fixture_groups"] == 0 ensure len(source_bindings()) == len(vehicle_parts())
test "qualification matrix separates executable reduced checks from external evidence": cases = validation_cases() mut executable = 0 mut external = 0 for validation_case in cases: if validation_case.status == QualificationStatus.executable_reduced: executable = executable + 1 if validation_case.status == QualificationStatus.planned_external or validation_case.status == QualificationStatus.blocked_missing_evidence: external = external + 1 ensure len(cases) >= 15 ensure executable == 4 ensure external >= 10Reflected API
Section titled “Reflected API”adaptation
Section titled “adaptation”Automotive adaptive-aerodynamics lifecycle over Sema standard-library gates.
struct AeroAdaptationSummary
Section titled “struct AeroAdaptationSummary”Fields
| field | type | descriptor |
|---|---|---|
active_model_id |
str |
|
candidate_model_id |
str |
|
selected_model_id |
str |
|
lifecycle |
str |
|
regime |
str |
|
mode |
str |
|
validation_status |
str |
|
detector_score |
f64 |
|
training_error |
f64 |
|
validation_error |
f64 |
|
invariant_violations |
int |
|
dwell_steps |
int |
|
hysteresis_margin |
f64 |
|
activated |
bool |
|
reason |
str |
|
evidence_ids |
list[str] |
def initial_aero_detector
Section titled “def initial_aero_detector”def initial_aero_detector() -> ResidualDetector !{}Returns ResidualDetector
Effects !{}
def update_aero_detector
Section titled “def update_aero_detector”def update_aero_detector(detector: ResidualDetector, residual: f64, observed_at: f64) -> ResidualDetector !{}Parameters
| name | type |
|---|---|
detector |
ResidualDetector |
residual |
f64 |
observed_at |
f64 |
Returns ResidualDetector
Effects !{}
def regime_label
Section titled “def regime_label”def regime_label(detector: ResidualDetector) -> str !{}Parameters
| name | type |
|---|---|
detector |
ResidualDetector |
Returns str
Effects !{}
def retained_adaptation
Section titled “def retained_adaptation”def retained_adaptation(detector: ResidualDetector, active_model_id: str) -> AeroAdaptationSummary !{}Parameters
| name | type |
|---|---|
detector |
ResidualDetector |
active_model_id |
str |
Returns AeroAdaptationSummary
Effects !{}
def evaluate_yaw_candidate
Section titled “def evaluate_yaw_candidate”def evaluate_yaw_candidate(detector: ResidualDetector, step: int, observed_at: f64, speed_mps: f64, sideslip_deg: f64, holdout: dict[str, f64]) -> AeroAdaptationSummary !{}Parameters
| name | type |
|---|---|
detector |
ResidualDetector |
step |
int |
observed_at |
f64 |
speed_mps |
f64 |
sideslip_deg |
f64 |
holdout |
dict[str, f64] |
Returns AeroAdaptationSummary
Effects !{}
Vehicle aerodynamics by a 3D constant-strength source-panel method with an image ground plane.
Method: Hess-Smith constant-strength source panels over a closed lofted body, with every panel mirrored through the road surface (z = 0) so the ground boundary condition is satisfied exactly. The Neumann condition (zero normal velocity on every panel) is closed by two dense linear solves, one per unit freestream direction; the boundary-value problem is linear in the freestream, so sigma(V, beta) = V (cos beta sigma_x + sin beta sigma_y) is exact for every wind speed and yaw and no matrix work is needed per frame.
Honesty: this is NOT a CFD run and no Navier-Stokes equation is integrated. Potential flow alone
yields exactly zero drag and zero side force on a closed body (d’Alembert’s paradox), so every
drag and side-force number here comes from the empirical viscous and base-pressure closure layered
on top of the solved pressure field. Each published quantity is tagged solved (out of the linear
system), correlated (empirical closure with a cited form), or numerical (a discretisation
diagnostic). See aero_equations() for the per-quantity provenance.
struct AeroPanel
Section titled “struct AeroPanel”Fields
| field | type | descriptor |
|---|---|---|
index |
int |
|
station |
int |
|
ring_index |
int |
|
center_x |
f64 |
|
center_y |
f64 |
|
center_z |
f64 |
|
normal_x |
f64 |
|
normal_y |
f64 |
|
normal_z |
f64 |
|
area_m2 |
f64 |
struct AeroBasis
Section titled “struct AeroBasis”Fields
| field | type | descriptor |
|---|---|---|
panels |
list[AeroPanel] |
|
source_x |
list[f64] |
|
source_y |
list[f64] |
|
mode_x |
list[f64] |
|
mode_y |
list[f64] |
|
wake_forward |
list[f64] |
|
wake_forward_cp |
list[f64] |
|
wake_reverse |
list[f64] |
|
wake_reverse_cp |
list[f64] |
|
wake_crossflow |
list[f64] |
|
wake_crossflow_cp |
list[f64] |
|
base_face |
list[f64] |
|
panel_count |
int |
|
wetted_area_m2 |
f64 |
|
frontal_area_m2 |
f64 |
|
side_area_m2 |
f64 |
|
planform_area_m2 |
f64 |
|
closure_residual_m2 |
f64 |
|
residual_x |
f64 |
|
residual_y |
f64 |
|
separation_x_m |
f64 |
|
calibrated_wheel_cd |
f64 |
|
calibration_speed_mps |
f64 |
|
calibration_target_cd |
f64 |
struct AeroForces
Section titled “struct AeroForces”Fields
| field | type | descriptor |
|---|---|---|
air_speed_mps |
f64 |
|
yaw_rad |
f64 |
|
dynamic_pressure_pa |
f64 |
|
reynolds_number |
f64 |
|
cd |
f64 |
|
cd_pressure |
f64 |
|
cd_base |
f64 |
|
cd_friction |
f64 |
|
cd_wheels |
f64 |
|
cy |
f64 |
|
cl |
f64 |
|
cmz |
f64 |
|
drag_n |
f64 |
|
side_n |
f64 |
|
lift_n |
f64 |
|
yaw_moment_nm |
f64 |
|
front_lift_n |
f64 |
|
rear_lift_n |
f64 |
|
pressure_drag_n |
f64 |
|
cp_min |
f64 |
|
cp_max |
f64 |
|
base_pressure_coefficient |
f64 |
|
separation_x_m |
f64 |
|
wake_width_m |
f64 |
|
wake_deficit_fraction |
f64 |
|
wake_shedding_hz |
f64 |
|
turbulence_intensity |
f64 |
|
wake_recirculation_length_m |
f64 |
|
dalembert_residual_cd |
f64 |
|
solver_residual |
f64 |
|
force_closure_residual |
f64 |
|
surface_cp |
list[f64] |
def smooth_step
Section titled “def smooth_step”def smooth_step(low: f64, high: f64, value: f64)Parameters
| name | type |
|---|---|
low |
f64 |
high |
f64 |
value |
f64 |
Cubic Hermite ramp, clamped to [0, 1] outside the interval.
def roof_height
Section titled “def roof_height”def roof_height(u: f64)Parameters
| name | type |
|---|---|
u |
f64 |
Upper silhouette: low pointed nose, hood, raked windscreen, roof peak, fastback taper.
def floor_height
Section titled “def floor_height”def floor_height(u: f64)Parameters
| name | type |
|---|---|
u |
f64 |
Lower silhouette: approach ramp, flat underbody at ride height, rear diffuser.
def half_width
Section titled “def half_width”def half_width(u: f64)Parameters
| name | type |
|---|---|
u |
f64 |
Plan-view half width: blunt nose growth to full width, mild fastback taper aft.
def station_ring
Section titled “def station_ring”def station_ring(u: f64, x_m: f64)Parameters
| name | type |
|---|---|
u |
f64 |
x_m |
f64 |
One superelliptic cross-section as AROUND flattened x,y,z points, mirror symmetric in y.
def panel_mesh
Section titled “def panel_mesh”def panel_mesh()Closed lofted body: two degenerate end caps plus RING_COUNT - 3 superelliptic stations.
Panels are ordered index = station * AROUND + ring_index, and ring_index k mirrors to AROUND - 1 - k through the y = 0 plane, which the symmetry-reduced solve relies on.
def influence_blocks
Section titled “def influence_blocks”def influence_blocks(tx: list[f64], ty: list[f64], tz: list[f64], sx: list[f64], sy: list[f64], sz: list[f64], area: list[f64], epsilon: list[f64]) -> anyParameters
| name | type |
|---|---|
tx |
list[f64] |
ty |
list[f64] |
tz |
list[f64] |
sx |
list[f64] |
sy |
list[f64] |
sz |
list[f64] |
area |
list[f64] |
epsilon |
list[f64] |
Returns any
Velocity induced at every target by unit constant sources plus their z = 0 ground images.
Returns the three dense component matrices [Kx, Ky, Kz]; the near field is regularised with r_eps = REGULARISATION_FACTOR * sqrt(area_j) exactly as the browser-side field evaluator does.
def normal_projection
Section titled “def normal_projection”def normal_projection(blocks: any, nx: list[f64], ny: list[f64], nz: list[f64], columns: int)Parameters
| name | type |
|---|---|
blocks |
any |
nx |
list[f64] |
ny |
list[f64] |
nz |
list[f64] |
columns |
int |
Contract the three influence blocks against the target normals into one dense matrix.
def identity_matrix
Section titled “def identity_matrix”def identity_matrix(n: int)Parameters
| name | type |
|---|---|
n |
int |
def dense_solve
Section titled “def dense_solve”def dense_solve(matrix: any, rhs: list[f64])Parameters
| name | type |
|---|---|
matrix |
any |
rhs |
list[f64] |
def dense_apply
Section titled “def dense_apply”def dense_apply(matrix: any, vector: list[f64], rows: int)Parameters
| name | type |
|---|---|
matrix |
any |
vector |
list[f64] |
rows |
int |
def stratford_limit
Section titled “def stratford_limit”def stratford_limit(recovery: f64, run_m: f64, gradient: f64, constant: f64)Parameters
| name | type |
|---|---|
recovery |
f64 |
run_m |
f64 |
gradient |
f64 |
constant |
f64 |
Stratford (1959) turbulent separation: Cbar (xi dCbar/dxi)^1/2 >= k (1e-6 Re_xi)^1/10.
Re_xi is taken at the calibration speed so the separation topology stays geometry locked, which
is what road vehicles show above Re_L ~ 4e6 where separation sits on fixed edges. correlated.
def line_walk
Section titled “def line_walk”def line_walk(panels: list[AeroPanel], cp: list[f64], side_line: list[f64], forward: bool)Parameters
| name | type |
|---|---|
panels |
list[AeroPanel] |
cp |
list[f64] |
side_line |
list[f64] |
forward |
bool |
March each longitudinal surface line and freeze the pressure downstream of separation.
Returns 2 * PANEL_COUNT values: a 0/1 separation flag per panel followed by the frozen separation pressure coefficient carried into the wake.
def ring_walk
Section titled “def ring_walk”def ring_walk(panels: list[AeroPanel], cp: list[f64])Parameters
| name | type |
|---|---|
panels |
list[AeroPanel] |
cp |
list[f64] |
March each cross-section from its crossflow stagnation point and mark the leeward wake.
Crossflow-analogy separation (Allen & Perkins): the pressure field driving it is the unit lateral mode alone, so the topology is yaw independent and can be precomputed once.
def aero_basis
Section titled “def aero_basis”def aero_basis() -> AeroBasis !{}Returns AeroBasis
Effects !{}
Build the panel mesh, run both dense source solves once, and freeze the wake topology.
def aero_solve
Section titled “def aero_solve”def aero_solve(basis: AeroBasis, air_speed_mps: f64, yaw_rad: f64) -> AeroForces !{}Parameters
| name | type |
|---|---|
basis |
AeroBasis |
air_speed_mps |
f64 |
yaw_rad |
f64 |
Returns AeroForces
Effects !{}
Integrate the surface pressure for one wind speed and yaw. O(panels), no matrix work.
The apparent wind in the vehicle frame is U = -V (cos beta, sin beta, 0): air arrives at the nose and leaves past the tail at zero yaw, and beta > 0 puts the wind on the vehicle’s left. The solved Cp field is invariant under freestream reversal, so it is evaluated straight from the exported +x and +y modes; only the viscous closure needs the physical flow direction.
def rounded
Section titled “def rounded”def rounded(value: f64)Parameters
| name | type |
|---|---|
value |
f64 |
Half-up rounding to five decimals so the exported panel payload stays compact.
def aero_basis_export
Section titled “def aero_basis_export”def aero_basis_export(basis: AeroBasis) -> dict[str, any] !{}Parameters
| name | type |
|---|---|
basis |
AeroBasis |
Returns dict[str, any]
Effects !{}
sema.circuitframe-aero-basis/v1: geometry, source strengths and the solver provenance.
def aero_equations
Section titled “def aero_equations”def aero_equations() -> list[dict[str, str]] !{}Returns list[dict[str, str]]
Effects !{}
Equation manifest rows; every owner states solved, correlated or numerical provenance.
def yaw_forces
Section titled “def yaw_forces”def yaw_forces(basis: AeroBasis, degrees: f64)Parameters
| name | type |
|---|---|
basis |
AeroBasis |
degrees |
f64 |
Evaluate one yaw station at the calibration wind speed. Used by the tests below.
assembly
Section titled “assembly”Physical assembly model: ports, mates, joints, mass properties, load paths, and the coupled thermal network.
enum JointKind
Section titled “enum JointKind”Variants
fixedrevoluteprismaticsphericalcylindricalgearrigid_bond
enum PlacementPattern
Section titled “enum PlacementPattern”Variants
singledistributedlateral_pairlongitudinal_paircorner_quad
enum LoadPathKind
Section titled “enum LoadPathKind”Variants
verticallongitudinallateralstructuralcrashrestraint
struct MateDefinition
Section titled “struct MateDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
port_a_id |
str |
|
port_b_id |
str |
|
kind |
PortKind |
|
preload_n |
f64 |
|
sealed |
bool |
|
serviceable |
bool |
|
robot_accessible |
bool |
struct JointDefinition
Section titled “struct JointDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
kind |
JointKind |
|
parent_part_id |
str |
|
child_part_id |
str |
|
parent_link |
str |
|
child_link |
str |
|
axis_x |
f64 |
|
axis_y |
f64 |
|
axis_z |
f64 |
|
origin_x |
f64 |
|
origin_y |
f64 |
|
origin_z |
f64 |
|
limit_lower |
f64 |
|
limit_upper |
f64 |
|
ratio |
f64 |
|
dof |
int |
|
idle_dof |
int |
|
corner |
str |
|
loop_closing |
bool |
struct PartPlacement
Section titled “struct PartPlacement”Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
pattern |
PlacementPattern |
|
pattern_instances |
int |
|
mass_kg |
f64 |
|
centre_x |
f64 |
|
centre_y |
f64 |
|
centre_z |
f64 |
|
extent_x |
f64 |
|
extent_y |
f64 |
|
extent_z |
f64 |
|
span_x |
f64 |
|
span_y |
f64 |
|
unsprung_fraction |
f64 |
struct MassProperties
Section titled “struct MassProperties”Fields
| field | type | descriptor |
|---|---|---|
total_mass_kg |
f64 |
|
sprung_mass_kg |
f64 |
|
unsprung_mass_kg |
f64 |
|
cg_x_m |
f64 |
|
cg_y_m |
f64 |
|
cg_z_m |
f64 |
|
ixx_kg_m2 |
f64 |
|
iyy_kg_m2 |
f64 |
|
izz_kg_m2 |
f64 |
|
front_axle_load_kg |
f64 |
|
rear_axle_load_kg |
f64 |
|
front_mass_fraction |
f64 |
|
wheelbase_m |
f64 |
|
cg_height_m |
f64 |
|
part_count |
int |
|
instance_count |
int |
|
closure_residual_kg |
f64 |
struct LoadPath
Section titled “struct LoadPath”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
kind |
LoadPathKind |
|
part_ids |
list[str] |
|
rated_load_n |
f64 |
|
utilisation |
f64 |
struct LoadPathReaction
Section titled “struct LoadPathReaction”Fields
| field | type | descriptor |
|---|---|---|
path_id |
str |
|
element_index |
int |
|
part_id |
str |
|
role |
str |
|
force_n |
f64 |
|
moment_nm |
f64 |
struct LoadPathResult
Section titled “struct LoadPathResult”Fields
| field | type | descriptor |
|---|---|---|
path_id |
str |
|
name |
str |
|
kind |
LoadPathKind |
|
applied_n |
f64 |
|
reaction_n |
f64 |
|
residual_n |
f64 |
|
peak_force_n |
f64 |
|
rated_load_n |
f64 |
|
utilisation |
f64 |
|
reactions |
list[LoadPathReaction] |
struct LoadCase
Section titled “struct LoadCase”Fields
| field | type | descriptor |
|---|---|---|
vertical_n |
f64 |
|
longitudinal_n |
f64 |
|
lateral_n |
f64 |
|
results |
list[LoadPathResult] |
|
equilibrium_residual_n |
f64 |
|
moment_residual_nm |
f64 |
|
worst_path_id |
str |
|
worst_utilisation |
f64 |
struct SuspensionState
Section titled “struct SuspensionState”Fields
| field | type | descriptor |
|---|---|---|
corner_id |
str |
|
vertical_load_n |
f64 |
|
arm_rotation_rad |
f64 |
|
wheel_travel_m |
f64 |
|
spring_stroke_m |
f64 |
|
damper_length_m |
f64 |
|
spring_force_n |
f64 |
|
wheel_rate_n_m |
f64 |
|
motion_ratio |
f64 |
|
camber_deg |
f64 |
|
camber_change_deg |
f64 |
|
toe_deg |
f64 |
|
toe_change_deg |
f64 |
|
kingpin_rotation_rad |
f64 |
|
jounce_limit_m |
f64 |
|
rebound_limit_m |
f64 |
|
travel_utilisation |
f64 |
|
at_limit |
bool |
struct ThermalNode
Section titled “struct ThermalNode”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
part_id |
str |
|
capacity_kj_k |
f64 |
|
ambient_coupling_w_k |
f64 |
|
initial_temp_c |
f64 |
struct ThermalLink
Section titled “struct ThermalLink”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
from_node_id |
str |
|
to_node_id |
str |
|
conductance_w_k |
f64 |
|
medium |
str |
struct ThermalNetwork
Section titled “struct ThermalNetwork”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
nodes |
list[ThermalNode] |
|
links |
list[ThermalLink] |
struct MobilityReport
Section titled “struct MobilityReport”Fields
| field | type | descriptor |
|---|---|---|
corner_id |
str |
|
link_count |
int |
|
joint_count |
int |
|
joint_dof_sum |
int |
|
gross_mobility |
int |
|
idle_dof |
int |
|
effective_mobility |
int |
|
expected_mobility |
int |
|
independent_loops |
int |
|
loop_closing_joint_ids |
list[str] |
|
ok |
bool |
def interface_specs
Section titled “def interface_specs”def interface_specs() -> list[dict[str, any]]Returns list[dict[str, any]]
Declared part-to-part interfaces. Every row expands into two ports and one terminating mate.
def open_port_specs
Section titled “def open_port_specs”def open_port_specs() -> list[dict[str, any]]Returns list[dict[str, any]]
Declared but intentionally unmated interfaces. None of them is a required port.
def vehicle_ports
Section titled “def vehicle_ports”def vehicle_ports()Full port inventory: two terminated ports per declared interface plus the open optional ports.
def vehicle_mates
Section titled “def vehicle_mates”def vehicle_mates()One mate per declared interface, carrying its preload, sealing, and service classification.
def port_termination_report
Section titled “def port_termination_report”def port_termination_report()Counts required, terminated, open, and multiply-mated ports by scanning the inventory.
def part_placements
Section titled “def part_placements”def part_placements()Placement of every declared part in vehicle coordinates, in the same order as vehicle_parts().
def placement_instances
Section titled “def placement_instances”def placement_instances(placement: PartPlacement)Parameters
| name | type |
|---|---|
placement |
PartPlacement |
Expands a placement pattern into its instance centres. Each row is [mass, x, y, z].
def mass_properties
Section titled “def mass_properties”def mass_properties() -> MassProperties !{}Returns MassProperties
Effects !{}
Total mass, centre of gravity, and inertia tensor summed from the part placements.
Every part is treated as a uniform rectangular prism of its declared extent, transferred to the vehicle centre of mass by the parallel-axis theorem. The closure residual compares the declared part masses against the placement masses and must be exactly zero.
def corner_table
Section titled “def corner_table”def corner_table()The four suspension corners: mirror signs, steering capability, and the subframe they hang from.
def joint
Section titled “def joint”def joint(id: str, name: str, kind: JointKind, parent_part_id: str, child_part_id: str, parent_link: str, child_link: str, axis: list[f64], origin: list[f64], limits: list[f64], ratio: f64, dof: int, idle_dof: int, corner: str, loop_closing: bool)Parameters
| name | type |
|---|---|
id |
str |
name |
str |
kind |
JointKind |
parent_part_id |
str |
child_part_id |
str |
parent_link |
str |
child_link |
str |
axis |
list[f64] |
origin |
list[f64] |
limits |
list[f64] |
ratio |
f64 |
dof |
int |
idle_dof |
int |
corner |
str |
loop_closing |
bool |
Constructs a joint from packed axis, origin, and limit triples.
def corner_joints
Section titled “def corner_joints”def corner_joints(entry: dict[str, any])Parameters
| name | type |
|---|---|
entry |
dict[str, any] |
Every joint of one suspension corner, mirrored from the reference hard points.
def variant_joint_kind
Section titled “def variant_joint_kind”def variant_joint_kind(configuration_variant: str)Parameters
| name | type |
|---|---|
configuration_variant |
str |
Subframe and pack mounts are elastomeric bolted joints conventionally and bonded structural joints in the CircuitFrame variant.
def structural_joints
Section titled “def structural_joints”def structural_joints(configuration_variant: str)Parameters
| name | type |
|---|---|
configuration_variant |
str |
Driveline, body, pack, and equipment joints outside the four suspension corners.
def vehicle_joints_for
Section titled “def vehicle_joints_for”def vehicle_joints_for(configuration_variant: str)Parameters
| name | type |
|---|---|
configuration_variant |
str |
Complete kinematic joint set for one configuration variant.
def vehicle_joints
Section titled “def vehicle_joints”def vehicle_joints()Complete kinematic joint set for the default CircuitFrame configuration.
def corner_loop_joint_ids
Section titled “def corner_loop_joint_ids”def corner_loop_joint_ids(entry: dict[str, any])Parameters
| name | type |
|---|---|
entry |
dict[str, any] |
The joints that form the closed kinematic loop of one corner.
Wheel bearing, brake, wheel, tire, halfshaft, and anti-roll-bar joints are excluded: they extend the corner as an open chain or couple the two corners of an axle, and are analysed separately.
def corner_mobility
Section titled “def corner_mobility”def corner_mobility(entry: dict[str, any]) !{}Parameters
| name | type |
|---|---|
entry |
dict[str, any] |
Effects !{}
Grubler-Kutzbach mobility of one suspension corner loop.
Gross mobility counts every joint freedom; the idle freedoms are the spins of the spherical-spherical steering link and of the spherical-prismatic-spherical damper unit, which move no wheel state. A steered corner must retain one wheel-travel and one steer freedom; a rear corner must retain wheel travel only.
def mobility_reports
Section titled “def mobility_reports”def mobility_reports() !{}Effects !{}
Mobility of all four suspension corner loops.
def arm_rotation_limits
Section titled “def arm_rotation_limits”def arm_rotation_limits() -> list[f64] !{}Returns list[f64]
Effects !{}
Lower control arm rotation at the rebound and jounce stops, from the declared wheel travel.
def corner_pose
Section titled “def corner_pose”def corner_pose(arm_rotation_rad: f64) -> list[f64] !{}Parameters
| name | type |
|---|---|
arm_rotation_rad |
f64 |
Returns list[f64]
Effects !{}
Closes the planar double-wishbone four-bar for a given lower-arm rotation.
Returns [lbj_y, lbj_z, ubj_y, ubj_z, knuckle_rotation_rad, wheel_y, wheel_z, wheel_travel_m]. The upper ball joint is the circle-circle intersection of the upper arm sweep and the rigid knuckle, so wheel travel and camber are consequences of the joint set, not of a fitted curve.
def damper_length
Section titled “def damper_length”def damper_length(arm_rotation_rad: f64)Parameters
| name | type |
|---|---|
arm_rotation_rad |
f64 |
Installed length of the air spring and damper unit for a given lower-arm rotation.
def motion_ratio
Section titled “def motion_ratio”def motion_ratio(arm_rotation_rad: f64)Parameters
| name | type |
|---|---|
arm_rotation_rad |
f64 |
Spring shortening per unit wheel rise, differentiated through the linkage.
def static_corner_load_n
Section titled “def static_corner_load_n”def static_corner_load_n()Static vertical load at one wheel with the vehicle at its declared kerb mass.
def air_spring_reference_pressure
Section titled “def air_spring_reference_pressure”def air_spring_reference_pressure() -> f64 !{}Returns f64
Effects !{}
Absolute bellows pressure that balances the static corner load at design height.
def air_spring_force
Section titled “def air_spring_force”def air_spring_force(arm_rotation_rad: f64, reference_pressure: f64) !{}Parameters
| name | type |
|---|---|
arm_rotation_rad |
f64 |
reference_pressure |
f64 |
Effects !{}
Axial air spring force from the polytropic gas state of the bellows.
def wheel_force
Section titled “def wheel_force”def wheel_force(arm_rotation_rad: f64, reference_pressure: f64) -> f64 !{}Parameters
| name | type |
|---|---|
arm_rotation_rad |
f64 |
reference_pressure |
f64 |
Returns f64
Effects !{}
Vertical wheel force supported by the corner, by virtual work through the motion ratio.
def vector_rotate
Section titled “def vector_rotate”def vector_rotate(vector: list[f64], axis: list[f64], angle: f64)Parameters
| name | type |
|---|---|
vector |
list[f64] |
axis |
list[f64] |
angle |
f64 |
Rodrigues rotation of a vector about a unit axis.
def tie_rod_residual
Section titled “def tie_rod_residual”def tie_rod_residual(pose: list[f64], steer_rotation_rad: f64)Parameters
| name | type |
|---|---|
pose |
list[f64] |
steer_rotation_rad |
f64 |
Length error of the steering link when the knuckle is rotated about its kingpin axis.
def bump_steer
Section titled “def bump_steer”def bump_steer(pose: list[f64])Parameters
| name | type |
|---|---|
pose |
list[f64] |
Kingpin rotation forced by the fixed-length steering link, and the toe change it produces.
Returns [kingpin_rotation_rad, toe_change_deg]. Solved with a damped Newton iteration seeded at the design position, so it tracks the physically near root of the length constraint.
def suspension_travel
Section titled “def suspension_travel”def suspension_travel(vertical_load_n: f64) -> SuspensionState !{}Parameters
| name | type |
|---|---|
vertical_load_n |
f64 |
Returns SuspensionState
Effects !{}
Solves the corner for a vertical wheel load and reports the resulting wheel motion.
The load is balanced against the polytropic air spring through the linkage motion ratio; the lower-arm rotation that satisfies it is bisected between the rebound and jounce stops. Camber and toe are then read out of the four-bar closure and the steering-link length constraint, so the wheel pose is a consequence of the declared joints.
def load_path_declarations
Section titled “def load_path_declarations”def load_path_declarations()Declared load paths from load introduction to structural reaction, in transmission order.
Utilisation is left at zero here; load_paths() fills it from the reference static load case.
def corner_statics
Section titled “def corner_statics”def corner_statics(lateral_n: f64, vertical_n: f64) !{}Parameters
| name | type |
|---|---|
lateral_n |
f64 |
vertical_n |
f64 |
Effects !{}
Static equilibrium of one corner at the design position.
Two 3x3 solves: the knuckle against the lower ball joint reaction and the axial upper-arm force, then the lower arm against the damper force and its inboard pivot reaction. Returns [lower_ball_y, lower_ball_z, upper_arm_axial, damper_axial, pivot_y, pivot_z, chassis_y, chassis_z, force_residual, moment_residual].
def load_paths
Section titled “def load_paths”def load_paths() !{}Effects !{}
Declared load paths with the utilisation each reaches under the reference static load case.
def reaction
Section titled “def reaction”def reaction(path_id: str, index: int, part_id: str, role: str, force_n: f64, moment_nm: f64)Parameters
| name | type |
|---|---|
path_id |
str |
index |
int |
part_id |
str |
role |
str |
force_n |
f64 |
moment_nm |
f64 |
One element of a load path chain.
def shared_path_result
Section titled “def shared_path_result”def shared_path_result(path: LoadPath, applied_n: f64, shares: list[f64], roles: list[str])Parameters
| name | type |
|---|---|
path |
LoadPath |
applied_n |
f64 |
shares |
list[f64] |
roles |
list[str] |
Distributes an applied load over a branching path whose shares sum to one.
def load_path_reactions
Section titled “def load_path_reactions”def load_path_reactions(vertical_n: f64, longitudinal_n: f64, lateral_n: f64) !{}Parameters
| name | type |
|---|---|
vertical_n |
f64 |
longitudinal_n |
f64 |
lateral_n |
f64 |
Effects !{}
Reactions along every declared load path for one static load case.
The vertical and lateral corner paths are closed by the two 3x3 equilibrium solves in corner_statics; the remaining paths distribute their input over branch shares that sum to one. Every path reports the residual between the applied load and the summed reaction.
def thermal_network
Section titled “def thermal_network”def thermal_network()Coupled lumped thermal network: the coolant loop is a shared node, not a fixed sink.
Heat leaves a component into the glycol loop, travels to the chiller and the cabin through the heat pump, and only then reaches ambient through the radiator and the body skin.
def isolated_thermal_network
Section titled “def isolated_thermal_network”def isolated_thermal_network()The same graph with every ambient coupling removed, for closed-system energy checks.
def node_index
Section titled “def node_index”def node_index(network: ThermalNetwork, node_id: str)Parameters
| name | type |
|---|---|
network |
ThermalNetwork |
node_id |
str |
Position of a node in the network ordering, or -1 when it is absent.
def thermal_step_network
Section titled “def thermal_step_network”def thermal_step_network(network: ThermalNetwork, temps: list[f64], heat_w: list[f64], ambient_c: f64, dt_s: f64)Parameters
| name | type |
|---|---|
network |
ThermalNetwork |
temps |
list[f64] |
heat_w |
list[f64] |
ambient_c |
f64 |
dt_s |
f64 |
Advances an explicit lumped-capacitance network one step.
C_i dT_i/dt = Q_i + sum_j k_ij (T_j - T_i) - h_i (T_i - T_amb). The link term is antisymmetric, so with the ambient couplings removed the total stored energy changes by exactly the injected heat.
def thermal_step
Section titled “def thermal_step”def thermal_step(temps: list[f64], heat_w: list[f64], ambient_c: f64, dt_s: f64) -> list[f64] !{}Parameters
| name | type |
|---|---|
temps |
list[f64] |
heat_w |
list[f64] |
ambient_c |
f64 |
dt_s |
f64 |
Returns list[f64]
Effects !{}
Advances the vehicle thermal network one step.
def thermal_initial_temps
Section titled “def thermal_initial_temps”def thermal_initial_temps() -> list[f64] !{}Returns list[f64]
Effects !{}
Declared soak temperatures of the network, in node order.
def thermal_stability_limit_s
Section titled “def thermal_stability_limit_s”def thermal_stability_limit_s()Largest explicit step the network tolerates: min over nodes of 2 C / (sum k + h).
def assembly_export
Section titled “def assembly_export”def assembly_export() -> dict[str, any] !{}Returns dict[str, any]
Effects !{}
Full assembly payload: placements, interfaces, kinematics, load paths, and the thermal graph.
def assembly_equations
Section titled “def assembly_equations”def assembly_equations() -> list[dict[str, str]] !{}Returns list[dict[str, str]]
Effects !{}
Manifest rows for the assembly model, in the same shape as physics.sema:dynamics_equations().
authoring
Section titled “authoring”Bounded natural-language circuit authoring compiled into typed Sema net definitions.
enum AuthoringIntent
Section titled “enum AuthoringIntent”Variants
connectconstrainunknown
struct CircuitAuthoringResult
Section titled “struct CircuitAuthoringResult”Fields
| field | type | descriptor |
|---|---|---|
intent |
AuthoringIntent |
|
accepted |
bool |
|
natural_description |
str |
|
formal_sema |
str |
|
net |
NetDefinition |
|
requirements |
list[str] |
|
assumptions |
list[str] |
|
diagnostics |
list[str] |
def formal_hv_drive_net
Section titled “def formal_hv_drive_net”def formal_hv_drive_net()def formal_zonal_supply_net
Section titled “def formal_zonal_supply_net”def formal_zonal_supply_net()def formal_data_backbone_net
Section titled “def formal_data_backbone_net”def formal_data_backbone_net()def unsupported_net
Section titled “def unsupported_net”def unsupported_net()def compile_circuit_description
Section titled “def compile_circuit_description”def compile_circuit_description(description: str) -> CircuitAuthoringResult !{}Parameters
| name | type |
|---|---|
description |
str |
Returns CircuitAuthoringResult
Effects !{}
candidate
Section titled “candidate”Bounded structural-conductor candidate search with immutable gate results.
struct PanelCandidate
Section titled “struct PanelCandidate”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
base_revision |
int |
|
width_mm |
f64 |
|
thickness_mm |
f64 |
|
length_m |
f64 |
|
mass_kg |
f64 |
|
current_density_a_mm2 |
f64 |
|
voltage_drop_v |
f64 |
|
loss_kw |
f64 |
|
estimated_temp_c |
f64 |
|
mass_reduction_fraction |
f64 |
|
passed |
bool |
|
reason |
str |
def evaluate_panel_candidate
Section titled “def evaluate_panel_candidate”def evaluate_panel_candidate(width_mm: f64, base_revision: int) !{}Parameters
| name | type |
|---|---|
width_mm |
f64 |
base_revision |
int |
Effects !{}
def optimize_panel
Section titled “def optimize_panel”def optimize_panel(base_revision: int) -> dict[str, any] !{}Parameters
| name | type |
|---|---|
base_revision |
int |
Returns dict[str, any]
Effects !{}
domain
Section titled “domain”Canonical automotive identities, completeness, evidence, and interface contracts.
enum Fidelity
Section titled “enum Fidelity”Variants
canonicalderivedreducedillustrativeunknownexcluded
enum PartGroup
Section titled “enum PartGroup”Variants
bodychassisenergydrivemotionthermalelectricalcontrolcabinsafetymanufacturing
enum PortKind
Section titled “enum PortKind”Variants
mechanicalhigh_voltagelow_voltagedatathermalfluidservice
enum AutomationClass
Section titled “enum AutomationClass”Variants
robot_validatedassistedmanualfailedunknown
enum QualificationStatus
Section titled “enum QualificationStatus”Variants
executable_reducedplanned_externalreference_onlyblocked_missing_evidence
enum EvidenceClass
Section titled “enum EvidenceClass”Variants
computationalstructuraldirect_parityreduced_modelmanufacturingnegativeprovenance
enum SourceAuthority
Section titled “enum SourceAuthority”Variants
customer_oemlicensed_benchmarkopen_researchengineered_assumptionunqualified_fixture
struct SourceRecord
Section titled “struct SourceRecord”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
authority |
SourceAuthority |
|
provider |
str |
|
vehicle_model |
str |
|
source_revision |
str |
|
artifact_kind |
str |
|
rights_scope |
str |
|
digest |
str |
|
fidelity |
Fidelity |
|
render_allowed |
bool |
|
simulation_allowed |
bool |
|
export_allowed |
bool |
struct SourceBinding
Section titled “struct SourceBinding”Fields
| field | type | descriptor |
|---|---|---|
entity_id |
str |
|
source_record_id |
str |
|
source_entity_id |
str |
|
fidelity |
Fidelity |
struct ConfigurationDelta
Section titled “struct ConfigurationDelta”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
base_configuration |
str |
|
target_configuration |
str |
|
unchanged_part_ids |
list[str] |
|
removed_route_ids |
list[str] |
|
added_feature_ids |
list[str] |
|
changed_operation_ids |
list[str] |
struct VehicleIdentity
Section titled “struct VehicleIdentity”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
revision |
int |
|
name |
str |
|
category |
str |
|
seats |
int |
|
fidelity |
Fidelity |
struct PartDefinition
Section titled “struct PartDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
group |
PartGroup |
|
count |
int |
|
material |
str |
|
mass_kg |
f64 |
|
fidelity |
Fidelity |
|
opaque |
bool |
|
evidence_ids |
list[str] |
struct PortDefinition
Section titled “struct PortDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
owner_part_id |
str |
|
kind |
PortKind |
|
mate_port_id |
str |
|
required |
bool |
|
terminated |
bool |
struct NetDefinition
Section titled “struct NetDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
domain |
str |
|
source_part_id |
str |
|
target_part_ids |
list[str] |
|
nominal_voltage_v |
f64 |
|
maximum_current_a |
f64 |
|
route |
str |
|
fidelity |
Fidelity |
|
evidence_ids |
list[str] |
struct OperationDefinition
Section titled “struct OperationDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
station |
str |
|
resource |
str |
|
consumed_part_ids |
list[str] |
|
created_features |
list[str] |
|
automation |
AutomationClass |
|
cycle_time_s |
f64 |
|
evidence_ids |
list[str] |
struct ValidationCaseDefinition
Section titled “struct ValidationCaseDefinition”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
domain |
str |
|
authority |
str |
|
method |
str |
|
status |
QualificationStatus |
|
fidelity |
Fidelity |
|
evidence_ids |
list[str] |
struct EvidenceRecord
Section titled “struct EvidenceRecord”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
kind |
EvidenceClass |
|
source |
str |
|
summary |
str |
|
fidelity |
Fidelity |
|
accepted |
bool |
struct CompletenessReport
Section titled “struct CompletenessReport”Fields
| field | type | descriptor |
|---|---|---|
profile |
str |
|
declared_parts |
int |
|
declared_instances |
int |
|
declared_nets |
int |
|
declared_operations |
int |
|
unterminated_required_ports |
int |
|
required_port_inventory_complete |
bool |
|
unknown_operations |
int |
|
major_assembly_complete |
bool |
|
production_complete |
bool |
|
exclusions |
list[str] |
hardware
Section titled “hardware”Engineering specification of the declared drive, brake, tyre, pack and gear hardware.
The vehicle’s mass is summed from the declared part placements in assembly.sema, but until now
nothing that accelerated or stopped that mass was declared: peak torque, drive power, brake force,
regenerative force, pack energy, pack resistance, tyre grip, cornering stiffness and rolling
resistance were all bare literals in physics.sema. This module declares the engineering
parameters of the components vehicle.sema already lists and derives those limits from them, so
the performance follows from the hardware rather than sitting beside it.
Every specification is keyed to the part id it represents, and part_reconciliation() checks that
each id resolves in vehicle_parts(), that the declared instance counts agree, and that the mass
the specification implies matches the mass the part already declares. hardware_limits() fails
loudly on any mismatch, so a specification cannot quietly drift away from the part it describes.
Honesty: these are engineered assumptions for a reconstruction, not OEM data. No motor was
dynamometer tested, no caliper was pressure tested and no cell was cycled. Each declared parameter
carries a solved, correlated or assumed tag in hardware_parameters(), using the same
vocabulary as the aerodynamic and assembly modules: assumed for a bounded engineering choice,
correlated for an empirical closure with a cited form, solved for a value that falls out of a
closed derivation over the declared inputs.
struct TractionMotorSpec
Section titled “struct TractionMotorSpec”One interior-permanent-magnet traction machine, keyed to its declared drive part.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
pole_pairs |
int |
|
rotor_radius_m |
f64 |
|
stack_length_m |
f64 |
|
stator_outer_radius_m |
f64 |
|
magnet_flux_linkage_wb |
f64 |
|
d_axis_inductance_h |
f64 |
|
q_axis_inductance_h |
f64 |
|
peak_phase_current_a |
f64 |
|
continuous_phase_current_a |
f64 |
|
current_angle_deg |
f64 |
|
max_speed_rpm |
f64 |
struct InverterSpec
Section titled “struct InverterSpec”One traction inverter, keyed to its declared power-electronics part.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
dc_link_voltage_v |
f64 |
|
peak_phase_current_a |
f64 |
|
on_state_resistance_ohm |
f64 |
|
housing_mass_kg |
f64 |
|
net_id |
str |
struct BrakeCornerSpec
Section titled “struct BrakeCornerSpec”One brake-by-wire corner module. Both axles are instances of the declared brakes part.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
axle |
str |
|
disc_outer_radius_m |
f64 |
|
disc_inner_radius_m |
f64 |
|
hat_radius_m |
f64 |
|
disc_thickness_m |
f64 |
|
vent_solid_fraction |
f64 |
|
piston_count |
int |
|
piston_bore_m |
f64 |
|
caliper_mass_kg |
f64 |
|
pad_mass_kg |
f64 |
|
actuator_mass_kg |
f64 |
struct TireSpec
Section titled “struct TireSpec”The declared road tyre, in the size notation moulded on its sidewall.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
section_width_m |
f64 |
|
aspect_ratio |
f64 |
|
rim_diameter_in |
f64 |
|
rolling_radius_factor |
f64 |
|
load_index_kg |
f64 |
|
peak_friction |
f64 |
|
slide_friction |
f64 |
|
load_sensitivity |
f64 |
|
friction_reference_load_fraction |
f64 |
|
cornering_stiffness_per_n |
f64 |
|
rolling_coefficient |
f64 |
|
rolling_speed_coefficient |
f64 |
|
carcass_thickness_m |
f64 |
|
carcass_density_kg_m3 |
f64 |
struct PackSpec
Section titled “struct PackSpec”The declared battery modules, described at the cell that fills them.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
module_count |
int |
|
series_per_module |
int |
|
parallel_per_module |
int |
|
cell_nominal_voltage_v |
f64 |
|
cell_capacity_ah |
f64 |
|
cell_resistance_ohm |
f64 |
|
cell_specific_energy_wh_kg |
f64 |
|
module_packaging_factor |
f64 |
|
module_interconnect_resistance_ohm |
f64 |
|
pack_bus_resistance_ohm |
f64 |
|
cell_pulse_charge_c_rate |
f64 |
|
cell_pulse_discharge_c_rate |
f64 |
struct GearStageSpec
Section titled “struct GearStageSpec”One helical reduction stage, described by the two pitch diameters that set its ratio.
Fields
| field | type | descriptor |
|---|---|---|
pinion_pitch_diameter_m |
f64 |
|
wheel_pitch_diameter_m |
f64 |
|
face_width_m |
f64 |
struct GearboxSpec
Section titled “struct GearboxSpec”The declared single-speed reduction gearset, one unit per drive axle.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
unit_count |
int |
|
first |
GearStageSpec |
|
second |
GearStageSpec |
struct MotorLimits
Section titled “struct MotorLimits”What one traction machine can actually deliver through its own inverter.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
inverter_part_id |
str |
|
effective_peak_current_a |
f64 |
|
current_limited_by |
str |
|
peak_torque_nm |
f64 |
|
continuous_torque_nm |
f64 |
|
base_speed_rpm |
f64 |
|
max_speed_rpm |
f64 |
|
constant_power_ratio |
f64 |
|
corner_power_kw |
f64 |
|
stator_flux_linkage_wb |
f64 |
|
power_factor |
f64 |
|
inverter_apparent_power_kva |
f64 |
|
inverter_peak_power_kw |
f64 |
|
inverter_loss_kw |
f64 |
|
dc_link_peak_current_a |
f64 |
|
airgap_shear_stress_kpa |
f64 |
struct BrakeAxleLimits
Section titled “struct BrakeAxleLimits”Both corner modules of one axle, reduced to the numbers the dynamics needs.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
axle |
str |
|
effective_radius_m |
f64 |
|
piston_area_m2 |
f64 |
|
clamp_force_n |
f64 |
|
corner_torque_nm |
f64 |
|
axle_force_n |
f64 |
|
disc_mass_kg |
f64 |
|
disc_thermal_capacity_j_k |
f64 |
struct TireLimits
Section titled “struct TireLimits”The contact-patch model the dynamics reads, evaluated against live corner loads.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
unloaded_radius_m |
f64 |
|
rolling_radius_m |
f64 |
|
rated_load_n |
f64 |
|
friction_reference_load_n |
f64 |
|
peak_friction |
f64 |
|
slide_friction |
f64 |
|
load_sensitivity |
f64 |
|
cornering_stiffness_per_n |
f64 |
|
rolling_coefficient |
f64 |
|
rolling_speed_coefficient |
f64 |
struct PackLimits
Section titled “struct PackLimits”The traction pack, summed from the declared cell out to the terminals.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
cell_count |
int |
|
series_count |
int |
|
parallel_count |
int |
|
energy_kwh |
f64 |
|
nominal_voltage_v |
f64 |
|
resistance_ohm |
f64 |
|
charge_power_kw |
f64 |
|
discharge_power_kw |
f64 |
|
mass_kg |
f64 |
struct PartReconciliation
Section titled “struct PartReconciliation”One declared part, checked against the specification that claims to describe it.
Fields
| field | type | descriptor |
|---|---|---|
part_id |
str |
|
resolved |
bool |
|
declared_count |
int |
|
expected_count |
int |
|
declared_mass_kg |
f64 |
|
derived_mass_kg |
f64 |
|
mass_error_fraction |
f64 |
|
tolerance_fraction |
f64 |
|
derivation |
str |
|
passed |
bool |
struct AxleHardware
Section titled “struct AxleHardware”Everything one axle’s 20 Hz longitudinal solve needs, pre-solved into flat scalars.
Nothing on this struct depends on the live vehicle state, so it is built once with the rest of the hardware. The loop is then left with only the arithmetic that genuinely varies: the friction circle on this axle’s live vertical load, the fade factor at its live disc temperature, and the comparison that decides which of the two is binding. Keeping it flat also keeps it cheap to pass around: the interpreter copies a struct argument by value, so the per-step helpers must never be handed anything that carries the panel basis or a pre-sampled table.
Fields
| field | type | descriptor |
|---|---|---|
axle |
str |
|
brake_axle_force_n |
f64 |
|
drive_force_per_throttle_n |
f64 |
|
power_force_numerator_w |
f64 |
|
power_constraint |
str |
|
peak_friction |
f64 |
|
slide_friction |
f64 |
|
load_sensitivity |
f64 |
|
friction_reference_load_n |
f64 |
|
cornering_stiffness_per_n |
f64 |
|
fade_onset_temp_c |
f64 |
|
fade_rate_per_k |
f64 |
|
fade_floor_fraction |
f64 |
|
disc_thermal_capacity_j_k |
f64 |
struct HardwareLimits
Section titled “struct HardwareLimits”Everything physics.sema used to hardcode, derived from the declared components.
Fields
| field | type | descriptor |
|---|---|---|
front_motor |
MotorLimits |
|
rear_motor |
MotorLimits |
|
front_brake |
BrakeAxleLimits |
|
rear_brake |
BrakeAxleLimits |
|
tire |
TireLimits |
|
pack |
PackLimits |
|
front_axle |
AxleHardware |
|
rear_axle |
AxleHardware |
|
final_drive_ratio |
f64 |
|
gearbox_efficiency |
f64 |
|
motor_efficiency |
f64 |
|
inverter_efficiency |
f64 |
|
max_motor_torque_nm |
f64 |
|
max_drive_power_kw |
f64 |
|
max_brake_force_n |
f64 |
|
max_regenerative_force_n |
f64 |
|
max_regenerative_power_kw |
f64 |
|
regenerative_front_share |
f64 |
|
max_line_pressure_pa |
f64 |
|
pad_friction |
f64 |
|
fade_onset_temp_c |
f64 |
|
fade_rate_per_k |
f64 |
|
fade_floor_fraction |
f64 |
|
disc_cooling_base_w_per_k |
f64 |
|
disc_cooling_speed_w_per_k_mps |
f64 |
|
driveline_top_speed_mps |
f64 |
def front_motor_spec
Section titled “def front_motor_spec”def front_motor_spec()Front machine: smaller rotor, more turns, sized for the lighter front axle duty.
def rear_motor_spec
Section titled “def rear_motor_spec”def rear_motor_spec()Rear machine: longer, wider rotor and roughly twice the current for the primary drive axle.
def front_inverter_spec
Section titled “def front_inverter_spec”def front_inverter_spec()Front inverter: standalone subframe housing, rated above the machine it feeds.
def rear_inverter_spec
Section titled “def rear_inverter_spec”def rear_inverter_spec()Rear inverter: shares the drive-unit casting, and is the binding current limit of that axle.
def front_brake_spec
Section titled “def front_brake_spec”def front_brake_spec()Front corner: 355 mm vented disc, four-piston fixed caliper.
def rear_brake_spec
Section titled “def rear_brake_spec”def rear_brake_spec()Rear corner: 330 mm vented disc, two-piston caliper carrying the park-brake actuator.
def tire_spec
Section titled “def tire_spec”def tire_spec()255/45 R21 summer tyre on all four corners.
def pack_spec
Section titled “def pack_spec”def pack_spec()Twenty-four 9s2p prismatic NMC modules wired in series into one 216s2p traction pack.
def gearbox_spec
Section titled “def gearbox_spec”def gearbox_spec()Two helical stages per drive unit; their pitch diameters are what make the 9.1:1 final drive.
def motor_limits
Section titled “def motor_limits”def motor_limits(motor: TractionMotorSpec, inverter: InverterSpec) !{}Parameters
| name | type |
|---|---|
motor |
TractionMotorSpec |
inverter |
InverterSpec |
Effects !{}
Solve one machine at its own current limit, then at whichever limit its inverter imposes.
Torque is the interior-permanent-magnet expression: magnet torque plus the reluctance term the declared saliency provides at the declared current angle. The base speed is where the stator flux linkage the same operating point produces can no longer be driven at the space-vector voltage ceiling, and the constant-power region runs from there to the declared maximum speed. Power factor comes out of the corner-point voltage and current phasors rather than being declared, so the inverter’s apparent-power rating and the machine’s shaft power stay consistent.
def brake_axle_limits
Section titled “def brake_axle_limits”def brake_axle_limits(corner: BrakeCornerSpec, rolling_radius_m: f64) !{}Parameters
| name | type |
|---|---|
corner |
BrakeCornerSpec |
rolling_radius_m |
f64 |
Effects !{}
Pressure, clamp force, corner torque and the wheel force both corners of one axle can make.
The effective radius is the uniform-pressure integral over the swept annulus, and the disc thermal mass is the vented swept volume in cast iron. That thermal mass is what turns half m v squared into a disc temperature, so a heavier car heats the same disc further on the same stop.
def tire_limits
Section titled “def tire_limits”def tire_limits()Turn the moulded tyre size into the radius, rated load and grip model the dynamics reads.
def pack_limits
Section titled “def pack_limits”def pack_limits()Sum 24 declared modules of cells into pack energy, voltage, resistance, power and mass.
def final_drive_ratio
Section titled “def final_drive_ratio”def final_drive_ratio()The 9.1:1 reduction is the product of the two declared stage pitch-diameter ratios.
def gear_blank_mass
Section titled “def gear_blank_mass”def gear_blank_mass(stage: GearStageSpec)Parameters
| name | type |
|---|---|
stage |
GearStageSpec |
Both gear blanks of one stage as webbed steel discs of their pitch diameter and face width.
def derived_part_mass
Section titled “def derived_part_mass”def derived_part_mass(part_id: str) !{}Parameters
| name | type |
|---|---|
part_id |
str |
Effects !{}
Mass the declared specification implies for one part, summed over its declared instances.
def declared_part
Section titled “def declared_part”def declared_part(part_id: str)Parameters
| name | type |
|---|---|
part_id |
str |
The declared part this specification claims to describe. Fails loudly on an unknown id.
def part_derivation
Section titled “def part_derivation”def part_derivation(part_id: str)Parameters
| name | type |
|---|---|
part_id |
str |
One sentence naming what the derived mass for this part is actually built from.
def reconcile_part
Section titled “def reconcile_part”def reconcile_part(part_id: str, expected_count: int) !{}Parameters
| name | type |
|---|---|
part_id |
str |
expected_count |
int |
Effects !{}
Check one specification against the part it claims to describe: identity, count and mass.
def part_reconciliation
Section titled “def part_reconciliation”def part_reconciliation() !{}Effects !{}
Every specification checked against vehicle_parts(), in the order the drive chain runs.
def net_current_limit
Section titled “def net_current_limit”def net_current_limit(net_id: str)Parameters
| name | type |
|---|---|
net_id |
str |
Declared maximum current of one high-voltage net, read out of vehicle_nets().
def generating_power
Section titled “def generating_power”def generating_power(motor: MotorLimits)Parameters
| name | type |
|---|---|
motor |
MotorLimits |
Electrical power one machine can regenerate: its continuous torque at its own base speed.
A braking burst is short, but the machines still recover at their continuous rating rather than their peak: peak current is a traction-side thermal allowance the pad-and-disc side never needs.
def axle_hardware
Section titled “def axle_hardware”def axle_hardware(axle: str, motor: MotorLimits, brake: BrakeAxleLimits, tire: TireLimits, ratio: f64, efficiency: f64)Parameters
| name | type |
|---|---|
axle |
str |
motor |
MotorLimits |
brake |
BrakeAxleLimits |
tire |
TireLimits |
ratio |
f64 |
efficiency |
f64 |
Collapse one axle’s declared machine, inverter, calipers and tyres into per-step scalars.
def hardware_limits
Section titled “def hardware_limits”def hardware_limits() -> HardwareLimits !{}Returns HardwareLimits
Effects !{}
Derive every performance limit physics.sema needs, and refuse to return a drifted set.
The reconciliation runs first. A specification whose part id no longer resolves, whose instance count no longer matches the declared count, or whose derived mass has drifted outside the stated tolerance stops the build here rather than quietly feeding a wrong limit into the dynamics.
def parameter_row
Section titled “def parameter_row”def parameter_row(part_id: str, key: str, label: str, value: f64, unit: str, source: str, note: str)Parameters
| name | type |
|---|---|
part_id |
str |
key |
str |
label |
str |
value |
f64 |
unit |
str |
source |
str |
note |
str |
One declared scalar with the part it belongs to and how it was arrived at.
def motor_parameters
Section titled “def motor_parameters”def motor_parameters(motor: TractionMotorSpec)Parameters
| name | type |
|---|---|
motor |
TractionMotorSpec |
Provenance rows for one machine, read straight off the specification struct.
def inverter_parameters
Section titled “def inverter_parameters”def inverter_parameters(inverter: InverterSpec)Parameters
| name | type |
|---|---|
inverter |
InverterSpec |
Provenance rows for one inverter, read straight off the specification struct.
def brake_parameters
Section titled “def brake_parameters”def brake_parameters(corner: BrakeCornerSpec)Parameters
| name | type |
|---|---|
corner |
BrakeCornerSpec |
Provenance rows for one brake corner, read straight off the specification struct.
def shared_parameters
Section titled “def shared_parameters”def shared_parameters()Provenance rows for the constants shared across specifications and reconciliations.
def hardware_parameters
Section titled “def hardware_parameters”def hardware_parameters()The whole provenance ledger, built from the specification structs so it cannot drift.
def motor_export
Section titled “def motor_export”def motor_export(limits: MotorLimits)Parameters
| name | type |
|---|---|
limits |
MotorLimits |
def brake_export
Section titled “def brake_export”def brake_export(limits: BrakeAxleLimits)Parameters
| name | type |
|---|---|
limits |
BrakeAxleLimits |
def reconciliation_export
Section titled “def reconciliation_export”def reconciliation_export(row: PartReconciliation)Parameters
| name | type |
|---|---|
row |
PartReconciliation |
def hardware_export
Section titled “def hardware_export”def hardware_export() -> dict[str, any] !{}Returns dict[str, any]
Effects !{}
API payload: the derived limits, the declared parameters behind them, and the reconciliation.
def hardware_equations
Section titled “def hardware_equations”def hardware_equations() -> list[dict[str, str]] !{}Returns list[dict[str, str]]
Effects !{}
Manifest rows for the hardware model, in the same shape as physics.sema:dynamics_equations().
def contains_id
Section titled “def contains_id”def contains_id(seen: list[str], value: str)Parameters
| name | type |
|---|---|
seen |
list[str] |
value |
str |
def ledger_covers
Section titled “def ledger_covers”def ledger_covers(rows: list[dict[str, any]], part_id: str)Parameters
| name | type |
|---|---|
rows |
list[dict[str, any]] |
part_id |
str |
Automotive-native Sema HTTP service for CircuitFrame Lab.
Everything expensive is solved once when the service starts: the source-panel aerodynamic basis, both reduced-order surrogates, the mass and joint budget, and the four printed-circuit boards with their DC nodal solutions. The 20 Hz drive loop and the on-demand wind-tunnel measurement then reuse those caches.
def response
Section titled “def response”def response(status: int, body: any) !{}Parameters
| name | type |
|---|---|
status |
int |
body |
any |
Effects !{}
def error_response
Section titled “def error_response”def error_response(status: int, code: str, detail: str) !{}Parameters
| name | type |
|---|---|
status |
int |
code |
str |
detail |
str |
Effects !{}
def physical_actions_allowed
Section titled “def physical_actions_allowed”def physical_actions_allowed()def evidence_export_allowed
Section titled “def evidence_export_allowed”def evidence_export_allowed()def initial_session
Section titled “def initial_session”def initial_session(context: PhysicsContext)Parameters
| name | type |
|---|---|
context |
PhysicsContext |
def cached_aero_basis
Section titled “def cached_aero_basis”def cached_aero_basis(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def cached_pcb
Section titled “def cached_pcb”def cached_pcb(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def cached_assembly
Section titled “def cached_assembly”def cached_assembly(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def cached_hardware
Section titled “def cached_hardware”def cached_hardware(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
The derived component limits and their reconciliation. Solved once with the context.
def progress_report
Section titled “def progress_report”def progress_report()def contains
Section titled “def contains”def contains(values: list[str], value: str)Parameters
| name | type |
|---|---|
values |
list[str] |
value |
str |
def equation_phase
Section titled “def equation_phase”def equation_phase(id: str, live: bool)Parameters
| name | type |
|---|---|
id |
str |
live |
bool |
Where a body is evaluated: inside every 20 Hz step, once at startup, or only when asked.
def grouped_equations
Section titled “def grouped_equations”def grouped_equations(rows: list[dict[str, str]], group: str, live: bool)Parameters
| name | type |
|---|---|
rows |
list[dict[str, str]] |
group |
str |
live |
bool |
Tag each declared equation with the subsystem that owns it and when its body runs.
def initial_residual_history
Section titled “def initial_residual_history”def initial_residual_history()def pushed_residual_history
Section titled “def pushed_residual_history”def pushed_residual_history(history: dict[str, any], residual: f64, score: f64, sequence: int, activated: bool) !{}Parameters
| name | type |
|---|---|
history |
dict[str, any] |
residual |
f64 |
score |
f64 |
sequence |
int |
activated |
bool |
Effects !{}
Bounded ring of the surrogate-versus-solved residual and the detector score that tracks it.
def idle_sweep
Section titled “def idle_sweep”def idle_sweep()def sweep_command
Section titled “def sweep_command”def sweep_command(index: int, speed_mps: f64)Parameters
| name | type |
|---|---|
index |
int |
speed_mps |
f64 |
Wind speed and bearing that put the apparent wind at the scripted sideslip. Returns [m/s, deg].
The demonstration commands a sideslip, not a wind. A fixed crosswind that walks a parked car clear of the calm model’s window barely moves the apparent angle at motorway speed, so a fixed wind would quietly stop demonstrating anything above about 30 m/s. Solving the wind vector for the target angle keeps it meaningful at any road speed, and both commanded values are published every step so nothing about the operating point is hidden.
def advanced_sweep
Section titled “def advanced_sweep”def advanced_sweep(sweep: dict[str, any], controls: ControlInput)Parameters
| name | type |
|---|---|
sweep |
dict[str, any] |
controls |
ControlInput |
def detector_body
Section titled “def detector_body”def detector_body(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def residual_history_body
Section titled “def residual_history_body”def residual_history_body(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
The whole ring, served on status only; a step carries one new sample and the client appends.
def sweep_body
Section titled “def sweep_body”def sweep_body(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def all_equations
Section titled “def all_equations”def all_equations()def status_body
Section titled “def status_body”def status_body(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def number_field_valid
Section titled “def number_field_valid”def number_field_valid(payload: dict[str, any], field: str, minimum: f64, maximum: f64)Parameters
| name | type |
|---|---|
payload |
dict[str, any] |
field |
str |
minimum |
f64 |
maximum |
f64 |
def measurement_body
Section titled “def measurement_body”def measurement_body(forces: any)Parameters
| name | type |
|---|---|
forces |
any |
def tunnel_response
Section titled “def tunnel_response”def tunnel_response(payload: any, session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
payload |
any |
session |
dict[str, any] |
Effects !{}
On-demand wind-tunnel measurement: the car is held, the stream runs, the loads are integrated.
def step_response
Section titled “def step_response”def step_response(payload: any, session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
payload |
any |
session |
dict[str, any] |
Effects !{}
def reset_session
Section titled “def reset_session”def reset_session(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def reset_response
Section titled “def reset_response”def reset_response(session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
session |
dict[str, any] |
Effects !{}
def candidate_response
Section titled “def candidate_response”def candidate_response(session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
session |
dict[str, any] |
Effects !{}
def commit_response
Section titled “def commit_response”def commit_response(payload: any, session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
payload |
any |
session |
dict[str, any] |
Effects !{}
def assembly_step_response
Section titled “def assembly_step_response”def assembly_step_response(payload: any, session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
payload |
any |
session |
dict[str, any] |
Effects !{}
def sweep_state_body
Section titled “def sweep_state_body”def sweep_state_body(session: dict[str, any])Parameters
| name | type |
|---|---|
session |
dict[str, any] |
def regime_sweep_response
Section titled “def regime_sweep_response”def regime_sweep_response(payload: any, session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
payload |
any |
session |
dict[str, any] |
Effects !{}
Arm or disarm the bounded wind-yaw demonstration; it never advances the causal sequence itself.
Anything short of an explicit {"action": "start"} disarms, so an empty body, {}, an explicit
stop or a cancel flag all reliably stop the demonstration.
def configuration_response
Section titled “def configuration_response”def configuration_response(payload: any, session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
payload |
any |
session |
dict[str, any] |
Effects !{}
def export_response
Section titled “def export_response”def export_response(session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
session |
dict[str, any] |
Effects !{}
def authoring_response
Section titled “def authoring_response”def authoring_response(payload: any) !{}Parameters
| name | type |
|---|---|
payload |
any |
Effects !{}
def live_response
Section titled “def live_response”def live_response(request: dict[str, any], session: dict[str, any]) !{}Parameters
| name | type |
|---|---|
request |
dict[str, any] |
session |
dict[str, any] |
Effects !{}
def serve_live
Section titled “def serve_live”def serve_live(port: int) -> None !{net.listen}Parameters
| name | type |
|---|---|
port |
int |
Returns None
Effects !{net.listen}
CircuitFrame Lab automotive digital-twin entrypoint.
def main
Section titled “def main”def main() !{net.listen}Effects !{net.listen}
Printed-circuit design and DC electrical analysis for the CircuitFrame boards.
Four boards are declared at component, pin, net, trace and via level and then analysed:
- DC modified nodal analysis. Copper branch conductances come from geometry
(R = rho * L / (w * t), rho corrected by the copper temperature coefficient),
every net’s feed pin is held at its declared operating potential, declared loads
inject current at their sink pin and return it at their return pin, and the
reference net pin is the 0 V node.
G v = iis solved withsolve(matrix, rhs). - IPC-2221 external/internal conductor sizing plus the inverted temperature-rise relation, evaluated per trace against the layer’s copper weight.
- IEC 60664-1 creepage screening on the 800 V board.
- A geometric design-rule check (track width, annular ring, pads inside outline).
Every board is an engineered reconstruction published for the CircuitFrame research demo. None of it is an OEM design, an OEM netlist, or a released manufacturing data set. Part numbers name the device class the reconstruction is dimensioned against.
enum PcbLayerFunction
Section titled “enum PcbLayerFunction”Variants
signalplanecoreprepregsoldermaskcoverlay
enum PcbNetClass
Section titled “enum PcbNetClass”Variants
hvpowergroundsignalcanethernetanalog
enum PcbPinKind
Section titled “enum PcbPinKind”Variants
powergroundsignalanalogthermalnc
struct PcbLayer
Section titled “struct PcbLayer”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
function |
PcbLayerFunction |
|
copper_um |
f64 |
|
dielectric_mm |
f64 |
|
dielectric_er |
f64 |
|
material |
str |
struct PcbStackup
Section titled “struct PcbStackup”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
layers |
list[PcbLayer] |
|
total_thickness_mm |
f64 |
|
ipc_class |
int |
|
impedance_target_ohm |
f64 |
struct PcbComponent
Section titled “struct PcbComponent”Fields
| field | type | descriptor |
|---|---|---|
refdes |
str |
|
part_number |
str |
|
description |
str |
|
footprint |
str |
|
pin_count |
int |
|
x_mm |
f64 |
|
y_mm |
f64 |
|
rotation_deg |
f64 |
|
power_dissipation_w |
f64 |
|
mass_g |
f64 |
struct PcbPin
Section titled “struct PcbPin”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
refdes |
str |
|
number |
int |
|
name |
str |
|
net_id |
str |
|
x_mm |
f64 |
|
y_mm |
f64 |
|
kind |
PcbPinKind |
struct PcbNet
Section titled “struct PcbNet”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
net_class |
PcbNetClass |
|
pin_ids |
list[str] |
|
nominal_voltage_v |
f64 |
|
current_a |
f64 |
|
is_reference |
bool |
struct PcbTrace
Section titled “struct PcbTrace”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
net_id |
str |
|
layer_id |
str |
|
width_mm |
f64 |
|
length_mm |
f64 |
|
current_a |
f64 |
|
from_pin_id |
str |
|
to_pin_id |
str |
|
resistance_mohm |
f64 |
|
voltage_drop_mv |
f64 |
|
power_loss_mw |
f64 |
|
temperature_rise_c |
f64 |
|
ipc_min_width_mm |
f64 |
|
compliant |
bool |
struct PcbVia
Section titled “struct PcbVia”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
net_id |
str |
|
drill_mm |
f64 |
|
pad_mm |
f64 |
|
from_layer_id |
str |
|
to_layer_id |
str |
|
current_a |
f64 |
|
resistance_mohm |
f64 |
struct PcbBoard
Section titled “struct PcbBoard”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
name |
str |
|
host_part_id |
str |
|
stackup |
PcbStackup |
|
components |
list[PcbComponent] |
|
pins |
list[PcbPin] |
|
nets |
list[PcbNet] |
|
traces |
list[PcbTrace] |
|
vias |
list[PcbVia] |
|
outline_width_mm |
f64 |
|
outline_height_mm |
f64 |
|
zone |
str |
|
substrate |
str |
struct PcbAnalysis
Section titled “struct PcbAnalysis”Fields
| field | type | descriptor |
|---|---|---|
board_id |
str |
|
node_count |
int |
|
branch_count |
int |
|
source_node |
str |
|
worst_net_id |
str |
|
worst_voltage_drop_mv |
f64 |
|
worst_drop_fraction |
f64 |
|
total_copper_loss_w |
f64 |
|
max_temperature_rise_c |
f64 |
|
ipc_violations |
int |
|
drc_violations |
int |
|
isolation_violations |
int |
|
solver_residual |
f64 |
|
kcl_residual_a |
f64 |
struct PcbRoutePlan
Section titled “struct PcbRoutePlan”Fields
| field | type | descriptor |
|---|---|---|
net_id |
str |
|
layer_id |
str |
|
width_mm |
f64 |
|
topology |
str |
|
via_count |
int |
|
via_drill_mm |
f64 |
|
via_pad_mm |
f64 |
|
via_layer_id |
str |
struct PcbSegment
Section titled “struct PcbSegment”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
net_id |
str |
|
layer_id |
str |
|
width_mm |
f64 |
|
from_pin_id |
str |
|
to_pin_id |
str |
|
via_count |
int |
|
via_drill_mm |
f64 |
|
via_pad_mm |
f64 |
|
via_layer_id |
str |
struct PcbLoad
Section titled “struct PcbLoad”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
board_id |
str |
|
pin_id |
str |
|
return_pin_id |
str |
|
current_a |
f64 |
|
description |
str |
struct PcbSolution
Section titled “struct PcbSolution”Fields
| field | type | descriptor |
|---|---|---|
node_ids |
list[str] |
|
node_voltages |
list[f64] |
|
branch_currents |
list[f64] |
|
fixed_count |
int |
|
solver_residual |
f64 |
|
kcl_residual_a |
f64 |
|
source_current_a |
f64 |
struct PcbIsolationCheck
Section titled “struct PcbIsolationCheck”Fields
| field | type | descriptor |
|---|---|---|
id |
str |
|
board_id |
str |
|
category |
str |
|
from_pin_id |
str |
|
to_pin_id |
str |
|
working_voltage_v |
f64 |
|
required_mm |
f64 |
|
measured_mm |
f64 |
|
reinforced |
bool |
|
compliant |
bool |
def operating_resistivity
Section titled “def operating_resistivity”def operating_resistivity() !{}Effects !{}
def net_class_key
Section titled “def net_class_key”def net_class_key(net_class: PcbNetClass)Parameters
| name | type |
|---|---|
net_class |
PcbNetClass |
def layer_function_key
Section titled “def layer_function_key”def layer_function_key(function: PcbLayerFunction)Parameters
| name | type |
|---|---|
function |
PcbLayerFunction |
def pin_kind_key
Section titled “def pin_kind_key”def pin_kind_key(kind: PcbPinKind)Parameters
| name | type |
|---|---|
kind |
PcbPinKind |
def pcb_pin
Section titled “def pcb_pin”def pcb_pin(refdes: str, number: int, name: str, net_id: str, x_mm: f64, y_mm: f64, kind: PcbPinKind)Parameters
| name | type |
|---|---|
refdes |
str |
number |
int |
name |
str |
net_id |
str |
x_mm |
f64 |
y_mm |
f64 |
kind |
PcbPinKind |
def pcb_load
Section titled “def pcb_load”def pcb_load(board_id: str, pin_id: str, return_pin_id: str, current_a: f64, description: str)Parameters
| name | type |
|---|---|
board_id |
str |
pin_id |
str |
return_pin_id |
str |
current_a |
f64 |
description |
str |
def route_points
Section titled “def route_points”def route_points(x1: f64, y1: f64, x2: f64, y2: f64)Parameters
| name | type |
|---|---|
x1 |
f64 |
y1 |
f64 |
x2 |
f64 |
y2 |
f64 |
def polyline_length_mm
Section titled “def polyline_length_mm”def polyline_length_mm(points: list[list[f64]])Parameters
| name | type |
|---|---|
points |
list[list[f64]] |
def pin_index_map
Section titled “def pin_index_map”def pin_index_map(pins: list[PcbPin]) -> dict[str, int]Parameters
| name | type |
|---|---|
pins |
list[PcbPin] |
Returns dict[str, int]
def layer_depth_mm
Section titled “def layer_depth_mm”def layer_depth_mm(stackup: PcbStackup, layer_id: str)Parameters
| name | type |
|---|---|
stackup |
PcbStackup |
layer_id |
str |
def layer_copper_um
Section titled “def layer_copper_um”def layer_copper_um(stackup: PcbStackup, layer_id: str)Parameters
| name | type |
|---|---|
stackup |
PcbStackup |
layer_id |
str |
def layer_is_external
Section titled “def layer_is_external”def layer_is_external(stackup: PcbStackup, layer_id: str)Parameters
| name | type |
|---|---|
stackup |
PcbStackup |
layer_id |
str |
def stackup_thickness_mm
Section titled “def stackup_thickness_mm”def stackup_thickness_mm(layers: list[PcbLayer])Parameters
| name | type |
|---|---|
layers |
list[PcbLayer] |
def ipc_min_width_mm
Section titled “def ipc_min_width_mm”def ipc_min_width_mm(current_a: f64, copper_um: f64, external: bool) !{}Parameters
| name | type |
|---|---|
current_a |
f64 |
copper_um |
f64 |
external |
bool |
Effects !{}
def ipc_temperature_rise_c
Section titled “def ipc_temperature_rise_c”def ipc_temperature_rise_c(current_a: f64, width_mm: f64, copper_um: f64, external: bool) !{}Parameters
| name | type |
|---|---|
current_a |
f64 |
width_mm |
f64 |
copper_um |
f64 |
external |
bool |
Effects !{}
def iec60664_creepage_mm
Section titled “def iec60664_creepage_mm”def iec60664_creepage_mm(working_voltage_v: f64)Parameters
| name | type |
|---|---|
working_voltage_v |
f64 |
def route_plan_for
Section titled “def route_plan_for”def route_plan_for(net: PcbNet, defaults: list[PcbRoutePlan], overrides: list[PcbRoutePlan])Parameters
| name | type |
|---|---|
net |
PcbNet |
defaults |
list[PcbRoutePlan] |
overrides |
list[PcbRoutePlan] |
def build_segments
Section titled “def build_segments”def build_segments(nets: list[PcbNet], defaults: list[PcbRoutePlan], overrides: list[PcbRoutePlan])Parameters
| name | type |
|---|---|
nets |
list[PcbNet] |
defaults |
list[PcbRoutePlan] |
overrides |
list[PcbRoutePlan] |
def segment_polyline
Section titled “def segment_polyline”def segment_polyline(segment: PcbSegment, pins: list[PcbPin], index_of: dict[str, int])Parameters
| name | type |
|---|---|
segment |
PcbSegment |
pins |
list[PcbPin] |
index_of |
dict[str, int] |
def single_via_resistance_ohm
Section titled “def single_via_resistance_ohm”def single_via_resistance_ohm(stackup: PcbStackup, segment: PcbSegment) !{}Parameters
| name | type |
|---|---|
stackup |
PcbStackup |
segment |
PcbSegment |
Effects !{}
def via_series_resistance_ohm
Section titled “def via_series_resistance_ohm”def via_series_resistance_ohm(stackup: PcbStackup, segment: PcbSegment) !{}Parameters
| name | type |
|---|---|
stackup |
PcbStackup |
segment |
PcbSegment |
Effects !{}
def copper_resistances_ohm
Section titled “def copper_resistances_ohm”def copper_resistances_ohm(stackup: PcbStackup, segments: list[PcbSegment], lengths_mm: list[f64]) !{}Parameters
| name | type |
|---|---|
stackup |
PcbStackup |
segments |
list[PcbSegment] |
lengths_mm |
list[f64] |
Effects !{}
def solve_linear
Section titled “def solve_linear”def solve_linear(matrix: list[list[f64]], rhs: list[f64])Parameters
| name | type |
|---|---|
matrix |
list[list[f64]] |
rhs |
list[f64] |
def solve_network
Section titled “def solve_network”def solve_network(pins: list[PcbPin], nets: list[PcbNet], segments: list[PcbSegment], resistances_ohm: list[f64], loads: list[PcbLoad])Parameters
| name | type |
|---|---|
pins |
list[PcbPin] |
nets |
list[PcbNet] |
segments |
list[PcbSegment] |
resistances_ohm |
list[f64] |
loads |
list[PcbLoad] |
def assemble_board
Section titled “def assemble_board”def assemble_board(board_id: str, name: str, host_part_id: str, zone: str, substrate: str, stackup: PcbStackup, components: list[PcbComponent], pins: list[PcbPin], nets: list[PcbNet], defaults: list[PcbRoutePlan], overrides: list[PcbRoutePlan], loads: list[PcbLoad], outline_width_mm: f64, outline_height_mm: f64) !{}Parameters
| name | type |
|---|---|
board_id |
str |
name |
str |
host_part_id |
str |
zone |
str |
substrate |
str |
stackup |
PcbStackup |
components |
list[PcbComponent] |
pins |
list[PcbPin] |
nets |
list[PcbNet] |
defaults |
list[PcbRoutePlan] |
overrides |
list[PcbRoutePlan] |
loads |
list[PcbLoad] |
outline_width_mm |
f64 |
outline_height_mm |
f64 |
Effects !{}
def bms_slave_stackup
Section titled “def bms_slave_stackup”def bms_slave_stackup()def bms_slave_components
Section titled “def bms_slave_components”def bms_slave_components()def bms_slave_pins
Section titled “def bms_slave_pins”def bms_slave_pins()def bms_slave_nets
Section titled “def bms_slave_nets”def bms_slave_nets()def bms_slave_plans
Section titled “def bms_slave_plans”def bms_slave_plans()def bms_slave_overrides
Section titled “def bms_slave_overrides”def bms_slave_overrides()def bms_slave_loads
Section titled “def bms_slave_loads”def bms_slave_loads()def bms_slave_board
Section titled “def bms_slave_board”def bms_slave_board() !{}Effects !{}
def front_zone_stackup
Section titled “def front_zone_stackup”def front_zone_stackup()def front_zone_components
Section titled “def front_zone_components”def front_zone_components()def front_zone_pins
Section titled “def front_zone_pins”def front_zone_pins()def front_zone_12v_load_a
Section titled “def front_zone_12v_load_a”def front_zone_12v_load_a()def front_zone_5v_load_a
Section titled “def front_zone_5v_load_a”def front_zone_5v_load_a()def front_zone_3v3_load_a
Section titled “def front_zone_3v3_load_a”def front_zone_3v3_load_a()def front_zone_48v_load_a
Section titled “def front_zone_48v_load_a”def front_zone_48v_load_a()def front_zone_nets
Section titled “def front_zone_nets”def front_zone_nets()def front_zone_plans
Section titled “def front_zone_plans”def front_zone_plans()def front_zone_overrides
Section titled “def front_zone_overrides”def front_zone_overrides()def front_zone_loads
Section titled “def front_zone_loads”def front_zone_loads()def front_zone_board
Section titled “def front_zone_board”def front_zone_board() !{}Effects !{}
def hv_junction_stackup
Section titled “def hv_junction_stackup”def hv_junction_stackup()def hv_junction_components
Section titled “def hv_junction_components”def hv_junction_components()def hv_junction_pins
Section titled “def hv_junction_pins”def hv_junction_pins()def hv_junction_12v_load_a
Section titled “def hv_junction_12v_load_a”def hv_junction_12v_load_a()def hv_junction_5v_load_a
Section titled “def hv_junction_5v_load_a”def hv_junction_5v_load_a()def hv_junction_nets
Section titled “def hv_junction_nets”def hv_junction_nets()def hv_junction_plans
Section titled “def hv_junction_plans”def hv_junction_plans()def hv_junction_overrides
Section titled “def hv_junction_overrides”def hv_junction_overrides()def hv_junction_loads
Section titled “def hv_junction_loads”def hv_junction_loads()def hv_junction_board
Section titled “def hv_junction_board”def hv_junction_board() !{}Effects !{}
def sill_panel_stackup
Section titled “def sill_panel_stackup”def sill_panel_stackup()def sill_panel_components
Section titled “def sill_panel_components”def sill_panel_components()def sill_panel_pins
Section titled “def sill_panel_pins”def sill_panel_pins()def sill_panel_nets
Section titled “def sill_panel_nets”def sill_panel_nets()def sill_panel_plans
Section titled “def sill_panel_plans”def sill_panel_plans()def sill_panel_overrides
Section titled “def sill_panel_overrides”def sill_panel_overrides()def sill_panel_loads
Section titled “def sill_panel_loads”def sill_panel_loads()def sill_panel_board
Section titled “def sill_panel_board”def sill_panel_board() !{}Effects !{}
def board_loads
Section titled “def board_loads”def board_loads(board_id: str)Parameters
| name | type |
|---|---|
board_id |
str |
def trace_branch_resistance_ohm
Section titled “def trace_branch_resistance_ohm”def trace_branch_resistance_ohm(board: PcbBoard, trace: PcbTrace)Parameters
| name | type |
|---|---|
board |
PcbBoard |
trace |
PcbTrace |
def board_solution
Section titled “def board_solution”def board_solution(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def node_voltage
Section titled “def node_voltage”def node_voltage(solution: PcbSolution, pin_id: str)Parameters
| name | type |
|---|---|
solution |
PcbSolution |
pin_id |
str |
def net_drop_v
Section titled “def net_drop_v”def net_drop_v(board: PcbBoard, solution: PcbSolution, net: PcbNet)Parameters
| name | type |
|---|---|
board |
PcbBoard |
solution |
PcbSolution |
net |
PcbNet |
def board_drc_violations
Section titled “def board_drc_violations”def board_drc_violations(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def isolated_domain_nets
Section titled “def isolated_domain_nets”def isolated_domain_nets(board_id: str)Parameters
| name | type |
|---|---|
board_id |
str |
def isolation_required_mm
Section titled “def isolation_required_mm”def isolation_required_mm(same_domain: bool, potential_difference_v: f64)Parameters
| name | type |
|---|---|
same_domain |
bool |
potential_difference_v |
f64 |
def net_lookup
Section titled “def net_lookup”def net_lookup(nets: list[PcbNet]) -> dict[str, int]Parameters
| name | type |
|---|---|
nets |
list[PcbNet] |
Returns dict[str, int]
def pcb_isolation_checks
Section titled “def pcb_isolation_checks”def pcb_isolation_checks(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def analyze_solved
Section titled “def analyze_solved”def analyze_solved(board: PcbBoard, field: PcbSolution, gates: list[PcbIsolationCheck])Parameters
| name | type |
|---|---|
board |
PcbBoard |
field |
PcbSolution |
gates |
list[PcbIsolationCheck] |
def pcb_analyze
Section titled “def pcb_analyze”def pcb_analyze(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def pcb_netlist_text
Section titled “def pcb_netlist_text”def pcb_netlist_text(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def pcb_boards
Section titled “def pcb_boards”def pcb_boards() !{}Effects !{}
def component_kind
Section titled “def component_kind”def component_kind(refdes: str)Parameters
| name | type |
|---|---|
refdes |
str |
def component_extent
Section titled “def component_extent”def component_extent(board: PcbBoard, refdes: str)Parameters
| name | type |
|---|---|
board |
PcbBoard |
refdes |
str |
def pad_extent
Section titled “def pad_extent”def pad_extent(footprint: str)Parameters
| name | type |
|---|---|
footprint |
str |
def layer_kind
Section titled “def layer_kind”def layer_kind(layer: PcbLayer)Parameters
| name | type |
|---|---|
layer |
PcbLayer |
def layer_color
Section titled “def layer_color”def layer_color(index: int, layer: PcbLayer)Parameters
| name | type |
|---|---|
index |
int |
layer |
PcbLayer |
def substrate_kind
Section titled “def substrate_kind”def substrate_kind(board_id: str)Parameters
| name | type |
|---|---|
board_id |
str |
def viewer_net_class
Section titled “def viewer_net_class”def viewer_net_class(net_class: PcbNetClass)Parameters
| name | type |
|---|---|
net_class |
PcbNetClass |
def vehicle_net_for
Section titled “def vehicle_net_for”def vehicle_net_for(net_id: str)Parameters
| name | type |
|---|---|
net_id |
str |
def board_pours
Section titled “def board_pours”def board_pours(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def board_export
Section titled “def board_export”def board_export(board: PcbBoard)Parameters
| name | type |
|---|---|
board |
PcbBoard |
def pcb_export
Section titled “def pcb_export”def pcb_export() -> dict[str, any] !{}Returns dict[str, any]
Effects !{}
def pcb_equations
Section titled “def pcb_equations”def pcb_equations() -> list[dict[str, str]] !{}Returns list[dict[str, str]]
Effects !{}
physics
Section titled “physics”Coupled electric-vehicle dynamics over the panel-solved aerodynamic field.
Aerodynamic loads come from aero.sema: a constant-strength source-panel solve with an image
ground plane, closed by an empirical viscous and base-pressure model. Mass, centre of gravity and
yaw inertia come from assembly.sema, summed from the declared part placements rather than typed
in as literals. The thermal state is the eight-node coolant network from the same module, so heat
moves between components instead of each one sinking independently to ambient.
Two reduced-order surrogates are fitted to the panel solve at startup and are continuously scored against it while the vehicle runs; that residual is what drives the model-adaptation lifecycle. The dynamics themselves always integrate the solved loads, never the surrogate.
struct ControlInput
Section titled “struct ControlInput”Fields
| field | type | descriptor |
|---|---|---|
throttle |
f64 |
|
brake |
f64 |
|
steering |
f64 |
|
wind_mps |
f64 |
|
wind_yaw_deg |
f64 |
struct VehicleState
Section titled “struct VehicleState”Fields
| field | type | descriptor |
|---|---|---|
time_s |
f64 |
|
distance_m |
f64 |
|
speed_mps |
f64 |
|
lateral_speed_mps |
f64 |
|
acceleration_mps2 |
f64 |
|
lateral_acceleration_mps2 |
f64 |
|
yaw_rate_rps |
f64 |
|
yaw_angle_rad |
f64 |
|
steering_angle_rad |
f64 |
|
battery_soc |
f64 |
|
thermal_temps_c |
list[f64] |
|
battery_temp_c |
f64 |
|
motor_temp_c |
f64 |
|
inverter_temp_c |
f64 |
|
coolant_temp_c |
f64 |
|
cabin_temp_c |
f64 |
|
tire_temp_c |
f64 |
|
front_disc_temp_c |
f64 |
|
rear_disc_temp_c |
f64 |
|
motor_rpm |
f64 |
|
model_id |
str |
struct AeroLoads
Section titled “struct AeroLoads”Body-axis aerodynamic loads. Positive drag resists forward travel; positive lift is upward.
Fields
| field | type | descriptor |
|---|---|---|
air_speed_mps |
f64 |
|
sideslip_rad |
f64 |
|
dynamic_pressure_pa |
f64 |
|
cd |
f64 |
|
cy |
f64 |
|
cl |
f64 |
|
cmz |
f64 |
|
drag_n |
f64 |
|
side_n |
f64 |
|
lift_n |
f64 |
|
front_lift_n |
f64 |
|
rear_lift_n |
f64 |
|
yaw_moment_nm |
f64 |
struct AeroSurrogate
Section titled “struct AeroSurrogate”Even-in-yaw drag and lift, odd-in-yaw side force and yaw moment, fitted to the panel solve.
Fields
| field | type | descriptor |
|---|---|---|
model_id |
str |
|
cd0 |
f64 |
|
cd2 |
f64 |
|
cd4 |
f64 |
|
cy1 |
f64 |
|
cy3 |
f64 |
|
cl0 |
f64 |
|
cl2 |
f64 |
|
cl4 |
f64 |
|
cmz1 |
f64 |
|
cmz3 |
f64 |
|
fit_max_yaw_rad |
f64 |
|
fit_nodes |
int |
|
fit_rms_cd |
f64 |
struct VehicleParameters
Section titled “struct VehicleParameters”Inertial and geometric parameters derived from the declared assembly, not typed in.
Fields
| field | type | descriptor |
|---|---|---|
mass_kg |
f64 |
|
cg_to_front_axle_m |
f64 |
|
cg_to_rear_axle_m |
f64 |
|
cg_height_m |
f64 |
|
yaw_inertia_kg_m2 |
f64 |
|
wheelbase_m |
f64 |
|
front_mass_fraction |
f64 |
|
track_m |
f64 |
struct AeroHoldout
Section titled “struct AeroHoldout”Fields
| field | type | descriptor |
|---|---|---|
active_training_error |
f64 |
|
candidate_training_error |
f64 |
|
active_validation_error |
f64 |
|
candidate_validation_error |
f64 |
struct CornerSuspension
Section titled “struct CornerSuspension”One wheel station read out of the declared double-wishbone closure.
Fields
| field | type | descriptor |
|---|---|---|
vertical_load_n |
f64 |
|
wheel_travel_m |
f64 |
|
camber_deg |
f64 |
|
toe_deg |
f64 |
|
damper_length_m |
f64 |
|
spring_force_n |
f64 |
|
travel_utilisation |
f64 |
struct SuspensionCorners
Section titled “struct SuspensionCorners”The four wheel stations, each solved at its own vertical load.
Lateral load transfer moves normal force onto the outside pair, so the closure returns different travel, camber and toe per side. With no lateral acceleration the two sides are identical.
Fields
| field | type | descriptor |
|---|---|---|
front_left |
CornerSuspension |
|
front_right |
CornerSuspension |
|
rear_left |
CornerSuspension |
|
rear_right |
CornerSuspension |
struct SuspensionTable
Section titled “struct SuspensionTable”Corner closures pre-solved on a load grid, in strictly increasing load order.
Fields
| field | type | descriptor |
|---|---|---|
loads_n |
list[f64] |
|
corners |
list[CornerSuspension] |
struct AxleLongitudinal
Section titled “struct AxleLongitudinal”One axle’s longitudinal state: what its hardware could do, and what the tyres allowed.
Both the brake and the drive side report which declared component set the number. At the tyre limit a stop is mass-independent; the moment the caliper is the binding constraint it is not, and the difference is visible here rather than folded into one force.
Fields
| field | type | descriptor |
|---|---|---|
axle |
str |
|
normal_force_n |
f64 |
|
friction_coefficient |
f64 |
|
tire_limit_n |
f64 |
|
brake_capability_n |
f64 |
|
brake_force_n |
f64 |
|
brake_constraint |
str |
|
motor_force_n |
f64 |
|
power_force_n |
f64 |
|
drive_force_n |
f64 |
|
drive_constraint |
str |
|
cornering_stiffness_n_rad |
f64 |
|
disc_temp_c |
f64 |
|
fade_fraction |
f64 |
struct LongitudinalDemand
Section titled “struct LongitudinalDemand”The live inputs one longitudinal solve needs, flattened out of the state and the aero solve.
Built once per step and handed to the four axle solves. Everything on it is a scalar so that the per-call copy the interpreter makes stays cheap.
Fields
| field | type | descriptor |
|---|---|---|
throttle |
f64 |
|
brake |
f64 |
|
power_limit |
f64 |
|
speed_mps |
f64 |
|
previous_acceleration_mps2 |
f64 |
|
front_disc_temp_c |
f64 |
|
rear_disc_temp_c |
f64 |
|
front_lift_n |
f64 |
|
rear_lift_n |
f64 |
|
lift_n |
f64 |
|
drag_n |
f64 |
struct LongitudinalSolution
Section titled “struct LongitudinalSolution”Both axles solved together under one load transfer, with the resulting body acceleration.
Fields
| field | type | descriptor |
|---|---|---|
front |
AxleLongitudinal |
|
rear |
AxleLongitudinal |
|
rolling_force_n |
f64 |
|
brake_line_pressure_pa |
f64 |
|
brake_force_n |
f64 |
|
drive_force_n |
f64 |
|
motor_torque_nm |
f64 |
|
acceleration_mps2 |
f64 |
|
limiting_constraint |
str |
struct PhysicsContext
Section titled “struct PhysicsContext”Everything expensive is built once here: the panel basis, the mass budget and both surrogates.
Fields
| field | type | descriptor |
|---|---|---|
basis |
AeroBasis |
|
parameters |
VehicleParameters |
|
calm |
AeroSurrogate |
|
crosswind |
AeroSurrogate |
|
holdout |
AeroHoldout |
|
suspension |
SuspensionTable |
|
hardware |
HardwareLimits |
struct DynamicsFrame
Section titled “struct DynamicsFrame”Fields
| field | type | descriptor |
|---|---|---|
state |
VehicleState |
|
controls |
ControlInput |
|
sequence |
int |
|
apparent_air_speed_mps |
f64 |
|
sideslip_rad |
f64 |
|
dynamic_pressure_pa |
f64 |
|
solved |
AeroLoads |
|
surrogate |
AeroLoads |
|
model_cd |
f64 |
|
reference_cd |
f64 |
|
cd_pressure |
f64 |
|
cd_base |
f64 |
|
cd_friction |
f64 |
|
cd_wheels |
f64 |
|
drag_force_n |
f64 |
|
lift_force_n |
f64 |
|
side_force_n |
f64 |
|
aero_yaw_moment_nm |
f64 |
|
rolling_force_n |
f64 |
|
brake_force_n |
f64 |
|
regenerative_force_n |
f64 |
|
drive_force_n |
f64 |
|
wheel_torque_nm |
f64 |
|
tire_slip_ratio |
f64 |
|
front_axle_normal_force_n |
f64 |
|
rear_axle_normal_force_n |
f64 |
|
tire_utilization_fraction |
f64 |
|
front_axle |
AxleLongitudinal |
|
rear_axle |
AxleLongitudinal |
|
limiting_constraint |
str |
|
brake_line_pressure_pa |
f64 |
|
brake_fade_fraction |
f64 |
|
front_disc_temp_c |
f64 |
|
rear_disc_temp_c |
f64 |
|
suspension |
SuspensionCorners |
|
mechanical_power_kw |
f64 |
|
electrical_power_kw |
f64 |
|
regenerative_power_kw |
f64 |
|
battery_power_kw |
f64 |
|
battery_loss_kw |
f64 |
|
motor_loss_kw |
f64 |
|
inverter_loss_kw |
f64 |
|
pack_open_circuit_voltage_v |
f64 |
|
pack_voltage_v |
f64 |
|
pack_current_a |
f64 |
|
pack_resistance_ohm |
f64 |
|
power_limit_fraction |
f64 |
|
power_residual_kw |
f64 |
|
aero_residual_fraction |
f64 |
|
reynolds_number |
f64 |
|
separation_x_m |
f64 |
|
cp_min |
f64 |
|
cp_max |
f64 |
|
base_pressure_coefficient |
f64 |
|
wake_width_m |
f64 |
|
wake_shedding_hz |
f64 |
|
turbulence_intensity |
f64 |
|
wake_velocity_deficit_fraction |
f64 |
|
wake_recirculation_length_m |
f64 |
|
dalembert_residual_cd |
f64 |
|
aero_solver_residual |
f64 |
|
aero_force_closure_residual |
f64 |
|
thermal_energy_residual_w |
f64 |
|
energy_residual_fraction |
f64 |
def vehicle_parameters
Section titled “def vehicle_parameters”def vehicle_parameters()Inertia and geometry summed from the declared part placements in assembly.sema.
def least_squares_pair
Section titled “def least_squares_pair”def least_squares_pair(u: list[f64], y: list[f64]) !{}Parameters
| name | type |
|---|---|
u |
list[f64] |
y |
list[f64] |
Effects !{}
Fit y ~ au + bu^2 through the origin by the 2x2 normal equations.
def least_squares_single
Section titled “def least_squares_single”def least_squares_single(u: list[f64], y: list[f64])Parameters
| name | type |
|---|---|
u |
list[f64] |
y |
list[f64] |
Fit y ~ a*u through the origin.
def yaw_fit_nodes
Section titled “def yaw_fit_nodes”def yaw_fit_nodes(count: int, maximum_deg: f64)Parameters
| name | type |
|---|---|
count |
int |
maximum_deg |
f64 |
Yaw stations in radians, evenly spaced from zero to the declared fit boundary.
def fit_surrogate
Section titled “def fit_surrogate”def fit_surrogate(basis: AeroBasis, model_id: str, count: int, maximum_deg: f64, quartic: bool) !{}Parameters
| name | type |
|---|---|
basis |
AeroBasis |
model_id |
str |
count |
int |
maximum_deg |
f64 |
quartic |
bool |
Effects !{}
Least-squares fit of the panel solve over a bounded yaw window.
Drag and lift are even in sideslip, side force and yaw moment are odd, so the basis functions are
beta^2/beta^4 and beta/beta^3 respectively. The zero-yaw intercepts are taken exactly from the
solve so the calibrated C_d(0) is preserved without relying on the fit.
def surrogate_loads
Section titled “def surrogate_loads”def surrogate_loads(surrogate: AeroSurrogate, air_speed_mps: f64, sideslip_rad: f64) !{}Parameters
| name | type |
|---|---|
surrogate |
AeroSurrogate |
air_speed_mps |
f64 |
sideslip_rad |
f64 |
Effects !{}
Evaluate the reduced-order coefficients. Cheap enough to run inside any control loop.
def solved_loads
Section titled “def solved_loads”def solved_loads(forces: AeroForces)Parameters
| name | type |
|---|---|
forces |
AeroForces |
def surrogate_for
Section titled “def surrogate_for”def surrogate_for(context: PhysicsContext, model_id: str)Parameters
| name | type |
|---|---|
context |
PhysicsContext |
model_id |
str |
def relative_drag_error
Section titled “def relative_drag_error”def relative_drag_error(basis: AeroBasis, surrogate: AeroSurrogate, degrees: list[f64]) !{}Parameters
| name | type |
|---|---|
basis |
AeroBasis |
surrogate |
AeroSurrogate |
degrees |
list[f64] |
Effects !{}
Mean relative drag-coefficient error of a surrogate against the panel solve.
def physics_context
Section titled “def physics_context”def physics_context() -> PhysicsContext !{}Returns PhysicsContext
Effects !{}
Build the panel basis, the mass budget and both surrogates. Call once at startup.
def aero_holdout_metrics
Section titled “def aero_holdout_metrics”def aero_holdout_metrics(context: PhysicsContext) -> dict[str, f64] !{}Parameters
| name | type |
|---|---|
context |
PhysicsContext |
Returns dict[str, f64]
Effects !{}
Training and held-out surrogate errors measured against the live panel solve.
def initial_vehicle_state
Section titled “def initial_vehicle_state”def initial_vehicle_state() -> VehicleState !{}Returns VehicleState
Effects !{}
def state_at
Section titled “def state_at”def state_at(speed_mps: f64, battery_soc: f64)Parameters
| name | type |
|---|---|
speed_mps |
f64 |
battery_soc |
f64 |
A settled state at one road speed and state of charge, with cold discs and cold tyres.
def state_carrying
Section titled “def state_carrying”def state_carrying(state: VehicleState, speed_mps: f64)Parameters
| name | type |
|---|---|
state |
VehicleState |
speed_mps |
f64 |
The same thermal and electrical state brought back to speed, for a repeated-stop sequence.
Only the motion resets. Disc, tyre, pack and coolant temperatures carry over, which is the whole point: the heat a stop leaves behind is what the next one starts from.
def with_added_mass
Section titled “def with_added_mass”def with_added_mass(context: PhysicsContext, added_kg: f64)Parameters
| name | type |
|---|---|
context |
PhysicsContext |
added_kg |
f64 |
The same vehicle carrying more mass: everything else declared stays exactly as it is.
Only the inertial parameters move. The motors, calipers, discs and tyres are the ones the parts list declares, which is the whole point of the comparison: what does the declared hardware do when it has more mass to move and to stop.
def apparent_wind
Section titled “def apparent_wind”def apparent_wind(state: VehicleState, controls: ControlInput)Parameters
| name | type |
|---|---|
state |
VehicleState |
controls |
ControlInput |
Apparent-wind magnitude and sideslip in the vehicle frame.
A head-on tunnel wind and forward road speed add; wind_yaw_deg rotates the tunnel wind around
the vehicle, so a parked car in a 27.8 m/s stream reports 27.8 m/s of apparent wind at that yaw.
def corner_state
Section titled “def corner_state”def corner_state(state: SuspensionState)Parameters
| name | type |
|---|---|
state |
SuspensionState |
Narrow a solved corner to the fields the dynamics frame publishes.
def suspension_table
Section titled “def suspension_table”def suspension_table()Pre-solve the declared corner closure on a grid uniform in lower-arm rotation.
suspension_travel bisects the arm rotation against the polytropic air spring, which costs far
more than the whole rest of a 20 Hz step. Sampling it once at startup and interpolating keeps the
loop inside its budget without inventing a second suspension model. Rotation-uniform spacing puts
the nodes where the linkage actually moves: the air spring stiffens so sharply that a load-uniform
grid would collapse the working range into a couple of intervals. Rotations that clamp to the same
load at a stop are dropped, so the lookup never meets a zero-width interval.
def interpolate_corner
Section titled “def interpolate_corner”def interpolate_corner(table: SuspensionTable, vertical_load_n: f64)Parameters
| name | type |
|---|---|
table |
SuspensionTable |
vertical_load_n |
f64 |
Read the pre-solved corner grid at one vertical wheel load, linearly between nodes.
def clamped_disc_temp
Section titled “def clamped_disc_temp”def clamped_disc_temp(value: f64)Parameters
| name | type |
|---|---|
value |
f64 |
Hold an integrated disc temperature inside the range the state invariant declares.
def axle_longitudinal
Section titled “def axle_longitudinal”def axle_longitudinal(hardware: AxleHardware, demand: LongitudinalDemand, normal_force_n: f64, disc_temp_c: f64) !{}Parameters
| name | type |
|---|---|
hardware |
AxleHardware |
demand |
LongitudinalDemand |
normal_force_n |
f64 |
disc_temp_c |
f64 |
Effects !{}
One axle solved against its own hardware and its own live vertical load.
The brake side walks the real chain: pedal demand sets line pressure, pressure and piston area
set clamp force, pad friction and the effective radius set corner torque, and the rolling radius
turns that into a wheel force. All of that is fixed by the declared components, so hardware
arrives with it already solved and only the pedal fraction and the fade factor are applied here.
The tyre side is the friction circle on this axle’s live load, with the declared load
sensitivity, so a heavier corner returns less grip per newton. Whichever of the two is smaller
is the force the axle makes, and which one it was is published.
def axle_pass
Section titled “def axle_pass”def axle_pass(front_hardware: AxleHardware, rear_hardware: AxleHardware, demand: LongitudinalDemand, static_front_n: f64, static_rear_n: f64, transfer_n: f64) !{}Parameters
| name | type |
|---|---|
front_hardware |
AxleHardware |
rear_hardware |
AxleHardware |
demand |
LongitudinalDemand |
static_front_n |
f64 |
static_rear_n |
f64 |
transfer_n |
f64 |
Effects !{}
Both axles solved at the load transfer one candidate acceleration implies.
def longitudinal_solution
Section titled “def longitudinal_solution”def longitudinal_solution(parameters: VehicleParameters, hardware: HardwareLimits, demand: LongitudinalDemand) !{}Parameters
| name | type |
|---|---|
parameters |
VehicleParameters |
hardware |
HardwareLimits |
demand |
LongitudinalDemand |
Effects !{}
Solve both axles and the body acceleration together, since each one sets the other.
Load transfer depends on the acceleration, and the acceleration depends on the per-axle limits that the transferred loads produce. One predictor from the previous step’s acceleration and one corrector from this step’s is enough at 20 Hz: the transfer moves by well under a percent over the second pass, and a fixed point is not worth the extra arithmetic inside the loop.
Nothing larger than a flat scalar struct crosses these call boundaries. The interpreter copies a
struct argument by value, and PhysicsContext carries the whole panel basis, so handing it to a
helper that runs four times a step costs milliseconds out of a fifty-millisecond budget.
def step_vehicle
Section titled “def step_vehicle”def step_vehicle(state: VehicleState, controls: ControlInput, duration_s: f64, sequence: int, model_id: str, context: PhysicsContext) -> DynamicsFrame !{}Parameters
| name | type |
|---|---|
state |
VehicleState |
controls |
ControlInput |
duration_s |
f64 |
sequence |
int |
model_id |
str |
context |
PhysicsContext |
Returns DynamicsFrame
Effects !{}
struct StopMeasurement
Section titled “struct StopMeasurement”One full-pedal stop, integrated at the same 20 Hz the live loop runs at.
Fields
| field | type | descriptor |
|---|---|---|
distance_m |
f64 |
|
duration_s |
f64 |
|
mean_deceleration_mps2 |
f64 |
|
front_constraint |
str |
|
rear_constraint |
str |
|
front_disc_temp_c |
f64 |
|
rear_disc_temp_c |
f64 |
|
peak_brake_force_n |
f64 |
struct LaunchMeasurement
Section titled “struct LaunchMeasurement”One full-throttle launch to a target speed, integrated at the same 20 Hz.
Fields
| field | type | descriptor |
|---|---|---|
duration_s |
f64 |
|
distance_m |
f64 |
|
launch_constraint |
str |
|
final_constraint |
str |
|
peak_drive_force_n |
f64 |
def measured_stop
Section titled “def measured_stop”def measured_stop(context: PhysicsContext, initial_speed_mps: f64)Parameters
| name | type |
|---|---|
context |
PhysicsContext |
initial_speed_mps |
f64 |
Brake from one speed to rest at full pedal and report what the vehicle actually did.
def measured_launch
Section titled “def measured_launch”def measured_launch(context: PhysicsContext, target_speed_mps: f64)Parameters
| name | type |
|---|---|
context |
PhysicsContext |
target_speed_mps |
f64 |
Accelerate from rest to one speed at full throttle and report what the hardware allowed.
def dynamics_equations
Section titled “def dynamics_equations”def dynamics_equations() -> list[dict[str, str]] !{}Returns list[dict[str, str]]
Effects !{}
vehicle
Section titled “vehicle”Automotive-native major-assembly vehicle, E/E, and manufacturing definition.
def variant_value
Section titled “def variant_value”def variant_value(configuration_variant: str, conventional: str, circuitframe: str)Parameters
| name | type |
|---|---|
configuration_variant |
str |
conventional |
str |
circuitframe |
str |
def vehicle_identity
Section titled “def vehicle_identity”def vehicle_identity() -> VehicleIdentity !{}Returns VehicleIdentity
Effects !{}
def vehicle_parts
Section titled “def vehicle_parts”def vehicle_parts() -> list[PartDefinition] !{}Returns list[PartDefinition]
Effects !{}
def vehicle_nets
Section titled “def vehicle_nets”def vehicle_nets(configuration_variant: str) -> list[NetDefinition] !{}Parameters
| name | type |
|---|---|
configuration_variant |
str |
Returns list[NetDefinition]
Effects !{}
def net_routing
Section titled “def net_routing”def net_routing() -> list[dict[str, any]]Returns list[dict[str, any]]
Per-net conductor routing for both configurations.
Lengths, terminations and splice counts are bounded engineered assumptions derived from the packaging of the reconstructed vehicle; they are not measured production harness data. The CircuitFrame column routes through the structure, so it is shorter and needs fewer separable interfaces, which is the claim this comparison exists to quantify.
def conductor_variant
Section titled “def conductor_variant”def conductor_variant(net: NetDefinition, route: list[f64], density_a_mm2: f64, jacket_factor: f64, conductor: str, route_text: str) !{}Parameters
| name | type |
|---|---|
net |
NetDefinition |
route |
list[f64] |
density_a_mm2 |
f64 |
jacket_factor |
f64 |
conductor |
str |
route_text |
str |
Effects !{}
Size one conductor run from its declared current and report the resulting electrical loads.
def vehicle_net_comparison
Section titled “def vehicle_net_comparison”def vehicle_net_comparison() -> list[dict[str, any]] !{}Returns list[dict[str, any]]
Effects !{}
Conventional harness versus CircuitFrame structural conductors over the same nine nets.
def manufacturing_operations
Section titled “def manufacturing_operations”def manufacturing_operations(configuration_variant: str)Parameters
| name | type |
|---|---|
configuration_variant |
str |
def validation_cases
Section titled “def validation_cases”def validation_cases()def evidence_records
Section titled “def evidence_records”def evidence_records()def source_records
Section titled “def source_records”def source_records()def source_bindings
Section titled “def source_bindings”def source_bindings()def source_gate
Section titled “def source_gate”def source_gate() -> dict[str, any] !{}Returns dict[str, any]
Effects !{}
def completeness_report
Section titled “def completeness_report”def completeness_report(configuration_variant: str) -> CompletenessReport !{}Parameters
| name | type |
|---|---|
configuration_variant |
str |
Returns CompletenessReport
Effects !{}
def vehicle_manifest
Section titled “def vehicle_manifest”def vehicle_manifest(configuration_variant: str) -> dict[str, any] !{}Parameters
| name | type |
|---|---|
configuration_variant |
str |
Returns dict[str, any]
Effects !{}