#!/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 _lager_raw.json und erzeugt daraus _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. _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 [--hand ] [--nachweis ] """ 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("%s" % "".join("%s" % e(str(c)) for c in r) for r in zeilen) return "%s%s
" % ( "".join("%s" % e(k) for k in kopf), z or "-") teile = [ "Lager-Nachweis", "", "

Lager-Nachweis (Mengen)

", "

Zeichnung: %s
Erzeugt: %s
Regelprofil: %s

" % ( e(str(raw.get("zeichnung"))), e(str(raw.get("erzeugt"))), e(str(raw.get("land")))), "

Interner Nachweis - die Pruefung gegen ASR/DGUV/EN ISO 14122 folgt in Phase 4.

", "

Warnungen

", "
    %s
" % ("".join("
  • %s
  • " % e(w) for w in ausgabe["_warnings"]) or "
  • keine
  • "), "

    Werte aus der Zeichnung

    ", tabelle(["Feld", "Wert", "Quelle", "Formel"], [(k, v["wert"], v["quelle"], v["formel"]) for k, v in ausgabe["_provenance"].items()]), "

    Konflikte mit der Hand-YAML (Hand gewinnt)

    ", tabelle(["Feld", "Hand", "Zeichnung", "Abweichung %"], [(k["pfad"], k["hand"], k["zeichnung"], k.get("abweichung_prozent", "")) for k in ausgabe["_conflicts"]]), "

    Von der Zeichnung gefuellte Felder (Hand leer/null)

    ", tabelle(["Feld", "Zeichnung"], [(f["pfad"], f["zeichnung"]) for f in fuellt]), "", ] 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="_lager_raw.json aus LAGER_EXPORT") ap.add_argument("yaml", help="Ziel: _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())