model: TroCandidate statt OpenPoint, tro_overrides nutzt model-Klassen (Schema v3)

- OpenPoint -> model.TroCandidate (type, position, belongsTo, note);
  TroDocument.candidates ersetzt openPoints, Schema sps_skel-tro-v3
- Positionsbehandlung von TroBase und TroCandidate im Mixin _Positioned
- tro_overrides.py: eigene ConnectionSpec/OpenPoint entfallen, stattdessen
  model.Connection / model.TroCandidate; einheitliche Feldnamen
  (tro_id/separator, hosts als Liste)
- Fixtures, Beispiel-JSON und Doku auf v3 nachgezogen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-10-10 11:31:05 +02:00
co-authored by Claude Opus 5.5
parent de8ecb9460
commit 29077caa81
10 changed files with 153 additions and 134 deletions
+2 -3
View File
@@ -152,7 +152,7 @@ extraction, not code to execute or modify.
Since 2026-10-09 the main path is **CSV → `tro_flow.py --json` → TRO-JSON →
`scl_skeleton.py` → SCL**. Layer order (who may import whom): `tro_catalog.py` (TRO *types*)
← `lib/model.py` (TRO *instances* and the TRO-JSON: `TroBase` → `DerivedTro`, `Connection`,
`OpenPoint`, `Analysis`, `TroDocument`) ← derivation (`material_flow.py`, `tro_flow.py`) ←
`TroCandidate`, `Analysis`, `TroDocument`) ← derivation (`material_flow.py`, `tro_flow.py`) ←
outputs (`scl_skeleton.py`, `tro_annotate.py`, reports). `items`/`fbType` are always read
from the catalog via the TRO type, never stored. See `doc/Python_Scripts.md` §6b.
Inside the SCL output, `lib/scl_context.py` (TRO-JSON → render context, all derivations)
@@ -165,7 +165,7 @@ and status: `doc/SCL_Templates.md`. Golden files in `tests/fixtures/scl/` pin th
byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended change.
1. **`lib/tro_flow.py --json`** (implemented) — writes `<csv>_tro.json` (schema
`sps_skel-tro-v2`, `model.TroDocument`): TROs, system boundaries `SYSn`, connections with
`sps_skel-tro-v3`, `model.TroDocument`): TROs, system boundaries `SYSn`, connections with
their kind, findings. See `doc/Python_Scripts.md` §4.
2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON through `TroDocument` (a
wrong/renamed key or `items` not matching the catalog stops the run) and emits an `FB_Main` SCL
@@ -194,7 +194,6 @@ What is **not** built out yet, relative to the original two-tool design:
standalone files) — only `FB_Main` is generated today;
- **type-based timing defaults** from `doc/TRO_Typen.md` and the `OVERRIDE_TIMING_JSON` CAD
attribute (timings are emitted as `TODO(E-Planung): pruefen` placeholders for now);
- `tro_overrides.py` using the shared `model.Connection` / `model.OpenPoint` classes;
- `Component` classes per part type (Separator, Switch, Scanner, VarioDrive, …) on the TRO,
driving the per-part SCL emitters in `scl_skeleton.py`.
+21 -8
View File
@@ -60,7 +60,7 @@ Supporting libraries (no CLI of their own, imported by the tools above):
name, Siemens FB block, sub-components and counts, color group, CAD marker
shape.
- **`lib/model.py`** — the shared pydantic model of TRO *instances* and of the
TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `OpenPoint`, `Analysis`,
TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`,
`TroDocument`). Written by `tro_flow.py --json`, read and validated by
`scl_skeleton.py`. See §6b.
- **`lib/dxf_registration.py`** — derives, verifies and persists the rigid
@@ -294,7 +294,7 @@ colors all come from `lib/tro_catalog.py` — nothing is hard-coded here.
| `--doc` | — | off | Also write a Markdown report (`<name>_tro_doc.md`) with the TRO list, connections, findings and the full type catalog. |
| `--connect` | — | off | Also write the TRO topology as `<name>_tro_connect.ini` (nodes, connections, system boundaries, predecessors/successors per TRO). Pure output, never read back. |
| `--true-scale` | — | off | Only with `--use-cords`: draw to scale. By default empty stretches longer than 1500 mm between nodes are shortened (order and neighbourhood are kept). |
| `--json` | — | off | Also write the TRO-JSON `<name>_tro.json` (schema `sps_skel-tro-v2`, model `TroDocument` in `lib/model.py`): TROs, system boundaries (`SYSn`), connections with their kind, findings. Input for `scl_skeleton.py`. |
| `--json` | — | off | Also write the TRO-JSON `<name>_tro.json` (schema `sps_skel-tro-v3`, model `TroDocument` in `lib/model.py`): TROs, system boundaries (`SYSn`), connections with their kind, findings. Input for `scl_skeleton.py`. |
TROs, types and connections always come from the layout alone;
`cfg/tro_overrides.ini` is not read. Errors in the derivation are fixed in
@@ -533,7 +533,7 @@ TRO in a plant, and the whole TRO-JSON, look like. It imports only the catalog.
```
tro_catalog.py type level TroDefinition (items, FB block, color, symbol)
▲ get_tro()
model.py instance level TroBase ─► DerivedTro, Connection, OpenPoint,
model.py instance level TroBase ─► DerivedTro, Connection, TroCandidate,
Analysis, TroDocument
▲
tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws)
@@ -544,14 +544,21 @@ tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws)
| `TroBase` | Core of every TRO: `id`, `type`, `confidence`, `separators`, `predecessors`, `successors`, `position` (`{x,y,z}` in JSON). `definition()`, `fb_block` (JSON `fbType`) and `items` are **read from the catalog** on every access, never stored — so they follow a type change and cannot go stale. |
| `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `reason`, `colocated`, `positionFrom`, `positionPoints`. Also used for the system boundaries `SYSn`. |
| `Connection` | Directed edge `from` → `to` with its `kind` (transfer / umlauf / weiche / unbestimmt / normal / bahn). |
| `OpenPoint` | A reported spot that is not a TRO yet (empty in the JSON for now; meant to replace the class of the same name in `tro_overrides.py`). |
| `TroCandidate` | A spot reported in a review where a TRO is **expected** but not derived (formerly `OpenPoint`): `name`, `type` (the expected catalog type), `position`, `belongsTo` (the TRO that holds the part today), `note`. Not a TRO — no separators, no connections, no SCL. Built by `tro_overrides.py` from `[<csv>.openpoint.<Name>]`; empty in the TRO-JSON for now. |
| `Analysis` | Working result of `tro_flow.analyse_tros` (moved here from `tro_flow.py`). |
| `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v2`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `openPoints`, `findings`. `from_analysis()`, `to_json()`, `from_json()`. |
| `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v3`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `candidates`, `findings`. `from_analysis()`, `to_json()`, `from_json()`. |
`TroBase` and `TroCandidate` share the `position` handling (JSON `{x,y,z}`,
`x`/`y`/`z`, `describe_position()`) through the private mixin `_Positioned`.
The same data has the same field name in every class, including
`tro_overrides.py`: `tro_id`, `type_name`, `separators`, `scanners`, `hosts`,
`label`, `reason`, `confidence`, `position`.
All models validate on creation and assignment and reject unknown keys
(`extra="forbid"`). JSON keys keep the camelCase names of the TRO-JSON (`id`,
`type`, `fbType`, `openPoints` …); Python code uses the field names
(`tro_id`, `type_name`, `fb_block`, `open_points`) — both are accepted on input.
`type`, `fbType`, `belongsTo` …); Python code uses the field names
(`tro_id`, `type_name`, `fb_block`, `belongs_to`) — both are accepted on input.
Schema v3 differs from v2 only in `candidates` replacing `openPoints`.
Tests: `tests/test_model.py` (round trip, catalog-backed fields, rejection of
wrong keys/items, SCL rendering from a document) — run with
@@ -634,7 +641,13 @@ Three kinds of entry, deliberately different in weight:
flow node is primary, the rest stay attached so the linking still sees both
sides of a transition. Like splits, this renumbers.
- **`openpoint`** — a spot reported in a review that is **not** turned into a
TRO. The structure stays untouched.
TRO. The structure stays untouched. Read as `model.TroCandidate` (`expect` →
`type_name`, `x`/`y`/`z` → `position`, missing `z` = 0).
In Python the entries are `TroOverride` (`tro_id` *or* `separator`, from
`TRO<n>` / `SEP<n>`), `SplitSpec`/`MergeSpec` (`hosts` is a list; the INI key
stays `host`, comma-separated), `model.Connection` for `[<csv>.connections]`,
and `model.TroCandidate` — field names as in `model.py`.
Consumer: `material_flow.py --connect` (writes the `[<plant>.manual]` section
into `connect.ini`). Keep in mind: **TRO IDs are running numbers of one run and
+2 -2
View File
@@ -12,7 +12,7 @@ It is written so that it fits the existing main path unchanged:
```
export.csv ─► tro_flow --json ─► <name>_tro.json ─► scl_skeleton ─► <name>_FB_Main.scl
(dxfmakros (derive) (TroDocument, (render) (skeleton with
EXPORTCSV) sps_skel-tro-v2) TODO(E-Planung))
EXPORTCSV) sps_skel-tro-v3) TODO(E-Planung))
```
Nothing upstream of `scl_skeleton` changes. The TRO-JSON stays the only input, and the
@@ -451,7 +451,7 @@ bin\scl_skeleton.bat --json <name>_tro.json [--templates [DIR]] [--strict] [--na
| File | Where | Who writes it |
|---|---|---|
| `<name>.csv` | `%SKEL_DATA%` | dxfmakros `EXPORTCSV` |
| `<name>_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v2`) |
| `<name>_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v3`) |
| `scl_naming.ini`, `<name>_scl_naming.ini` | `%SKEL_CFG%` | you, once per project (optional) |
| `lib/scl_templates/*.scl.j2` | repo | maintained with the PLC engineers |
+1 -1
View File
@@ -477,5 +477,5 @@
"to": "TRO05"
}
],
"openPoints": []
"candidates": []
}
+5 -5
View File
@@ -2349,7 +2349,7 @@ def render_connect_ini(graph: Graph, section: str = "Anlage", overrides=None) ->
add("")
if overrides is not None and (overrides.tros or overrides.splits
or overrides.open_points):
or overrides.candidates):
add(f"[{section}.manual]")
add("; Vor Ort bestaetigt, im CSV-Export nicht enthalten.")
add(f"; Quelle: {overrides.source}")
@@ -2358,16 +2358,16 @@ def render_connect_ini(graph: Graph, section: str = "Anlage", overrides=None) ->
for section_key, override in sorted(overrides.tros.items()):
if not override.type_name:
continue
wie = f"Separator {override.key}" if override.by_separator else override.key
wie = f"Separator {override.separator}" if override.separator else override.tro_id
add(f"{section_key} = {override.type_name} | {wie} | {override.reason}")
for spec in overrides.splits:
add(f"{spec.name} = NEU {spec.type_name} "
f"| Separator {', '.join(spec.separators)}"
+ (f" + Scanner {', '.join(spec.scanners)}" if spec.scanners else "")
+ (f" | Bahn {spec.host}" if spec.host else "")
+ (f" | Bahn {', '.join(spec.hosts)}" if spec.hosts else "")
+ f" | {spec.reason}")
for point in overrides.open_points:
add(f"{point.name} = OFFEN {point.expect or '?'} "
for point in overrides.candidates:
add(f"{point.name} = OFFEN {point.type_name or '?'} "
f"| x={point.x:.2f} y={point.y:.2f} "
f"| heute Teil von {point.belongs_to or '-'} "
f"| {point.note}")
+64 -49
View File
@@ -19,20 +19,23 @@ Klassen:
DerivedTro TroBase + was tro_flow bei der Ableitung zusaetzlich weiss
(Hosts, Scanner, Begruendung, Positionsquelle ...)
Connection eine gerichtete Verbindung zwischen zwei TROs/Stationen
OpenPoint eine gemeldete Stelle, die noch kein TRO ist
TroCandidate eine im Review gemeldete Stelle, an der ein TRO erwartet
wird, die aber (noch) keiner ist
Analysis Arbeitsergebnis von tro_flow.analyse_tros
TroDocument die TRO-JSON (<csv>_tro.json): von tro_flow --json
geschrieben, von scl_skeleton gelesen und geprueft
In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`,
`fbType`, `openPoints`, ...); im Python-Code heissen die Felder weiter
`tro_id`/`type_name` (Aliase, `populate_by_name`).
`fbType`, `candidates`, ...); im Python-Code heissen die Felder weiter
`tro_id`/`type_name` (Aliase, `populate_by_name`). Gleiche Daten heissen in
allen Klassen gleich (auch in tro_overrides.py): `type_name`, `separators`,
`hosts`, `position`, `reason`, `confidence`.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import TYPE_CHECKING, Any
from pydantic import (
BaseModel,
@@ -51,14 +54,15 @@ __all__ = [
"TroBase",
"DerivedTro",
"Connection",
"OpenPoint",
"TroCandidate",
"Analysis",
"TroDocument",
]
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell).
SCHEMA = "sps_skel-tro-v2"
# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell), v3 wie v2 mit
# `candidates` statt `openPoints`.
SCHEMA = "sps_skel-tro-v3"
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
# Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt
@@ -71,7 +75,48 @@ _CONFIG = ConfigDict(
)
class TroBase(BaseModel):
class _Positioned(BaseModel):
"""
Anlagenkoordinate (mm) als Feld ``position``, in der JSON {"x", "y", "z"}.
Das Feld selbst deklariert jede Unterklasse, damit es dort in der
JSON-Reihenfolge an seiner Stelle steht.
"""
if TYPE_CHECKING:
position: Any
@field_validator("position", mode="before", check_fields=False)
@classmethod
def _position_from_json(cls, value: Any) -> Any:
if isinstance(value, dict):
return (value["x"], value["y"], value.get("z", 0.0))
return value
@field_serializer("position", when_used="json-unless-none", check_fields=False)
def _position_to_json(self, value: tuple[float, float, float]) -> dict[str, float]:
return {"x": round(value[0], 2), "y": round(value[1], 2), "z": round(value[2], 2)}
@property
def x(self) -> float | None:
return self.position[0] if self.position else None
@property
def y(self) -> float | None:
return self.position[1] if self.position else None
@property
def z(self) -> float | None:
return self.position[2] if self.position else None
def describe_position(self) -> str:
"""Koordinate als Text, z. B. '1030, 4700'."""
if not self.position:
return "-"
return f"{self.position[0]:.0f}, {self.position[1]:.0f}"
class TroBase(_Positioned):
"""
Kern eines TRO (Instanz in einer Anlage), unabhaengig von seiner Quelle.
@@ -145,37 +190,6 @@ class TroBase(BaseModel):
)
return data
# -- Position ------------------------------------------------------------
@field_validator("position", mode="before")
@classmethod
def _position_from_json(cls, value: Any) -> Any:
if isinstance(value, dict):
return (value["x"], value["y"], value.get("z", 0.0))
return value
@field_serializer("position", when_used="json-unless-none")
def _position_to_json(self, value: tuple[float, float, float]) -> dict[str, float]:
return {"x": round(value[0], 2), "y": round(value[1], 2), "z": round(value[2], 2)}
@property
def x(self) -> float | None:
return self.position[0] if self.position else None
@property
def y(self) -> float | None:
return self.position[1] if self.position else None
@property
def z(self) -> float | None:
return self.position[2] if self.position else None
def describe_position(self) -> str:
"""Koordinate als Text, z. B. '1030, 4700'."""
if not self.position:
return "-"
return f"{self.position[0]:.0f}, {self.position[1]:.0f}"
class DerivedTro(TroBase):
"""
@@ -210,22 +224,23 @@ class Connection(BaseModel):
kind: str = ""
class OpenPoint(BaseModel):
"""Eine im Review gemeldete Stelle, die noch kein eigener TRO ist."""
class TroCandidate(_Positioned):
"""
Eine im Review gemeldete Stelle, an der ein TRO erwartet wird.
Kein TRO: keine Separatoren, keine Verbindungen, keine SCL. Das Bauteil
steckt heute in einem anderen TRO (``belongs_to``). ``type_name`` ist der
hier erwartete Katalogtyp.
"""
model_config = _CONFIG
name: str
x: float
y: float
z: float | None = None
expect: str = ""
type_name: str = Field(default="", alias="type")
position: tuple[float, float, float]
belongs_to: str = Field(default="", alias="belongsTo")
note: str = ""
def describe_position(self) -> str:
return f"{self.x:.0f}, {self.y:.0f}"
class Analysis(BaseModel):
"""Ergebnis von tro_flow.analyse_tros: TROs, Kanten, Befunde."""
@@ -272,7 +287,7 @@ class TroDocument(BaseModel):
# Systemgrenzen (SYSn): Materialein-/ausgang, kein TRO - keine SCL
boundaries: list[DerivedTro] = Field(default_factory=list)
connections: list[Connection] = Field(default_factory=list)
open_points: list[OpenPoint] = Field(default_factory=list, alias="openPoints")
candidates: list[TroCandidate] = Field(default_factory=list)
findings: list[str] = Field(default_factory=list)
@classmethod
+41 -60
View File
@@ -26,15 +26,19 @@ mehrere Anlagen in einer Datei stehen koennen):
reason = ... Begruendung (kommt in die Doku)
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO (model.TroCandidate)
x = 529.95 Anlagenkoordinate in mm
y = -3895.27
z = 1468
expect = 1Sep1Swi hier erwarteter TRO-Typ
expect = 1Sep1Swi hier erwarteter TRO-Typ (-> type_name)
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
note = ... Erlaeuterung
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
`model.Connection` bzw. `model.TroCandidate`.
"""
from __future__ import annotations
@@ -44,6 +48,7 @@ import os
from dataclasses import dataclass, field
from pathlib import Path
from model import Connection, TroCandidate
from tro_catalog import get_tro
OVERRIDE_FILE = "tro_overrides.ini"
@@ -63,16 +68,17 @@ class TroOverride:
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
"""
key: str
by_separator: bool = False
# genau eins von beiden ist gesetzt
tro_id: str = ""
separator: str = ""
type_name: str = ""
reason: str = ""
confidence: str = ""
def matches(self, tro) -> bool:
if self.by_separator:
return self.key in tro.separators
return self.key == tro.tro_id
if self.separator:
return self.separator in tro.separators
return self.tro_id == tro.tro_id
@dataclass
@@ -90,21 +96,12 @@ class SplitSpec:
type_name: str
separators: list[str] = field(default_factory=list)
scanners: list[str] = field(default_factory=list)
host: str = ""
hosts: list[str] = field(default_factory=list)
label: str = ""
reason: str = ""
confidence: str = DEFAULT_CONFIDENCE
@dataclass
class ConnectionSpec:
"""Eine vor Ort bestaetigte Verbindung zwischen zwei TROs."""
src: str
dst: str
kind: str = ""
@dataclass
class MergeSpec:
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
@@ -119,28 +116,12 @@ class MergeSpec:
name: str
type_name: str
separators: list[str] = field(default_factory=list)
host: str = ""
hosts: list[str] = field(default_factory=list)
label: str = ""
reason: str = ""
confidence: str = DEFAULT_CONFIDENCE
@dataclass
class OpenPoint:
"""Eine im Review gemeldete Stelle, die noch kein eigener TRO ist."""
name: str
x: float
y: float
z: float | None = None
expect: str = ""
belongs_to: str = ""
note: str = ""
def describe_position(self) -> str:
return f"{self.x:.0f}, {self.y:.0f}"
@dataclass
class Overrides:
"""Alle Korrekturen zu einer CSV-Datei."""
@@ -150,15 +131,15 @@ class Overrides:
splits: list[SplitSpec] = field(default_factory=list)
merges: list[MergeSpec] = field(default_factory=list)
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
connections: list[ConnectionSpec] = field(default_factory=list)
open_points: list[OpenPoint] = field(default_factory=list)
connections: list[Connection] = field(default_factory=list)
candidates: list[TroCandidate] = field(default_factory=list)
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
applied: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
def __bool__(self) -> bool:
return bool(self.tros or self.splits or self.merges or self.connections
or self.open_points)
or self.candidates)
def override_path(cfg_dir: Path | None = None) -> Path:
@@ -179,6 +160,10 @@ def _as_float(raw: str, where: str, warnings: list[str]) -> float | None:
return None
def _as_list(raw: str) -> list[str]:
return [s.strip() for s in raw.split(",") if s.strip()]
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
path = override_path(cfg_dir)
@@ -205,7 +190,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
)
continue
result.connections.append(
ConnectionSpec(src=src, dst=dst, kind=rest.strip())
Connection(src=src, dst=dst, kind=rest.strip())
)
continue
@@ -230,8 +215,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
key = name.upper()
by_sep = key.startswith("SEP")
result.tros[key] = TroOverride(
key=key[3:] if by_sep else key,
by_separator=by_sep,
tro_id="" if by_sep else key,
separator=key[3:] if by_sep else "",
type_name=type_name,
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
@@ -244,7 +229,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
f"- neuer TRO nicht angelegt"
)
continue
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
seps = _as_list(values.get("separators", ""))
if not seps:
result.warnings.append(
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
@@ -255,9 +240,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
name=name,
type_name=type_name,
separators=seps,
scanners=[s.strip() for s in values.get("scanners", "").split(",")
if s.strip()],
host=values.get("host", "").strip(),
scanners=_as_list(values.get("scanners", "")),
hosts=_as_list(values.get("hosts", values.get("host", ""))),
label=values.get("label", "").strip(),
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
@@ -271,7 +255,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
f"- Zusammenfassung nicht durchgefuehrt"
)
continue
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
seps = _as_list(values.get("separators", ""))
if len(seps) < 2:
result.warnings.append(
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
@@ -282,7 +266,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
name=name,
type_name=type_name,
separators=seps,
host=values.get("host", "").strip(),
hosts=_as_list(values.get("hosts", values.get("host", ""))),
label=values.get("label", "").strip(),
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
@@ -294,30 +278,27 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
if x is None or y is None:
continue
z_raw = values.get("z", "").strip()
expect = values.get("expect", "").strip()
if expect and get_tro(expect) is None:
z = _as_float(z_raw, f"[{section}] z", result.warnings) if z_raw else None
type_name = values.get("expect", "").strip()
if type_name and get_tro(type_name) is None:
result.warnings.append(
f"[{section}]: erwarteter Typ '{expect}' steht nicht im "
f"[{section}]: erwarteter Typ '{type_name}' steht nicht im "
f"TRO-Katalog"
)
result.open_points.append(
OpenPoint(
result.candidates.append(
TroCandidate(
name=name,
x=x,
y=y,
z=_as_float(z_raw, f"[{section}] z", result.warnings)
if z_raw
else None,
expect=expect,
type_name=type_name,
position=(x, y, z if z is not None else 0.0),
belongs_to=values.get("belongs_to", "").strip(),
note=values.get("note", "").strip(),
)
)
else:
result.warnings.append(
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro' oder "
f"'openpoint'"
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
f"'merge' oder 'openpoint'"
)
result.open_points.sort(key=lambda p: p.name)
result.candidates.sort(key=lambda p: p.name)
return result
+2 -2
View File
@@ -1,5 +1,5 @@
{
"schema": "sps_skel-tro-v2",
"schema": "sps_skel-tro-v3",
"source": "HundM05.csv",
"created": "2026-10-09 11:30",
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
@@ -917,7 +917,7 @@
"kind": "bahn"
}
],
"openPoints": [],
"candidates": [],
"findings": [
"Scanner 0019 'Scanner :1': montiert an 0006-L, liest aber an Separator 0018 auf 0005",
"TRO16 (VF_5): Scanner 0019 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
+2 -2
View File
@@ -1,5 +1,5 @@
{
"schema": "sps_skel-tro-v2",
"schema": "sps_skel-tro-v3",
"source": "mubea.csv",
"created": "2026-10-09 11:38",
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
@@ -888,7 +888,7 @@
"kind": "normal"
}
],
"openPoints": [],
"candidates": [],
"findings": [
"Kreisel2: Ausschleuselement 0057 und ILS Weiche 0125 benachbart - als 2Sep2Swi zusammengefasst (FB_ILS_MTRO_2Sep2Swi fehlt noch in der ILSLib)",
"Scanner 0008 'Scanner :5': montiert an 0010, liest aber an Separator 0042 auf 0010-R",
+13 -2
View File
@@ -6,7 +6,7 @@ import json
import pytest
from pydantic import ValidationError
from model import SCHEMA, Analysis, Connection, DerivedTro, TroDocument
from model import SCHEMA, Analysis, Connection, DerivedTro, TroCandidate, TroDocument
from scl_skeleton import render
@@ -64,7 +64,18 @@ def test_json_uses_tro_json_keys(document):
assert first["items"] == {"Separator": 1, "Switch": 1}
assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0}
assert data["connections"][1] == {"from": "TRO01", "to": "TRO02", "kind": "weiche"}
assert "openPoints" in data
assert data["candidates"] == []
def test_candidate_round_trip():
candidate = TroCandidate(name="Qualitaetskontrolle", type_name="1Sep1Swi",
position=(-1026.0, 5250.05, 1478.5), belongs_to="TRO09")
data = json.loads(candidate.model_dump_json(by_alias=True))
assert data["type"] == "1Sep1Swi"
assert data["position"] == {"x": -1026.0, "y": 5250.05, "z": 1478.5}
assert data["belongsTo"] == "TRO09"
assert TroCandidate.model_validate(data) == candidate
assert candidate.describe_position() == "-1026, 5250"
def test_items_and_fb_block_follow_the_type():