Skip to content

Commit ef39ed5

Browse files
RFloEngclaude
andcommitted
Fix AC→SVJ compatibility: resolve rim/tire dims from tires.sets
The AC→SVJ converter stores authoritative rim data in tires.sets.<key> rather than in suspension.{corner}.wheel (where rim_diameter is hardcoded to a 0.432 m placeholder). This commit adds tires.sets fallback logic to both the Python analyser and the JS browser so all 1 297-car libraries produce correct rim sizes, loaded radii, and Pacejka curves. Python (svj_analyze.py): - _tire_key() gains optional tires_sets param; when set_ref is present, rim.diameter / rim.width_nominal / overall_diameter÷2 override wheel values - summarize_vehicle() and sanity_check() both fetch tires.sets and pass it in JS browser (svj_library_browser.html): - Per-corner loop builds effW overlay from tires.sets when set_ref present, so tireKey() receives correct dimensions instead of the placeholder - Pacejka fallback: when suspension.tire has no pacejka block, reads tires.sets.<set_ref>.pacejka_mf52 and maps MF6.2 p-coefficients (pCy1/pDy1/pKy1/pEy1) to pseudo B/C/D/E for plotting Result: rim catalogue now spans 5"–24" correctly (was flat 17" for all cars); all 908 unique tire variants carry loaded_radius_mm; Pacejka tab populated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e03830f commit ef39ed5

2 files changed

Lines changed: 87 additions & 11 deletions

File tree

svj_analyze.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,39 @@ def _interp_curve(pts: list, qx: float) -> float | None:
208208
return None
209209

210210

211-
def _tire_key(wheel: dict) -> tuple | None:
212-
"""Identity of a tire footprint: (rim_diam_mm, rim_width_mm, loaded_r_mm)."""
211+
def _tire_key(wheel: dict, tires_sets: dict | None = None) -> tuple | None:
212+
"""Identity of a tire footprint: (rim_diam_mm, rim_width_mm, loaded_r_mm).
213+
214+
When *tires_sets* is supplied and the wheel carries a *set_ref*, values from
215+
``tires.sets.<set_ref>`` take precedence over the wheel block. This is needed
216+
for AC→SVJ files where ``wheel.rim_diameter`` is hardcoded to a placeholder
217+
(0.432 m) and the authoritative rim data lives in ``tires.sets``.
218+
"""
213219
if not isinstance(wheel, dict):
214220
return None
215221
rd = _num(wheel.get("rim_diameter"))
216222
rw = _num(wheel.get("rim_width"))
217223
lr = _num(wheel.get("loaded_radius"))
224+
# Prefer tires.sets data when set_ref is available (AC→SVJ converter pattern)
225+
if tires_sets:
226+
sref = wheel.get("set_ref")
227+
if sref:
228+
ts = tires_sets.get(sref) or {}
229+
rim = ts.get("rim") or {}
230+
dims = ts.get("dimensions") or {}
231+
# rim.diameter is authoritative; wheel.rim_diameter may be a placeholder
232+
rd_ts = _num(rim.get("diameter"))
233+
if rd_ts is not None:
234+
rd = rd_ts
235+
# rim.width_nominal is available (approximated as section width in AC→SVJ)
236+
rw_ts = _num(rim.get("width_nominal"))
237+
if rw_ts is not None:
238+
rw = rw_ts
239+
# loaded_radius must be derived from overall_diameter (not stored directly)
240+
if lr is None:
241+
od = _num(dims.get("overall_diameter"))
242+
if od is not None:
243+
lr = od / 2
218244
if rd is None and rw is None and lr is None:
219245
return None
220246
return (
@@ -253,6 +279,7 @@ def summarize_vehicle(doc: dict, source: str) -> dict[str, Any]:
253279
suspension = doc.get("suspension", {}) or {}
254280
powertrain = doc.get("powertrain", {}) or {}
255281
electric = doc.get("electric", {}) or {}
282+
tires_sets = (doc.get("tires") or {}).get("sets") or {}
256283

257284
# Chassis basics
258285
mass = _num(chassis.get("mass_total"))
@@ -302,7 +329,7 @@ def summarize_vehicle(doc: dict, source: str) -> dict[str, Any]:
302329
damper = corner.get("damper") or {}
303330
brake = corner.get("brake") or {}
304331
disc = brake.get("disc") if isinstance(brake, dict) else {}
305-
key = _tire_key(wheel)
332+
key = _tire_key(wheel, tires_sets)
306333
tires[c] = {
307334
"key": list(key) if key else None,
308335
"label": _tire_label(key) if key else None,
@@ -1015,6 +1042,7 @@ def _add(check: str, sev: str, msg: str) -> None:
10151042
powertrain = doc.get("powertrain", {}) or {}
10161043
suspension = doc.get("suspension", {}) or {}
10171044
electric = doc.get("electric", {}) or {}
1045+
tires_sets = (doc.get("tires") or {}).get("sets") or {}
10181046

10191047
wb = _num(chassis.get("wheelbase"))
10201048
cog = chassis.get("center_of_gravity")
@@ -1038,14 +1066,16 @@ def _add(check: str, sev: str, msg: str) -> None:
10381066
f"CoG height {height:.3f}m outside typical [0.15, 1.80]m range")
10391067

10401068
# 3. Rim Ø < 2 × loaded radius per corner
1069+
# Use tires.sets resolved values so AC→SVJ placeholder rim_diameter is handled.
10411070
for c in CORNERS:
10421071
corner = suspension.get(c) or {}
10431072
wheel = corner.get("wheel") or {}
1044-
rd = _num(wheel.get("rim_diameter"))
1045-
lr = _num(wheel.get("loaded_radius"))
1046-
if rd is not None and lr is not None and rd >= 2 * lr:
1047-
_add(f"tire_{c}_geometry", "error",
1048-
f"{c}: rim_diameter {rd*1000:.0f}mm >= 2×loaded_radius {lr*1000:.0f}mm")
1073+
key = _tire_key(wheel, tires_sets)
1074+
if key and key[0] is not None and key[2] is not None:
1075+
rd_mm, _, lr_mm = key
1076+
if rd_mm >= 2 * lr_mm:
1077+
_add(f"tire_{c}_geometry", "error",
1078+
f"{c}: rim_diameter {rd_mm:.0f}mm >= 2×loaded_radius {lr_mm:.0f}mm")
10491079

10501080
# 4. Damper curves must be strictly monotonically increasing in x
10511081
for c in CORNERS:

svj_library_browser.html

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,8 @@ <h2>Pacejka Magic Formula — tire model curves</h2>
344344
</div>
345345
<div class="car-picker" id="pjCarPicker" style="margin-bottom:10px;"></div>
346346
<div class="empty" id="empty-pacejka" style="display:none;">No Pacejka coefficients found in loaded vehicles.
347-
<br><span style="font-size:11px;color:var(--muted);">SVJ path: <code>suspension.{corner}.tire.pacejka.{lat|lon}.{B,C,D,E}</code></span></div>
347+
<br><span style="font-size:11px;color:var(--muted);">SVJ path: <code>suspension.{corner}.tire.pacejka.{lat|lon}.{B,C,D,E}</code>
348+
or <code>tires.sets.&lt;set_ref&gt;.pacejka_mf52.{lateral|longitudinal}</code> (AC→SVJ)</span></div>
348349
<div id="pjPlot" style="width:100%;height:65vh;"></div>
349350
<div id="pjTable" style="margin-top:14px;overflow-x:auto;font-size:11px;"></div>
350351
</div>
@@ -497,7 +498,25 @@ <h2>Vehicle handling fingerprint — normalised radar chart</h2>
497498
const w = co.wheel || {};
498499
const sp = co.spring || {}, da = co.damper || {}, br = co.brake || {};
499500
const disc = br.disc || {};
500-
const key = tireKey(w);
501+
// Prefer tires.sets data when set_ref is present (AC→SVJ converter pattern:
502+
// wheel.rim_diameter is hardcoded 0.432 placeholder; real data is in tires.sets)
503+
const srefW = w.set_ref || (co.tire || {}).set_ref;
504+
let effW = w;
505+
if (srefW) {
506+
const ts = doc.tires?.sets?.[srefW];
507+
if (ts) {
508+
const rim = ts.rim || {};
509+
const dims = ts.dimensions || {};
510+
const od = dims.overall_diameter;
511+
effW = {
512+
rim_diameter: rim.diameter ?? w.rim_diameter,
513+
rim_width: rim.width_nominal ?? w.rim_width,
514+
loaded_radius: w.loaded_radius ?? (od != null ? od / 2 : null),
515+
mass: w.mass,
516+
};
517+
}
518+
}
519+
const key = tireKey(effW);
501520
tires[c] = {
502521
key, label: tireLabel(key),
503522
rim_diameter_mm: key ? key[0] : null,
@@ -519,7 +538,7 @@ <h2>Vehicle handling fingerprint — normalised radar chart</h2>
519538
};
520539
if (corners[c].estimated) est_corners++;
521540

522-
// Pacejka coefficients — try tire.pacejka or tile.magic_formula
541+
// Pacejka — try inline tire.pacejka / tire.magic_formula first
523542
const tire_block = co.tire || {};
524543
const pj_block = tire_block.pacejka || tire_block.magic_formula || null;
525544
if (pj_block) {
@@ -534,6 +553,33 @@ <h2>Vehicle handling fingerprint — normalised radar chart</h2>
534553
const mz = normCoeffs(pj_block.mz ?? pj_block.aligning ?? null);
535554
if (lat || lon || mz) corners[c].pacejka = { lat, lon, mz };
536555
}
556+
// Fallback: AC→SVJ format stores Pacejka in tires.sets.<set_ref>.pacejka_mf52
557+
// using MF6.2 p-coefficients; map to approximate B/C/D/E for plotting
558+
if (!corners[c].pacejka) {
559+
if (srefW) {
560+
const ts = doc.tires?.sets?.[srefW];
561+
const mf = ts?.pacejka_mf52 || ts?.pacejka;
562+
if (mf) {
563+
// MF6.2 → pseudo B/C/D/E at reference load (single-point approximation)
564+
// C = pCy1/pCx1, D = pDy1/pDx1, B = pKy1/(C×D), E = pEy1/pEx1
565+
const mf6ToBCDE = (obj, lat) => {
566+
if (!obj) return null;
567+
const pC = num(lat ? obj.pCy1 : obj.pCx1);
568+
const pD = num(lat ? obj.pDy1 : obj.pDx1);
569+
const pK = num(lat ? obj.pKy1 : obj.pKx1);
570+
const pE = num(lat ? obj.pEy1 : obj.pEx1);
571+
if (pC === null && pD === null) return null;
572+
const C = pC, D = pD, E = pE;
573+
const B = (pK !== null && C !== null && D !== null && C * D !== 0)
574+
? pK / (C * D) : null;
575+
return (B !== null || C !== null || D !== null) ? {B, C, D, E} : null;
576+
};
577+
const lat2 = mf6ToBCDE(mf.lateral, true);
578+
const lon2 = mf6ToBCDE(mf.longitudinal, false);
579+
if (lat2 || lon2) corners[c].pacejka = { lat: lat2, lon: lon2, mz: null };
580+
}
581+
}
582+
}
537583
}
538584

539585
const _COMP_KEYS = [

0 commit comments

Comments
 (0)