Files
dxfmakros/lib/lager_yaml_export.py
T

440 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
lager_yaml_export.py - Lagerplanung aus BricsCAD -> Overlay-YAML fuer das RFQ-Programm.
Liest die von LAGER_EXPORT (Lisp/LagerInsert.lsp) geschriebene Rohdatei
<zeichnung>_lager_raw.json und erzeugt daraus
<projekt>_drawing.yaml gleiches Schema wie die Hand-YAML (z.B.
cfg/example_goh_storage.yaml), aber NUR mit den
Feldern, die die Zeichnung liefert, plus
_provenance (Herkunft/Formel je Wert),
_conflicts (Abweichungen zur Hand-YAML) und
_warnings.
<projekt>_nachweis.html interner Mengen-Nachweis (optional, --nachweis)
Merge-Vertrag (umgesetzt im RFQ-Repo): Hand-Wert vorhanden und nicht null ->
Hand gewinnt; sonst Zeichnungswert. Abweichungen erscheinen in _conflicts.
Python aggregiert nur - jede fachliche Regel lebt in LISP
(Lisp/LagerPruefung.lsp, siehe doc/plan/lager_rfq.md).
Aufruf:
python lager_yaml_export.py <lager_raw.json> <out_drawing.yaml>
[--hand <hand.yaml>] [--nachweis <out.html>]
"""
import argparse
import html
import json
import sys
from collections import Counter, OrderedDict
import yaml
# Vergleichstoleranz Hand <-> Zeichnung: relativ 1 % ODER absolut 0,005
REL_TOL = 0.01
ABS_TOL = 0.005
# ------------------------------------------------------------------ Einlesen
def lade_raw(pfad):
"""Rohdaten laden. AutoLISP schreibt im ANSI-Codepage -> cp1252-Rueckfall."""
with open(pfad, "rb") as f:
roh = f.read()
try:
text = roh.decode("utf-8")
except UnicodeDecodeError:
text = roh.decode("cp1252")
return json.loads(text)
def _zahl(v, vorgabe=0.0):
return float(v) if isinstance(v, (int, float)) else vorgabe
def _m(mm, stellen=3):
return round(_zahl(mm) / 1000.0, stellen)
def _m2(mm2, stellen=1):
return round(_zahl(mm2) / 1e6, stellen)
# ------------------------------------------------------------------ Aggregation
class Sammler:
"""Werte mit Herkunft sammeln: setze() schreibt in das Overlay und die Provenienz."""
def __init__(self):
self.provenance = OrderedDict()
self.warnungen = []
def herkunft(self, pfad, wert, quelle, formel):
self.provenance[pfad] = {"wert": wert, "quelle": quelle, "formel": formel}
return wert
def warnung(self, text):
if text not in self.warnungen:
self.warnungen.append(text)
def _haeufigster(werte, pfad, s):
"""Haeufigster Wert; bei mehreren verschiedenen Werten eine Warnung."""
werte = [w for w in werte if w is not None]
if not werte:
return None
zaehlung = Counter(werte)
wert, _ = zaehlung.most_common(1)[0]
if len(zaehlung) > 1:
s.warnung("%s: Bereiche mit unterschiedlichen Werten %s - haeufigster Wert %s uebernommen"
% (pfad, sorted(zaehlung), wert))
return wert
def _haengeebenen(reihen):
"""Laenge je Haengeebene (Schienenhoehe, Warenlaenge) summieren.
Jede Reihe traegt ihre Laenge in jede ihrer Haengeebenen ein -
rack_length_m ist die Summe aller Reihen der Ebene.
"""
summen = OrderedDict()
for r in reihen:
for e in r.get("haengeebenen") or []:
key = (_zahl(e.get("schienenhoehe_mm")), _zahl(e.get("warenlaenge_mm")))
summen[key] = summen.get(key, 0.0) + _zahl(r.get("laenge_mm"))
return [{"rail_height_m": _m(h), "rack_length_m": round(l / 1000.0, 1), "goods_length_m": _m(w)}
for (h, w), l in sorted(summen.items(), key=lambda kv: -kv[0][0])]
def aggregiere(raw):
"""Rohdaten -> (overlay, sammler). overlay enthaelt nur Zeichnungswerte."""
s = Sammler()
for w in raw.get("warnungen") or []:
s.warnung(w)
belag_yaml = raw.get("belag_yaml") or {}
reihen_als_gitter = bool(raw.get("reihen_als_gitterrost"))
geschosse = sorted(raw.get("geschosse") or [],
key=lambda g: (_zahl(g.get("hoehe_belag_mm")), g.get("nr") or 0))
bereiche = raw.get("bereiche") or []
alle_reihen = list(raw.get("reihen") or [])
racks = raw.get("goh_racks") or []
gaenge = raw.get("gaenge") or []
# goh_rack-Bloecke zaehlen wie Regalreihen (Herkunft wird mitgefuehrt)
for r in racks:
alle_reihen.append(dict(r, quelle="goh_rack"))
ohne_geschoss = [r for r in alle_reihen if r.get("geschoss") is None]
if ohne_geschoss and geschosse:
s.warnung("%d Regalreihe(n) ohne Geschoss - nicht in levels enthalten" % len(ohne_geschoss))
overlay = OrderedDict()
# ---------------------------------------------------------- layout
layout = OrderedDict()
if bereiche:
quelle = "LG_BEREICH (%d Bereich(e))" % len(bereiche)
felder = [
("aisle_width_m", "gangbreite_mm", lambda v: _m(v), "GANGBREITE_mm / 1000"),
("rack_rows_per_aisle", "reihen_je_gang", int, "REIHEN_JE_GANG"),
("rack_depth_m", "regaltiefe_mm", lambda v: _m(v), "REGALTIEFE_mm / 1000"),
("trolley_rails_per_aisle", "schienen_je_gang", int, "SCHIENEN_JE_GANG"),
]
for feld, key, conv, formel in felder:
w = _haeufigster([conv(b[key]) for b in bereiche if b.get(key) is not None],
"layout." + feld, s)
if w is not None:
layout[feld] = s.herkunft("layout." + feld, w, quelle, formel)
elif alle_reihen:
w = _haeufigster([_m(r["tiefe_mm"]) for r in alle_reihen if r.get("tiefe_mm") is not None],
"layout.rack_depth_m", s)
if w is not None:
layout["rack_depth_m"] = s.herkunft("layout.rack_depth_m", w,
"Regalreihen (Tiefe)", "haeufigste Reihentiefe")
# Gaenge je Geschoss: Anzahl und Summe der Laengen (nur Gaenge zwischen den
# Reihen, ART=gang - auch manuelle). Randgaenge entlang der Kontur zaehlen
# nicht als aisle, stecken aber im Belag (walkway) und werden im Detail genannt.
gang_je_gs = {}
rand_je_gs = {}
for g in gaenge:
if g.get("art") == "gang":
n, l = gang_je_gs.get(g.get("geschoss"), (0, 0.0))
gang_je_gs[g.get("geschoss")] = (n + 1, l + _zahl(g.get("laenge_mm")))
elif g.get("art") == "randgang":
rand_je_gs[g.get("geschoss")] = rand_je_gs.get(g.get("geschoss"), 0.0) + _zahl(g.get("laenge_mm"))
if gang_je_gs:
gs_max = max(gang_je_gs, key=lambda k: gang_je_gs[k][1])
detail = ", ".join("G%s: %d / %.1f m" % (k, v[0], v[1] / 1000.0)
for k, v in sorted(gang_je_gs.items(), key=lambda kv: str(kv[0])))
if rand_je_gs:
detail += "; ohne Randgaenge " + ", ".join(
"G%s: %.1f m" % (k, v / 1000.0)
for k, v in sorted(rand_je_gs.items(), key=lambda kv: str(kv[0])))
layout["aisles"] = s.herkunft("layout.aisles", gang_je_gs[gs_max][0],
"LG_GANG_* (ART=gang)",
"Anzahl Hauptgaenge, Geschoss mit der groessten Ganglaenge (%s)" % detail)
layout["aisle_length_m"] = s.herkunft("layout.aisle_length_m",
round(gang_je_gs[gs_max][1] / 1000.0, 1),
"LG_GANG_* (ART=gang)",
"Summe Hauptganglaengen je Ebene, groesste (%s)" % detail)
bboxen = [g["kontur_bbox"] for g in geschosse if g.get("kontur_bbox")]
if bboxen:
xmin = min(b[0] for b in bboxen)
ymin = min(b[1] for b in bboxen)
xmax = max(b[2] for b in bboxen)
ymax = max(b[3] for b in bboxen)
dx, dy = xmax - xmin, ymax - ymin
layout["block_length_m"] = s.herkunft("layout.block_length_m", round(max(dx, dy) / 1000.0, 2),
"Belagskonturen LG_GESCHOSS",
"achsparallele Huelle aller Konturen, laengere Seite")
layout["block_width_m"] = s.herkunft("layout.block_width_m", round(min(dx, dy) / 1000.0, 2),
"Belagskonturen LG_GESCHOSS",
"achsparallele Huelle aller Konturen, kuerzere Seite")
if layout:
overlay["layout"] = layout
# ---------------------------------------------------------- levels
levels = []
for i, g in enumerate(geschosse):
p = "levels[%d]" % i
nr = g.get("nr")
lv = OrderedDict()
if g.get("bezeichnung"):
lv["remark"] = s.herkunft(p + ".remark", g["bezeichnung"], "LG_GESCHOSS %s" % nr, "BEZEICHNUNG")
lv["floor_elevation_m"] = s.herkunft(p + ".floor_elevation_m", _m(g.get("hoehe_belag_mm")),
"LG_GESCHOSS %s" % nr, "HOEHE_BELAG_mm / 1000")
if g.get("lichte_hoehe_mm") is not None:
lv["clear_height_m"] = s.herkunft(p + ".clear_height_m", _m(g["lichte_hoehe_mm"]),
"LG_GESCHOSS %s" % nr, "LICHTE_HOEHE_mm (Vorgabe)")
elif i + 1 < len(geschosse):
nx = geschosse[i + 1]
lichte = _zahl(nx.get("hoehe_belag_mm")) - _zahl(nx.get("konstr_hoehe_mm")) \
- _zahl(g.get("hoehe_belag_mm"))
lv["clear_height_m"] = s.herkunft(
p + ".clear_height_m", _m(lichte), "LG_GESCHOSS %s / %s" % (nr, nx.get("nr")),
"Belag darueber (%s) - Konstruktionshoehe darueber (%s) - eigener Belag (%s), mm"
% (nx.get("hoehe_belag_mm"), nx.get("konstr_hoehe_mm"), g.get("hoehe_belag_mm")))
else:
s.warnung("Geschoss %s (oberstes): clear_height_m nicht berechenbar - LICHTE_HOEHE_mm setzen" % nr)
reihen = [r for r in alle_reihen if r.get("geschoss") == nr]
hl = _haengeebenen(reihen)
if hl:
lv["hanging_levels"] = hl
n_rack = sum(1 for r in reihen if r.get("quelle") == "goh_rack")
for j, e in enumerate(hl):
s.herkunft("%s.hanging_levels[%d].rack_length_m" % (p, j), e["rack_length_m"],
"%d Regalreihe(n) Geschoss %s (davon %d goh_rack, Haengeebenen aus Vorgabe)"
% (len(reihen), nr, n_rack),
"Summe LAENGE aller Reihen mit Schienenhoehe %s / Warenlaenge %s"
% (e["rail_height_m"], e["goods_length_m"]))
spec = belag_yaml.get(g.get("belag_art") or "", g.get("belag_art"))
if spec: # Buehne (kein Hallenboden)
gs_gaenge = [x for x in gaenge if x.get("geschoss") == nr]
belag = sum(_zahl(x.get("laenge_mm")) * _zahl(x.get("breite_mm"))
for x in gs_gaenge if x.get("belag") != "gitterrost")
gitter = sum(_zahl(x.get("laenge_mm")) * _zahl(x.get("breite_mm"))
for x in gs_gaenge if x.get("belag") == "gitterrost")
reihen_fl = sum(_zahl(r.get("laenge_mm")) * _zahl(r.get("tiefe_mm")) for r in reihen)
if reihen_als_gitter:
gitter += reihen_fl
if gs_gaenge:
formel = "Summe LAENGE x BREITE der Gaenge/Quergaenge mit Belag"
quelle = "LG_GANG_* Geschoss %s, %d Flaeche(n)" % (nr, len(gs_gaenge))
else:
# Ohne Gangflaechen: Belag = Kontur minus Reihengrundflaeche
belag = _zahl(g.get("kontur_flaeche_mm2")) - reihen_fl
formel = "Konturflaeche - Grundflaeche der Regalreihen (keine LG_GANG-Flaechen)"
quelle = "Belagskontur Geschoss %s" % nr
lv["walkway"] = OrderedDict([
("area_m2", s.herkunft(p + ".walkway.area_m2", _m2(belag), quelle, formel)),
("spec", s.herkunft(p + ".walkway.spec", spec, "LG_GESCHOSS %s" % nr, "BELAG_ART")),
])
lv["grating"] = s.herkunft(
p + ".grating", _m2(gitter), "Geschoss %s" % nr,
"Gaenge mit Belag gitterrost" + (" + Grundflaeche der Regalreihen (Laenge x Tiefe)"
if reihen_als_gitter else ""))
levels.append(lv)
if levels:
overlay["levels"] = levels
return overlay, s
# ------------------------------------------------------------------ Abgleich
def _gleich(a, b):
if isinstance(a, bool) or isinstance(b, bool):
return a == b
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
return abs(a - b) <= max(ABS_TOL, REL_TOL * max(abs(a), abs(b)))
return str(a).strip() == str(b).strip()
def _blaetter(obj, pfad=""):
"""(pfad, wert) aller Blattwerte; Listen per Index, _-Schluessel ausgenommen."""
if isinstance(obj, dict):
for k, v in obj.items():
if str(k).startswith("_"):
continue
yield from _blaetter(v, "%s.%s" % (pfad, k) if pfad else str(k))
elif isinstance(obj, list):
for i, v in enumerate(obj):
yield from _blaetter(v, "%s[%d]" % (pfad, i))
else:
yield pfad, obj
def _hole(obj, pfad):
"""Wert unter pfad ("levels[1].walkway.area_m2"); (gefunden, wert)."""
teile = []
for t in pfad.split("."):
while "[" in t:
name, rest = t.split("[", 1)
if name:
teile.append(name)
idx, t = rest.split("]", 1)
teile.append(int(idx))
if t:
teile.append(t)
cur = obj
for t in teile:
if isinstance(t, int):
if not isinstance(cur, list) or t >= len(cur):
return False, None
cur = cur[t]
else:
if not isinstance(cur, dict) or t not in cur:
return False, None
cur = cur[t]
return True, cur
def abgleich(overlay, hand):
"""Overlay gegen Hand-YAML: (konflikte, fuellt).
konflikte = Hand hat einen anderen Wert (Hand gewinnt beim Merge),
fuellt = Hand-Feld fehlt oder ist null (Zeichnung fuellt es).
"""
konflikte, fuellt = [], []
for pfad, wert in _blaetter(overlay):
gefunden, h = _hole(hand, pfad)
if not gefunden or h is None:
fuellt.append({"pfad": pfad, "zeichnung": wert})
elif not isinstance(h, (dict, list)) and not _gleich(h, wert):
k = {"pfad": pfad, "hand": h, "zeichnung": wert}
if isinstance(h, (int, float)) and isinstance(wert, (int, float)) and h:
k["abweichung_prozent"] = round((wert - h) / abs(h) * 100.0, 1)
konflikte.append(k)
n_hand = len(hand.get("levels") or []) if isinstance(hand, dict) else 0
n_zg = len(overlay.get("levels") or [])
if n_hand and n_zg and n_hand != n_zg:
konflikte.append({"pfad": "levels", "hand": "%d Ebenen" % n_hand,
"zeichnung": "%d Ebenen" % n_zg})
return konflikte, fuellt
# ------------------------------------------------------------------ Ausgabe
def _yaml_text(daten):
def _dict_repr(dumper, d):
return dumper.represent_mapping("tag:yaml.org,2002:map", d.items())
yaml.SafeDumper.add_representer(OrderedDict, _dict_repr)
return yaml.safe_dump(daten, sort_keys=False, allow_unicode=True,
default_flow_style=False, width=120)
def baue_ausgabe(raw, hand=None):
overlay, s = aggregiere(raw)
ausgabe = OrderedDict(overlay)
ausgabe["_provenance"] = s.provenance
konflikte, fuellt = abgleich(overlay, hand) if hand is not None else ([], [])
ausgabe["_conflicts"] = konflikte
ausgabe["_warnings"] = s.warnungen
return ausgabe, fuellt
def schreibe_yaml(pfad, ausgabe, raw):
kopf = ("# Overlay aus der Zeichnung - erzeugt von lib/lager_yaml_export.py\n"
"# Zeichnung: %s (%s)\n"
"# Merge im RFQ-Programm: Hand-YAML gewinnt, diese Werte fuellen nur fehlende/null-Felder.\n"
"# _provenance/_conflicts/_warnings sind Zusatzinformation und kein Teil des RFQ-Schemas.\n"
% (raw.get("zeichnung", "?"), raw.get("erzeugt", "?")))
with open(pfad, "w", encoding="utf-8", newline="\n") as f:
f.write(kopf)
f.write(_yaml_text(ausgabe))
def schreibe_nachweis(pfad, ausgabe, fuellt, raw):
e = html.escape
def tabelle(kopf, zeilen):
z = "".join("<tr>%s</tr>" % "".join("<td>%s</td>" % e(str(c)) for c in r) for r in zeilen)
return "<table><thead><tr>%s</tr></thead><tbody>%s</tbody></table>" % (
"".join("<th>%s</th>" % e(k) for k in kopf), z or "<tr><td colspan=9>-</td></tr>")
teile = [
"<!doctype html><html lang='de'><head><meta charset='utf-8'><title>Lager-Nachweis</title>",
"<style>body{font-family:Segoe UI,Arial,sans-serif;margin:24px;color:#222}"
"table{border-collapse:collapse;margin:8px 0 24px}td,th{border:1px solid #bbb;padding:4px 8px;"
"text-align:left;vertical-align:top}th{background:#eee}.w{color:#a40}</style></head><body>",
"<h1>Lager-Nachweis (Mengen)</h1>",
"<p>Zeichnung: %s<br>Erzeugt: %s<br>Regelprofil: %s</p>" % (
e(str(raw.get("zeichnung"))), e(str(raw.get("erzeugt"))), e(str(raw.get("land")))),
"<p class='w'>Interner Nachweis - die Pruefung gegen ASR/DGUV/EN ISO 14122 folgt in Phase 4.</p>",
"<h2>Warnungen</h2>",
"<ul>%s</ul>" % ("".join("<li class='w'>%s</li>" % e(w) for w in ausgabe["_warnings"]) or "<li>keine</li>"),
"<h2>Werte aus der Zeichnung</h2>",
tabelle(["Feld", "Wert", "Quelle", "Formel"],
[(k, v["wert"], v["quelle"], v["formel"]) for k, v in ausgabe["_provenance"].items()]),
"<h2>Konflikte mit der Hand-YAML (Hand gewinnt)</h2>",
tabelle(["Feld", "Hand", "Zeichnung", "Abweichung %"],
[(k["pfad"], k["hand"], k["zeichnung"], k.get("abweichung_prozent", ""))
for k in ausgabe["_conflicts"]]),
"<h2>Von der Zeichnung gefuellte Felder (Hand leer/null)</h2>",
tabelle(["Feld", "Zeichnung"], [(f["pfad"], f["zeichnung"]) for f in fuellt]),
"</body></html>",
]
with open(pfad, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(teile))
def main(argv=None):
ap = argparse.ArgumentParser(description="Lager-Rohdaten -> RFQ-Overlay-YAML")
ap.add_argument("raw", help="<zeichnung>_lager_raw.json aus LAGER_EXPORT")
ap.add_argument("yaml", help="Ziel: <projekt>_drawing.yaml")
ap.add_argument("--hand", help="Hand-YAML fuer den Abgleich (Konfliktliste)")
ap.add_argument("--nachweis", help="Ziel: interner Nachweis als HTML")
a = ap.parse_args(argv)
try:
raw = lade_raw(a.raw)
except (OSError, ValueError) as ex:
print("FEHLER: Rohdaten nicht lesbar: %s" % ex, file=sys.stderr)
return 2
hand = None
if a.hand:
try:
with open(a.hand, encoding="utf-8") as f:
hand = yaml.safe_load(f) or {}
except (OSError, yaml.YAMLError) as ex:
print("FEHLER: Hand-YAML nicht lesbar: %s" % ex, file=sys.stderr)
return 2
ausgabe, fuellt = baue_ausgabe(raw, hand)
schreibe_yaml(a.yaml, ausgabe, raw)
if a.nachweis:
schreibe_nachweis(a.nachweis, ausgabe, fuellt, raw)
print("Overlay: %s (%d Werte, %d Konflikt(e), %d Warnung(en))"
% (a.yaml, len(ausgabe["_provenance"]), len(ausgabe["_conflicts"]), len(ausgabe["_warnings"])))
return 0
if __name__ == "__main__":
sys.exit(main())