Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,14 @@ path = plant.finalize_trace()
기록한 경로를 돌려준다(기록을 켠 적이 없으면 `None`).
- 컨테이너는 zip: `manifest.json` + `channels/*.f64` + `overlays/*.json`.
이 파일 하나면 렌더가 되므로 재시뮬레이션도, 결과 파일 재파싱도 필요 없다.
- manifest 는 `schema_version` `"0.2"` 와 필수 필드 `role` 을 선언한다. `role` 은
- manifest 는 `schema_version`(writer 는 `"0.4"` 를 쓴다. 버전별 추가 필드는 아래
절 참조)과 필수 필드 `role` 을 선언한다. `role` 은
검증 대상인 `"plant"` 또는 최적화·MPC 내부 예측 모델로 쓰인 `"predictor"` 다.
`VDSimPlant` 은 그 자체가 플랜트이므로 `enable_trace` 의 기본값은 `plant` 이고,
`vdsim_trace.TraceWriter` 를 직접 만드는 생산자는 `role=` 을 반드시 넘겨야 한다
— 기본값이 없으므로 예측기 run 이 빠뜨림만으로 플랜트 근거가 되는 일이 없다.
기존 `0.1` trace 도 그대로 읽힌다. `role` 이 없으면 경고 1회와 함께 `plant` 로
간주하고, `0.2` 에서 누락되면 에러다. 렌더러는 HUD 에 문자열로만 표시하고
간주하고, `0.2` 이상에서 누락되면 에러다. 렌더러는 HUD 에 문자열로만 표시하고
이 값으로 화면 구성을 바꾸지 않는다.

### 렌더 프리셋
Expand Down Expand Up @@ -458,6 +459,21 @@ plant.finalize_trace()
`0.1`·`0.2` trace는 계속 읽힌다. `0.3` 파일에서 필수 필드가 빠지면 경고가 아니라
에러다.

### trace 스키마 `0.4` — 하드포인트가 붙었는가

`0.4` 는 manifest 필수 필드 `kinematics_attached`(bool) 1개를 더한다. 값은 그 run 에서
서스펜션 하드포인트 attach 가 실제로 반환한 결과이며, 어떤 YAML 이 있는지로 추정하지
않는다. L3 와 L4 의 물리는 하드포인트가 붙었을 때만 달라지므로, L3/L4 trace 에
서스펜션 기구학이 들어 있는지는 이 필드로 판별한다.

- `kinematics_attached` 가 없는 `0.4` trace 는 에러다.
- `0.3` 이하 trace 는 값을 unknown(`None`)으로 읽고 경고 1회를 낸다. `false` 로
읽지 않는다.
- 하드포인트 없는 `level="L4"` 는 세션을 만들 때 `ValueError` 로 거부되므로, L3
물리를 담은 L4 trace 는 생길 수 없다. `VDSimPlant` 는 하드포인트 입력이 없어서
같은 이유로 `level="L4"` 를 거부한다. `Experiment` 설정에서는
`kinematics: {front: mp_front_sedan, rear: ta_rear_sedan}` 로 하드포인트를 지정한다.

## campaign — 선언 1개로 여러 run 돌리기

**run** 은 시뮬레이션 1회이자 `.vdtrace` 1개, **campaign** 은 YAML 선언 1개로
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,15 @@ path = plant.finalize_trace()
it returns the written path (`None` when recording was never enabled).
- The container is a zip: `manifest.json` + `channels/*.f64` + `overlays/*.json`.
That one file is enough to render — no re-simulation, no results file.
- The manifest declares `schema_version` `"0.2"` and a required `role`, either
- The manifest declares `schema_version` (the writer emits `"0.4"`; the fields
each version added are listed below) and a required `role`, either
`"plant"` (the simulator under verification) or `"predictor"` (the same code
driven as an optimiser's internal model). `enable_trace` defaults to `plant`
because `VDSimPlant` *is* the plant; a producer that builds a
`vdsim_trace.TraceWriter` directly must pass `role=` — there is no default,
so a predictor run cannot be recorded as plant evidence by omission.
Reading a legacy `0.1` trace still works: a missing `role` resolves to
`plant` with one warning. A `0.2` trace without one is an error.
`plant` with one warning. A `0.2` or later trace without one is an error.
The renderer prints the role in the HUD and never branches on it.

### Render presets
Expand Down Expand Up @@ -498,6 +499,23 @@ plant.finalize_trace()
`0.1` and `0.2` traces stay readable. A `0.3` file that omits any of the
required fields is an error, not a warning.

### Trace schema `0.4` — whether hardpoints were attached

`0.4` adds one required manifest field, `kinematics_attached` (bool). It is the
value the suspension-hardpoint attach actually returned for that run, not a
guess from which YAML files exist. The physics of L3 and L4 differ only when
hardpoints are attached, so this field is what tells a reader whether an L3/L4
trace carries suspension kinematics.

- A `0.4` trace without `kinematics_attached` is an error.
- A `0.3` or older trace reads with the value unknown (`None`) and one warning.
It is never read as `false`.
- `level="L4"` with no hardpoints is refused when the session is built
(`ValueError`), so no L4 trace can carry L3 physics. `VDSimPlant` has no
hardpoint input and refuses `level="L4"` for the same reason. An
`Experiment` config gets hardpoints with
`kinematics: {front: mp_front_sedan, rear: ta_rear_sedan}`.

## Campaigns — many runs from one declaration

A **run** is one simulation and one `.vdtrace`. A **campaign** is a set of runs
Expand Down
9 changes: 4 additions & 5 deletions configs/campaigns/mu_sweep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ sweep:
grid:
mu: [0.9, 0.7, 0.5]

# Note: `level` is not a sweepable axis. L3 and L4 differ only once suspension
# hardpoints are attached and the scenario path attaches none, so the runner
# refuses the axis rather than archive traces whose manifest model_level is
# false. Declare one level per campaign; Q20 lifts the refusal
# (18_dev_briefing_0903 §30.4, §31 C-1).
# Note: `level` is a sweepable axis since Q20. An `L4` run needs suspension
# hardpoints; without them it is refused (index status `error`) rather than
# run as L3 under an L4 label, and every trace states `kinematics_attached`
# (18_dev_briefing_0903 §51).
6 changes: 3 additions & 3 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ the last section.
| ISO 7401 step-steer / 4138 understeer / 3888-2 DLC — run + measured | — |
| L1↔L2↔L3 cross-model consistency where physics overlaps | Dependent axles (twist-beam / solid beam) — configs are stubs |
| FMI round-trip Δ=0 (machine precision); ISO 8608 PSD RMS per class | L3 unsprung lateral-transfer term (small) |
| Full suite: **505/505 ctest green (as of 2026-09-22)** (`validation` preset — see the currency block below) | — |
| Full suite: **506/506 ctest green (as of 2026-09-22)** (`validation` preset — see the currency block below) | — |
| Drivetrain engine inertia (open-diff carrier coupling) | — |
| **ISO step-steer signature gated in CI** (`ctest -R IsoBaseline`, sedan L2 LuGre) | DLC moose gate is a preset property, not a defect (see note) |

Expand Down Expand Up @@ -48,11 +48,11 @@ version and the new numbers have to land in the same commit.

<!-- VALIDATION-CURRENCY BEGIN -->
```text
tests: 505/505
tests: 506/506
config: cmake --preset validation && cmake --build --preset validation && ctest --preset validation
presets: CMakePresets.json@fb1f0c198a0201fcce2543a9f5b2242a255fb97a
toolchain: cmake 3.31.10
commit: 7b7947f
commit: 6e2592f
date: 2026-09-22
excluded: gui_v3_e2e (GUI v3 test group formally deferred -- VDSIM_BUILD_GUI_V3_TESTS=OFF)
excluded: gui_v3_api_smoke (GUI v3 test group formally deferred -- VDSIM_BUILD_GUI_V3_TESTS=OFF)
Expand Down
27 changes: 15 additions & 12 deletions docs/design/BATCH_RUNNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,23 +156,26 @@ Q0 is the case in point.
- **Overlay rendering and aggregate plots.** Those stay where they are; a
consumer script that reads the index is the place for them.

## 7. A `level` axis is refused
## 7. A `level` axis and the bare-L4 refusal

`L4` and `L3` are the same physics unless suspension hardpoints are attached,
and the scenario path attaches none. A level sweep would therefore produce
identical runs whose manifests disagree about which model produced them, and
the false `model_level` outlives any warning printed at run time. So the runner
refuses the axis outright, on every declaration form:
`L4` and `L3` are the same physics unless suspension hardpoints are attached.
Until Q20 a level sweep therefore produced identical runs whose manifests
disagreed about which model produced them, and the runner refused the axis.

Q20 moved the refusal to where the false label is born: building an `L4`
session with no hardpoints attached raises, on every path (campaign, `Sim`,
`Experiment`), whether or not a trace is recorded. In a campaign that run
becomes an `error` row with no trace:

```
CampaignError: 'level' is not a campaign axis until Q20: L4 is identical to L3
unless suspension hardpoints are attached, so a level sweep records traces
whose manifest model_level is false. Declare one level per campaign.
ValueError: level='L4' with no suspension hardpoints attached is L3 under another
label (the two are bit-identical until an attach); pass kin=...
(kinematics={'front': <stem>, 'rear': <stem>}) or use level='L3'
```

Declare one level per campaign instead. The refusal is lifted by Q20, which
attaches the hardpoints and adds a `kinematics_attached` field to the manifest
so a reader can tell the two apart. See 18_dev_briefing_0903 §30.4, §31 C-1.
A `level` axis is accepted again, and every trace written from schema 0.4 on
states `kinematics_attached`, so a reader can tell the two apart. See
18_dev_briefing_0903 §30.4, §31 C-1, §51.

## 8. CLI

Expand Down
16 changes: 16 additions & 0 deletions python/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,22 @@ PYBIND11_MODULE(vdsim, m) {
auto k = vdsim::create_lookup_kinematics(csv_path);
return vdsim::attach_rear_kinematics(dyn, std::move(k));
});
// Native hardpoint YAML (double_wishbone / macpherson / trailing_arm /
// 5link) instead of a lookup CSV. Same dispatch the co-sim server uses in
// attach_susp_parts(), so the Python lab path and the realtime path resolve
// the same file to the same mechanism.
m.def("attach_front_kinematics_from_yaml",
[](vdsim::IVehicleDynamics& dyn, const std::string& yaml_path) {
return vdsim::attach_front_kinematics(
dyn, vdsim::create_native_kinematics_from_yaml(yaml_path));
},
py::arg("dyn"), py::arg("yaml_path"));
m.def("attach_rear_kinematics_from_yaml",
[](vdsim::IVehicleDynamics& dyn, const std::string& yaml_path) {
return vdsim::attach_rear_kinematics(
dyn, vdsim::create_native_kinematics_from_yaml(yaml_path));
},
py::arg("dyn"), py::arg("yaml_path"));

py::class_<vdsim::mb::KcSweepSample>(m, "KcSweepSample")
.def_readonly("abscissa", &vdsim::mb::KcSweepSample::abscissa)
Expand Down
27 changes: 10 additions & 17 deletions python/vdsim_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@
The index is a lookup table, not a result database: it carries the axis values
and the run's disposition, never metrics (EX3).

A ``level`` axis is refused, not merely discouraged: ``L4`` is identical to
``L3`` unless suspension hardpoints are attached, the scenario path attaches
none, and the manifest would still claim ``model_level: L4``
(18_dev_briefing_0903 30.4, 31 C-1). Declare one level per campaign. Q20 lifts
the refusal once the manifest carries ``kinematics_attached``.
A ``level`` axis is allowed since Q20 (18_dev_briefing_0903 §51). It was
refused while an ``L4`` run with no suspension hardpoints still produced a
trace claiming ``model_level: L4``; now the session seam refuses such a run
(it lands in the index as ``error``) and every trace states
``kinematics_attached``, so a level sweep can no longer archive a false label.
"""
import argparse
import copy
Expand Down Expand Up @@ -76,18 +76,11 @@
DEFAULT_ROOT_SEED = 1000

#: Axes a campaign may not sweep, with the message the refusal carries.
#: ``level`` is here because sweeping it would mass-produce traces whose
#: ``model_level`` names a model the run did not use: the L4 class differs from
#: L3 only in ``level()`` until suspension hardpoints are attached, and nothing
#: on the scenario path attaches them. The alternative -- letting the sweep run
#: and warning -- was rejected because the false provenance survives in the
#: archived trace long after the warning has scrolled away.
FORBIDDEN_AXES = {
"level": ("'level' is not a campaign axis until Q20: L4 is identical to L3 "
"unless suspension hardpoints are attached, so a level sweep "
"records traces whose manifest model_level is false. Declare one "
"level per campaign."),
}
#: Empty since Q20 lifted ``level`` (C-1 condition v): the refusal existed
#: because a bare L4 run recorded a false ``model_level``, and that run is now
#: rejected where the session is built. The mechanism stays so a future axis
#: with the same property is refused in the one place every form goes through.
FORBIDDEN_AXES = {}

#: Trace file name inside a run directory. Fixed, so a consumer can find the
#: artefact from the run id alone.
Expand Down
Loading
Loading