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:
co-authored by
Claude Opus 5.5
parent
de8ecb9460
commit
29077caa81
@@ -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 →
|
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*)
|
`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`,
|
← `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
|
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.
|
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)
|
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.
|
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
|
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.
|
their kind, findings. See `doc/Python_Scripts.md` §4.
|
||||||
2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON through `TroDocument` (a
|
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
|
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;
|
standalone files) — only `FB_Main` is generated today;
|
||||||
- **type-based timing defaults** from `doc/TRO_Typen.md` and the `OVERRIDE_TIMING_JSON` CAD
|
- **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);
|
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,
|
- `Component` classes per part type (Separator, Switch, Scanner, VarioDrive, …) on the TRO,
|
||||||
driving the per-part SCL emitters in `scl_skeleton.py`.
|
driving the per-part SCL emitters in `scl_skeleton.py`.
|
||||||
|
|
||||||
|
|||||||
+21
-8
@@ -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
|
name, Siemens FB block, sub-components and counts, color group, CAD marker
|
||||||
shape.
|
shape.
|
||||||
- **`lib/model.py`** — the shared pydantic model of TRO *instances* and of the
|
- **`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
|
`TroDocument`). Written by `tro_flow.py --json`, read and validated by
|
||||||
`scl_skeleton.py`. See §6b.
|
`scl_skeleton.py`. See §6b.
|
||||||
- **`lib/dxf_registration.py`** — derives, verifies and persists the rigid
|
- **`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. |
|
| `--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. |
|
| `--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). |
|
| `--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;
|
TROs, types and connections always come from the layout alone;
|
||||||
`cfg/tro_overrides.ini` is not read. Errors in the derivation are fixed in
|
`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)
|
tro_catalog.py type level TroDefinition (items, FB block, color, symbol)
|
||||||
▲ get_tro()
|
▲ get_tro()
|
||||||
model.py instance level TroBase ─► DerivedTro, Connection, OpenPoint,
|
model.py instance level TroBase ─► DerivedTro, Connection, TroCandidate,
|
||||||
Analysis, TroDocument
|
Analysis, TroDocument
|
||||||
▲
|
▲
|
||||||
tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws)
|
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. |
|
| `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`. |
|
| `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). |
|
| `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`). |
|
| `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
|
All models validate on creation and assignment and reject unknown keys
|
||||||
(`extra="forbid"`). JSON keys keep the camelCase names of the TRO-JSON (`id`,
|
(`extra="forbid"`). JSON keys keep the camelCase names of the TRO-JSON (`id`,
|
||||||
`type`, `fbType`, `openPoints` …); Python code uses the field names
|
`type`, `fbType`, `belongsTo` …); Python code uses the field names
|
||||||
(`tro_id`, `type_name`, `fb_block`, `open_points`) — both are accepted on input.
|
(`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
|
Tests: `tests/test_model.py` (round trip, catalog-backed fields, rejection of
|
||||||
wrong keys/items, SCL rendering from a document) — run with
|
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
|
flow node is primary, the rest stay attached so the linking still sees both
|
||||||
sides of a transition. Like splits, this renumbers.
|
sides of a transition. Like splits, this renumbers.
|
||||||
- **`openpoint`** — a spot reported in a review that is **not** turned into a
|
- **`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
|
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
|
into `connect.ini`). Keep in mind: **TRO IDs are running numbers of one run and
|
||||||
|
|||||||
@@ -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
|
export.csv ─► tro_flow --json ─► <name>_tro.json ─► scl_skeleton ─► <name>_FB_Main.scl
|
||||||
(dxfmakros (derive) (TroDocument, (render) (skeleton with
|
(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
|
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 |
|
| File | Where | Who writes it |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `<name>.csv` | `%SKEL_DATA%` | dxfmakros `EXPORTCSV` |
|
| `<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) |
|
| `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 |
|
| `lib/scl_templates/*.scl.j2` | repo | maintained with the PLC engineers |
|
||||||
|
|
||||||
|
|||||||
@@ -477,5 +477,5 @@
|
|||||||
"to": "TRO05"
|
"to": "TRO05"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"openPoints": []
|
"candidates": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2349,7 +2349,7 @@ def render_connect_ini(graph: Graph, section: str = "Anlage", overrides=None) ->
|
|||||||
add("")
|
add("")
|
||||||
|
|
||||||
if overrides is not None and (overrides.tros or overrides.splits
|
if overrides is not None and (overrides.tros or overrides.splits
|
||||||
or overrides.open_points):
|
or overrides.candidates):
|
||||||
add(f"[{section}.manual]")
|
add(f"[{section}.manual]")
|
||||||
add("; Vor Ort bestaetigt, im CSV-Export nicht enthalten.")
|
add("; Vor Ort bestaetigt, im CSV-Export nicht enthalten.")
|
||||||
add(f"; Quelle: {overrides.source}")
|
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()):
|
for section_key, override in sorted(overrides.tros.items()):
|
||||||
if not override.type_name:
|
if not override.type_name:
|
||||||
continue
|
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}")
|
add(f"{section_key} = {override.type_name} | {wie} | {override.reason}")
|
||||||
for spec in overrides.splits:
|
for spec in overrides.splits:
|
||||||
add(f"{spec.name} = NEU {spec.type_name} "
|
add(f"{spec.name} = NEU {spec.type_name} "
|
||||||
f"| Separator {', '.join(spec.separators)}"
|
f"| Separator {', '.join(spec.separators)}"
|
||||||
+ (f" + Scanner {', '.join(spec.scanners)}" if spec.scanners else "")
|
+ (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}")
|
+ f" | {spec.reason}")
|
||||||
for point in overrides.open_points:
|
for point in overrides.candidates:
|
||||||
add(f"{point.name} = OFFEN {point.expect or '?'} "
|
add(f"{point.name} = OFFEN {point.type_name or '?'} "
|
||||||
f"| x={point.x:.2f} y={point.y:.2f} "
|
f"| x={point.x:.2f} y={point.y:.2f} "
|
||||||
f"| heute Teil von {point.belongs_to or '-'} "
|
f"| heute Teil von {point.belongs_to or '-'} "
|
||||||
f"| {point.note}")
|
f"| {point.note}")
|
||||||
|
|||||||
+64
-49
@@ -19,20 +19,23 @@ Klassen:
|
|||||||
DerivedTro TroBase + was tro_flow bei der Ableitung zusaetzlich weiss
|
DerivedTro TroBase + was tro_flow bei der Ableitung zusaetzlich weiss
|
||||||
(Hosts, Scanner, Begruendung, Positionsquelle ...)
|
(Hosts, Scanner, Begruendung, Positionsquelle ...)
|
||||||
Connection eine gerichtete Verbindung zwischen zwei TROs/Stationen
|
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
|
Analysis Arbeitsergebnis von tro_flow.analyse_tros
|
||||||
TroDocument die TRO-JSON (<csv>_tro.json): von tro_flow --json
|
TroDocument die TRO-JSON (<csv>_tro.json): von tro_flow --json
|
||||||
geschrieben, von scl_skeleton gelesen und geprueft
|
geschrieben, von scl_skeleton gelesen und geprueft
|
||||||
|
|
||||||
In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`,
|
In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`,
|
||||||
`fbType`, `openPoints`, ...); im Python-Code heissen die Felder weiter
|
`fbType`, `candidates`, ...); im Python-Code heissen die Felder weiter
|
||||||
`tro_id`/`type_name` (Aliase, `populate_by_name`).
|
`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 __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
@@ -51,14 +54,15 @@ __all__ = [
|
|||||||
"TroBase",
|
"TroBase",
|
||||||
"DerivedTro",
|
"DerivedTro",
|
||||||
"Connection",
|
"Connection",
|
||||||
"OpenPoint",
|
"TroCandidate",
|
||||||
"Analysis",
|
"Analysis",
|
||||||
"TroDocument",
|
"TroDocument",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
|
# 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).
|
# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell), v3 wie v2 mit
|
||||||
SCHEMA = "sps_skel-tro-v2"
|
# `candidates` statt `openPoints`.
|
||||||
|
SCHEMA = "sps_skel-tro-v3"
|
||||||
|
|
||||||
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
|
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
|
||||||
# Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt
|
# 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.
|
Kern eines TRO (Instanz in einer Anlage), unabhaengig von seiner Quelle.
|
||||||
|
|
||||||
@@ -145,37 +190,6 @@ class TroBase(BaseModel):
|
|||||||
)
|
)
|
||||||
return data
|
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):
|
class DerivedTro(TroBase):
|
||||||
"""
|
"""
|
||||||
@@ -210,22 +224,23 @@ class Connection(BaseModel):
|
|||||||
kind: str = ""
|
kind: str = ""
|
||||||
|
|
||||||
|
|
||||||
class OpenPoint(BaseModel):
|
class TroCandidate(_Positioned):
|
||||||
"""Eine im Review gemeldete Stelle, die noch kein eigener TRO ist."""
|
"""
|
||||||
|
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
|
model_config = _CONFIG
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
x: float
|
type_name: str = Field(default="", alias="type")
|
||||||
y: float
|
position: tuple[float, float, float]
|
||||||
z: float | None = None
|
|
||||||
expect: str = ""
|
|
||||||
belongs_to: str = Field(default="", alias="belongsTo")
|
belongs_to: str = Field(default="", alias="belongsTo")
|
||||||
note: str = ""
|
note: str = ""
|
||||||
|
|
||||||
def describe_position(self) -> str:
|
|
||||||
return f"{self.x:.0f}, {self.y:.0f}"
|
|
||||||
|
|
||||||
|
|
||||||
class Analysis(BaseModel):
|
class Analysis(BaseModel):
|
||||||
"""Ergebnis von tro_flow.analyse_tros: TROs, Kanten, Befunde."""
|
"""Ergebnis von tro_flow.analyse_tros: TROs, Kanten, Befunde."""
|
||||||
@@ -272,7 +287,7 @@ class TroDocument(BaseModel):
|
|||||||
# Systemgrenzen (SYSn): Materialein-/ausgang, kein TRO - keine SCL
|
# Systemgrenzen (SYSn): Materialein-/ausgang, kein TRO - keine SCL
|
||||||
boundaries: list[DerivedTro] = Field(default_factory=list)
|
boundaries: list[DerivedTro] = Field(default_factory=list)
|
||||||
connections: list[Connection] = 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)
|
findings: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+41
-60
@@ -26,15 +26,19 @@ mehrere Anlagen in einer Datei stehen koennen):
|
|||||||
reason = ... Begruendung (kommt in die Doku)
|
reason = ... Begruendung (kommt in die Doku)
|
||||||
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
|
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
|
x = 529.95 Anlagenkoordinate in mm
|
||||||
y = -3895.27
|
y = -3895.27
|
||||||
z = 1468
|
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
|
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
|
||||||
note = ... Erlaeuterung
|
note = ... Erlaeuterung
|
||||||
|
|
||||||
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
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
|
from __future__ import annotations
|
||||||
@@ -44,6 +48,7 @@ import os
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from model import Connection, TroCandidate
|
||||||
from tro_catalog import get_tro
|
from tro_catalog import get_tro
|
||||||
|
|
||||||
OVERRIDE_FILE = "tro_overrides.ini"
|
OVERRIDE_FILE = "tro_overrides.ini"
|
||||||
@@ -63,16 +68,17 @@ class TroOverride:
|
|||||||
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
|
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: str
|
# genau eins von beiden ist gesetzt
|
||||||
by_separator: bool = False
|
tro_id: str = ""
|
||||||
|
separator: str = ""
|
||||||
type_name: str = ""
|
type_name: str = ""
|
||||||
reason: str = ""
|
reason: str = ""
|
||||||
confidence: str = ""
|
confidence: str = ""
|
||||||
|
|
||||||
def matches(self, tro) -> bool:
|
def matches(self, tro) -> bool:
|
||||||
if self.by_separator:
|
if self.separator:
|
||||||
return self.key in tro.separators
|
return self.separator in tro.separators
|
||||||
return self.key == tro.tro_id
|
return self.tro_id == tro.tro_id
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -90,21 +96,12 @@ class SplitSpec:
|
|||||||
type_name: str
|
type_name: str
|
||||||
separators: list[str] = field(default_factory=list)
|
separators: list[str] = field(default_factory=list)
|
||||||
scanners: list[str] = field(default_factory=list)
|
scanners: list[str] = field(default_factory=list)
|
||||||
host: str = ""
|
hosts: list[str] = field(default_factory=list)
|
||||||
label: str = ""
|
label: str = ""
|
||||||
reason: str = ""
|
reason: str = ""
|
||||||
confidence: str = DEFAULT_CONFIDENCE
|
confidence: str = DEFAULT_CONFIDENCE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ConnectionSpec:
|
|
||||||
"""Eine vor Ort bestaetigte Verbindung zwischen zwei TROs."""
|
|
||||||
|
|
||||||
src: str
|
|
||||||
dst: str
|
|
||||||
kind: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MergeSpec:
|
class MergeSpec:
|
||||||
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
|
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
|
||||||
@@ -119,28 +116,12 @@ class MergeSpec:
|
|||||||
name: str
|
name: str
|
||||||
type_name: str
|
type_name: str
|
||||||
separators: list[str] = field(default_factory=list)
|
separators: list[str] = field(default_factory=list)
|
||||||
host: str = ""
|
hosts: list[str] = field(default_factory=list)
|
||||||
label: str = ""
|
label: str = ""
|
||||||
reason: str = ""
|
reason: str = ""
|
||||||
confidence: str = DEFAULT_CONFIDENCE
|
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
|
@dataclass
|
||||||
class Overrides:
|
class Overrides:
|
||||||
"""Alle Korrekturen zu einer CSV-Datei."""
|
"""Alle Korrekturen zu einer CSV-Datei."""
|
||||||
@@ -150,15 +131,15 @@ class Overrides:
|
|||||||
splits: list[SplitSpec] = field(default_factory=list)
|
splits: list[SplitSpec] = field(default_factory=list)
|
||||||
merges: list[MergeSpec] = field(default_factory=list)
|
merges: list[MergeSpec] = field(default_factory=list)
|
||||||
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
||||||
connections: list[ConnectionSpec] = field(default_factory=list)
|
connections: list[Connection] = field(default_factory=list)
|
||||||
open_points: list[OpenPoint] = field(default_factory=list)
|
candidates: list[TroCandidate] = field(default_factory=list)
|
||||||
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
||||||
applied: list[str] = field(default_factory=list)
|
applied: list[str] = field(default_factory=list)
|
||||||
warnings: list[str] = field(default_factory=list)
|
warnings: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
return bool(self.tros or self.splits or self.merges or self.connections
|
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:
|
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
|
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:
|
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||||
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
|
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
|
||||||
path = override_path(cfg_dir)
|
path = override_path(cfg_dir)
|
||||||
@@ -205,7 +190,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
result.connections.append(
|
result.connections.append(
|
||||||
ConnectionSpec(src=src, dst=dst, kind=rest.strip())
|
Connection(src=src, dst=dst, kind=rest.strip())
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -230,8 +215,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|||||||
key = name.upper()
|
key = name.upper()
|
||||||
by_sep = key.startswith("SEP")
|
by_sep = key.startswith("SEP")
|
||||||
result.tros[key] = TroOverride(
|
result.tros[key] = TroOverride(
|
||||||
key=key[3:] if by_sep else key,
|
tro_id="" if by_sep else key,
|
||||||
by_separator=by_sep,
|
separator=key[3:] if by_sep else "",
|
||||||
type_name=type_name,
|
type_name=type_name,
|
||||||
reason=values.get("reason", "").strip(),
|
reason=values.get("reason", "").strip(),
|
||||||
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
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"
|
f"- neuer TRO nicht angelegt"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
|
seps = _as_list(values.get("separators", ""))
|
||||||
if not seps:
|
if not seps:
|
||||||
result.warnings.append(
|
result.warnings.append(
|
||||||
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
|
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,
|
name=name,
|
||||||
type_name=type_name,
|
type_name=type_name,
|
||||||
separators=seps,
|
separators=seps,
|
||||||
scanners=[s.strip() for s in values.get("scanners", "").split(",")
|
scanners=_as_list(values.get("scanners", "")),
|
||||||
if s.strip()],
|
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
||||||
host=values.get("host", "").strip(),
|
|
||||||
label=values.get("label", "").strip(),
|
label=values.get("label", "").strip(),
|
||||||
reason=values.get("reason", "").strip(),
|
reason=values.get("reason", "").strip(),
|
||||||
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
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"
|
f"- Zusammenfassung nicht durchgefuehrt"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
|
seps = _as_list(values.get("separators", ""))
|
||||||
if len(seps) < 2:
|
if len(seps) < 2:
|
||||||
result.warnings.append(
|
result.warnings.append(
|
||||||
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
|
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,
|
name=name,
|
||||||
type_name=type_name,
|
type_name=type_name,
|
||||||
separators=seps,
|
separators=seps,
|
||||||
host=values.get("host", "").strip(),
|
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
||||||
label=values.get("label", "").strip(),
|
label=values.get("label", "").strip(),
|
||||||
reason=values.get("reason", "").strip(),
|
reason=values.get("reason", "").strip(),
|
||||||
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
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:
|
if x is None or y is None:
|
||||||
continue
|
continue
|
||||||
z_raw = values.get("z", "").strip()
|
z_raw = values.get("z", "").strip()
|
||||||
expect = values.get("expect", "").strip()
|
z = _as_float(z_raw, f"[{section}] z", result.warnings) if z_raw else None
|
||||||
if expect and get_tro(expect) is None:
|
type_name = values.get("expect", "").strip()
|
||||||
|
if type_name and get_tro(type_name) is None:
|
||||||
result.warnings.append(
|
result.warnings.append(
|
||||||
f"[{section}]: erwarteter Typ '{expect}' steht nicht im "
|
f"[{section}]: erwarteter Typ '{type_name}' steht nicht im "
|
||||||
f"TRO-Katalog"
|
f"TRO-Katalog"
|
||||||
)
|
)
|
||||||
result.open_points.append(
|
result.candidates.append(
|
||||||
OpenPoint(
|
TroCandidate(
|
||||||
name=name,
|
name=name,
|
||||||
x=x,
|
type_name=type_name,
|
||||||
y=y,
|
position=(x, y, z if z is not None else 0.0),
|
||||||
z=_as_float(z_raw, f"[{section}] z", result.warnings)
|
|
||||||
if z_raw
|
|
||||||
else None,
|
|
||||||
expect=expect,
|
|
||||||
belongs_to=values.get("belongs_to", "").strip(),
|
belongs_to=values.get("belongs_to", "").strip(),
|
||||||
note=values.get("note", "").strip(),
|
note=values.get("note", "").strip(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result.warnings.append(
|
result.warnings.append(
|
||||||
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro' oder "
|
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
||||||
f"'openpoint'"
|
f"'merge' oder 'openpoint'"
|
||||||
)
|
)
|
||||||
|
|
||||||
result.open_points.sort(key=lambda p: p.name)
|
result.candidates.sort(key=lambda p: p.name)
|
||||||
return result
|
return result
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"schema": "sps_skel-tro-v2",
|
"schema": "sps_skel-tro-v3",
|
||||||
"source": "HundM05.csv",
|
"source": "HundM05.csv",
|
||||||
"created": "2026-10-09 11:30",
|
"created": "2026-10-09 11:30",
|
||||||
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
|
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
|
||||||
@@ -917,7 +917,7 @@
|
|||||||
"kind": "bahn"
|
"kind": "bahn"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"openPoints": [],
|
"candidates": [],
|
||||||
"findings": [
|
"findings": [
|
||||||
"Scanner 0019 'Scanner :1': montiert an 0006-L, liest aber an Separator 0018 auf 0005",
|
"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.",
|
"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.",
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"schema": "sps_skel-tro-v2",
|
"schema": "sps_skel-tro-v3",
|
||||||
"source": "mubea.csv",
|
"source": "mubea.csv",
|
||||||
"created": "2026-10-09 11:38",
|
"created": "2026-10-09 11:38",
|
||||||
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
|
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
|
||||||
@@ -888,7 +888,7 @@
|
|||||||
"kind": "normal"
|
"kind": "normal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"openPoints": [],
|
"candidates": [],
|
||||||
"findings": [
|
"findings": [
|
||||||
"Kreisel2: Ausschleuselement 0057 und ILS Weiche 0125 benachbart - als 2Sep2Swi zusammengefasst (FB_ILS_MTRO_2Sep2Swi fehlt noch in der ILSLib)",
|
"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",
|
"Scanner 0008 'Scanner :5': montiert an 0010, liest aber an Separator 0042 auf 0010-R",
|
||||||
|
|||||||
+13
-2
@@ -6,7 +6,7 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
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
|
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["items"] == {"Separator": 1, "Switch": 1}
|
||||||
assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0}
|
assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0}
|
||||||
assert data["connections"][1] == {"from": "TRO01", "to": "TRO02", "kind": "weiche"}
|
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():
|
def test_items_and_fb_block_follow_the_type():
|
||||||
|
|||||||
Reference in New Issue
Block a user