diff --git a/Protolist/HolyList-Latest-Phase-Current.xlsx b/Protolist/HolyList-Latest-Phase-Current.xlsx new file mode 100644 index 0000000..b8bebaf Binary files /dev/null and b/Protolist/HolyList-Latest-Phase-Current.xlsx differ diff --git a/beamline_editor/excel_utils.py b/beamline_editor/excel_utils.py new file mode 100644 index 0000000..5f075f2 --- /dev/null +++ b/beamline_editor/excel_utils.py @@ -0,0 +1,136 @@ +""" +excel_utils.py — Utility for reading an Excel sheet into a list of dicts. + +read_excel_sheet(file_path, sheet_name) + Read one tab of an Excel workbook and return its rows as a list of + dictionaries, one dict per row, keyed by the column headers in row 1. + + Parameters + ---------- + file_path : str | Path + Path to the .xlsx (or .xlsm / .xls) file. + sheet_name : str + Exact name of the tab to read. + + Returns + ------- + list[dict] + Each dict maps column header → cell value for one data row. + Empty cells are returned as None (not NaN). + The header row itself is not included in the output. + + Raises + ------ + FileNotFoundError + If file_path does not exist. + ValueError + If sheet_name is not found in the workbook, listing the + available sheet names for convenience. + ImportError + If pandas or openpyxl are not installed. + + Notes + ----- + - The first row of the sheet is always treated as the header. + - Numeric types (int, float) are preserved as Python numbers. + - datetime / date cells are returned as Python datetime objects. + - Boolean cells are returned as Python bool. + - Completely empty rows (all cells None) are dropped. + - Duplicate column names get a suffix _2, _3, … to keep them unique. + +Example +------- + from excel_utils import read_excel_sheet + + rows = read_excel_sheet("devices.xlsx", "SLS2") + for row in rows: + print(row["Domain"], row["Group"], row["s (m)"]) +""" + +from __future__ import annotations +from pathlib import Path +from typing import Any +import math + + +def read_excel_sheet( + file_path: str | Path, + sheet_name: str, +) -> list[dict[str, Any]]: + """ + Read *sheet_name* from *file_path* and return its rows as dicts. + + See module docstring for full details. + """ + try: + import pandas as pd + except ImportError: + raise ImportError( + "pandas is required: pip install pandas openpyxl" + ) from None + + path = Path(file_path) + if not path.exists(): + raise FileNotFoundError(f"Excel file not found: {path}") + + # Read with openpyxl engine; keep_default_na=False so empty cells + # come back as "" rather than NaN — we normalise them to None below. + try: + df = pd.read_excel( + path, + sheet_name = sheet_name, + engine = "openpyxl", + header = 0, # first row = column names + dtype = object, # preserve native cell types + keep_default_na = False, # don't convert "" → NaN + ) + except Exception as exc: + # Provide a helpful message when the sheet name is wrong + try: + import openpyxl + wb = openpyxl.load_workbook(path, read_only=True, data_only=True) + names = wb.sheetnames + wb.close() + except Exception: + names = [] + if names: + raise ValueError( + f"Sheet '{sheet_name}' not found in '{path.name}'. " + f"Available sheets: {names}" + ) from exc + raise + + # ── de-duplicate column names ────────────────────────────────────────────── + seen: dict[str, int] = {} + columns: list[str] = [] + for col in df.columns: + name = str(col) if col is not None else "" + if name in seen: + seen[name] += 1 + name = f"{name}_{seen[name]}" + else: + seen[name] = 1 + columns.append(name) + df.columns = columns + + # ── convert to list of dicts, normalise values ───────────────────────────── + records: list[dict[str, Any]] = [] + for _, row in df.iterrows(): + record: dict[str, Any] = {} + all_none = True + for col in columns: + val = row[col] + # Normalise: empty string and NaN → None + if val == "" or (isinstance(val, float) and math.isnan(val)): + val = None + # Convert numpy scalar types to plain Python + if hasattr(val, "item"): + val = val.item() + record[col] = val + if val is not None: + all_none = False + # Drop completely empty rows + if not all_none: + records.append(record) + + return records diff --git a/beamline_editor/table_utils.py b/beamline_editor/table_utils.py index fa8515b..cab3b88 100644 --- a/beamline_editor/table_utils.py +++ b/beamline_editor/table_utils.py @@ -2,35 +2,47 @@ table_utils.py — Utility helpers for populating Qt table widgets. populate_table(table, records) - Fill a QTableWidget from a list of dicts. + Fill a QTableWidget from a list of dicts, with Excel-style column + filters on: Domain, Prefix, Suffix, Group, Baugruppe. Columns (fixed, in this order): Domain, Prefix, Suffix, Index, s (m), z (m), x (m), y (m), L (m), Reference, Group, Baugruppe, Variante, Roll, PS-Ch. "Index" is always rendered as a zero-padded 3-digit integer (e.g. 7 → "007"). + Position columns always render with 4 decimal places. Row background colour is determined by the "Group" value: - "Section" → light grey (#D0D0D0) - "Magnet" / "Magnets" → orange (#FFAA55) - "RF" → teal (#66BBAA) + "Section" / "Start" → light grey (#D0D0D0) + "Magnet" / "Magnets" → orange (#FFAA55) + "RF" → cyan (#00FFFF) "Diagnostics" → yellow (#FFEE88) - "Vacuum" → light green (#90EE90) + "Vacuum" / "Safety" → light green (#90EE90) "ID" → light purple (#CC99FF) anything else → white (#FFFFFF) Font colour is always black. - Any key absent from a dict produces a blank cell; existing table - content is replaced. +Filter behaviour +---------------- + Filterable columns show a small ▼ button in their header cell. + Clicking it opens a checklist popup: + • "All" checkbox — when toggled, sets every entry to its new state. + • One checkbox per unique value in that column. + Rows whose cell value is unchecked are hidden; others are shown. + Filters are independent per column and cumulative across columns. """ from __future__ import annotations from typing import Any -from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView -from PyQt5.QtGui import QColor, QBrush -from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QTableWidget, QTableWidgetItem, QHeaderView, + QWidget, QVBoxLayout, QCheckBox, QFrame, + QScrollArea, QPushButton, QHBoxLayout, QApplication, +) +from PyQt5.QtGui import QColor, QBrush, QFont +from PyQt5.QtCore import Qt, QPoint # ── Column definition ───────────────────────────────────────────────────────── @@ -51,22 +63,27 @@ COLUMNS: list[str] = [ "Variante", "Roll", "PS-Ch.", + "Comment", ] -# ── Row colour map (Group value → background hex) ───────────────────────────── +# Columns that get a filter drop-down +FILTER_COLS: set[str] = {"Domain", "Prefix", "Suffix", "Group", "Baugruppe"} + +# ── Row colour map ──────────────────────────────────────────────────────────── _GROUP_COLORS: dict[str, str] = { - "Section": "#D0D0D0", # light grey - "Magnet": "#FFAA55", # orange - "Magnets": "#FFAA55", # orange (alternate spelling) - "RF": "#66BBAA", # teal - "Diagnostics": "#FFEE88", # yellow - "Vacuum": "#90EE90", # light green - "ID": "#CC99FF", # light purple + "Section": "#D0D0D0", + "Start": "#D0D0D0", + "Magnet": "#FFAA55", + "Magnets": "#FFAA55", + "RF": "#00FFFF", + "Diagnostics": "#FFEE88", + "Vacuum": "#90EE90", + "Safety": "#90EE90", + "ID": "#CC99FF", } -_DEFAULT_COLOR = "#FFFFFF" # white -_FONT_COLOR = QColor("#000000") # black text for all rows - +_DEFAULT_COLOR = "#FFFFFF" +_FONT_COLOR = QColor("#000000") # ── Formatting helpers ──────────────────────────────────────────────────────── @@ -74,7 +91,6 @@ _POSITION_COLS = {"s (m)", "z (m)", "x (m)", "y (m)", "L (m)"} def _format_value(key: str, value: Any) -> str: - """Return the display string for a single cell value.""" if key == "Index": if value is None or value == "": return "" @@ -92,6 +108,316 @@ def _format_value(key: str, value: Any) -> str: return "" if value is None else str(value) +# ── Filter popup ────────────────────────────────────────────────────────────── + +class _FilterPopup(QFrame): + """ + A floating checklist that appears below a header cell. + + Parameters + ---------- + parent : widget to use as Qt parent (keeps it on screen) + col_index : logical column index in the table + values : ordered list of unique string values in that column + active : set of values currently shown (all = no filter) + on_apply : callable(col_index, active_set) — called on OK / any change + """ + + _SS = """ + QFrame { + background: #2A2D3E; + border: 1px solid #555880; + border-radius: 4px; + } + QCheckBox { + font-family: 'Segoe UI'; + font-size: 11px; + color: #E8EAF6; + padding: 2px 4px; + } + QCheckBox::indicator { + width: 13px; + height: 13px; + border: 1px solid #7880BB; + border-radius: 2px; + background: #1E2035; + } + QCheckBox::indicator:checked { + background: #4F8EF7; + border-color: #4F8EF7; + } + QCheckBox::indicator:indeterminate { + background: #7880BB; + border-color: #7880BB; + } + QPushButton { + font-family: 'Segoe UI'; + font-size: 11px; + color: #E8EAF6; + background: #3A3D55; + border: 1px solid #555880; + border-radius: 3px; + padding: 3px 10px; + } + QPushButton:hover { background: #4F5280; } + QPushButton:pressed{ background: #4F8EF7; } + QScrollArea { background: #2A2D3E; border: none; } + QWidget { background: #2A2D3E; } + """ + + def __init__(self, parent, col_index, values, active, on_apply): + super().__init__(parent, Qt.Popup | Qt.FramelessWindowHint) + self.setStyleSheet(self._SS) + self._col = col_index + self._values = values + self._active = set(active) + self._on_apply = on_apply + self._build() + + def _build(self): + outer = QVBoxLayout(self) + outer.setContentsMargins(6, 6, 6, 6) + outer.setSpacing(4) + + # ── scrollable checklist ────────────────────────────────────────────── + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.NoFrame) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + inner_widget = QWidget() + inner_layout = QVBoxLayout(inner_widget) + inner_layout.setContentsMargins(2, 2, 2, 2) + inner_layout.setSpacing(1) + + # "All" toggle + self._all_cb = QCheckBox("All") + all_checked = len(self._active) == len(self._values) + self._all_cb.setCheckState( + Qt.Checked if all_checked + else Qt.PartiallyChecked if self._active + else Qt.Unchecked + ) + self._all_cb.setTristate(True) + font = self._all_cb.font() + font.setBold(True) + self._all_cb.setFont(font) + self._all_cb.stateChanged.connect(self._all_toggled) + inner_layout.addWidget(self._all_cb) + + # separator line + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setStyleSheet("color: #444466;") + inner_layout.addWidget(line) + + # individual value checkboxes + self._cbs: list[QCheckBox] = [] + for val in self._values: + cb = QCheckBox(val if val != "" else "(blank)") + cb.setChecked(val in self._active) + cb.stateChanged.connect(self._item_toggled) + inner_layout.addWidget(cb) + self._cbs.append(cb) + + inner_layout.addStretch() + scroll.setWidget(inner_widget) + + # cap height so it doesn't grow taller than the screen + max_h = min(300, 40 + len(self._values) * 22) + scroll.setFixedHeight(max_h) + scroll.setMinimumWidth(160) + outer.addWidget(scroll) + + # ── OK / Cancel buttons ─────────────────────────────────────────────── + btn_row = QHBoxLayout() + btn_row.setSpacing(6) + ok_btn = QPushButton("OK") + cl_btn = QPushButton("Cancel") + ok_btn.clicked.connect(self._apply) + cl_btn.clicked.connect(self.close) + btn_row.addStretch() + btn_row.addWidget(ok_btn) + btn_row.addWidget(cl_btn) + outer.addLayout(btn_row) + + self.adjustSize() + + # ── slot: "All" toggled ─────────────────────────────────────────────────── + def _all_toggled(self, state): + # Tristate cycles Unchecked→PartiallyChecked→Checked; + # we skip PartiallyChecked and treat it as Checked. + target = state != Qt.Unchecked + # block item signals while we update them + for cb in self._cbs: + cb.blockSignals(True) + cb.setChecked(target) + cb.blockSignals(False) + self._active = set(self._values) if target else set() + # keep All in a clean binary state + self._all_cb.blockSignals(True) + self._all_cb.setTristate(False) + self._all_cb.setChecked(target) + self._all_cb.setTristate(True) + self._all_cb.blockSignals(False) + + # ── slot: individual item toggled ───────────────────────────────────────── + def _item_toggled(self, _state): + self._active = { + self._values[i] + for i, cb in enumerate(self._cbs) + if cb.isChecked() + } + # update All checkbox appearance + n = len(self._active) + self._all_cb.blockSignals(True) + self._all_cb.setTristate(True) + if n == 0: + self._all_cb.setCheckState(Qt.Unchecked) + elif n == len(self._values): + self._all_cb.setCheckState(Qt.Checked) + else: + self._all_cb.setCheckState(Qt.PartiallyChecked) + self._all_cb.blockSignals(False) + + def _apply(self): + self._on_apply(self._col, self._active) + self.close() + + +# ── Filter-aware header ─────────────────────────────────────────────────────── + +class _FilterHeader(QHeaderView): + """ + A horizontal header that draws a small ▼ button on filterable columns + and opens a _FilterPopup when clicked. + """ + + _BTN_W = 16 # width of the filter button in pixels + + def __init__(self, table: QTableWidget): + super().__init__(Qt.Horizontal, table) + self._table = table + # col_index → set of currently shown values (None = no filter applied) + self._filters: dict[int, set[str] | None] = {} + # col_index → sorted list of all unique values + self._col_values: dict[int, list[str]] = {} + + self.sectionClicked.connect(self._on_section_clicked) + self.setMouseTracking(True) + + def set_column_values(self, col: int, values: list[str]): + self._col_values[col] = values + self._filters.setdefault(col, None) # None = all shown + + def paintSection(self, painter, rect, logical_index): + super().paintSection(painter, rect, logical_index) + if logical_index not in self._col_values: + return + + active = self._filters.get(logical_index) + is_filtered = active is not None and len(active) < len( + self._col_values.get(logical_index, []) + ) + + # Draw filter button in top-right of header cell + btn_rect = self._btn_rect(rect) + painter.save() + painter.setBrush(QColor("#4F8EF7" if is_filtered else "#FFFFFF")) + painter.setPen(QColor("#AAAAAA")) + painter.drawRect(btn_rect) + painter.setPen(QColor("#FFFFFF" if is_filtered else "#333333")) + f = painter.font() + f.setPointSize(7) + f.setBold(False) + painter.setFont(f) + painter.drawText(btn_rect, Qt.AlignCenter, "▼") + painter.restore() + + def _set_section_bold(self, col: int, bold: bool): + """Set the header label font to bold/normal via the model's FontRole.""" + model = self._table.horizontalHeader().model() + from PyQt5.QtGui import QFont as _QFont + f = _QFont() + f.setBold(bold) + model.setHeaderData(col, Qt.Horizontal, f, Qt.FontRole) + + def _btn_rect(self, section_rect): + from PyQt5.QtCore import QRect + bw = self._BTN_W + return QRect( + section_rect.right() - bw - 1, + section_rect.top() + 2, + bw, + section_rect.height() - 4, + ) + + def mousePressEvent(self, event): + logical = self.logicalIndexAt(event.pos()) + if logical not in self._col_values: + super().mousePressEvent(event) + return + # check if click is on the button + vis = self.visualIndex(logical) + pos = self.sectionPosition(logical) + width = self.sectionSize(logical) + from PyQt5.QtCore import QRect + sect_rect = QRect(pos, 0, width, self.height()) + btn = self._btn_rect(sect_rect) + if btn.contains(event.pos()): + self._open_popup(logical, event.globalPos()) + else: + super().mousePressEvent(event) + + def _open_popup(self, col: int, global_pos: QPoint): + values = self._col_values.get(col, []) + active = self._filters.get(col) or set(values) + popup = _FilterPopup( + self._table, col, values, active, + on_apply=self._apply_filter, + ) + # position below the header cell + popup.move(global_pos.x(), global_pos.y() + 4) + popup.show() + + def _apply_filter(self, col: int, active: set[str]): + self._filters[col] = active + is_filtered = len(active) < len(self._col_values.get(col, [])) + self._set_section_bold(col, is_filtered) + self._reapply_all() + self.viewport().update() + + def _reapply_all(self): + """Show/hide rows based on every active column filter.""" + table = self._table + for row in range(table.rowCount()): + hidden = False + for col, active in self._filters.items(): + if active is None: + continue + item = table.item(row, col) + cell_val = item.text() if item else "" + # match "(blank)" entries + if cell_val == "" and "(blank)" in active: + continue + if cell_val not in active: + hidden = True + break + table.setRowHidden(row, hidden) + + def reset_filters(self): + """Clear all filters and show every row.""" + self._filters = {col: None for col in self._col_values} + for col in self._col_values: + self._set_section_bold(col, False) + for row in range(self._table.rowCount()): + self._table.setRowHidden(row, False) + self.viewport().update() + + def _on_section_clicked(self, logical): + pass # prevent default sort on header click + + # ── Public function ──────────────────────────────────────────────────────────── def populate_table( @@ -99,41 +425,184 @@ def populate_table( records: list[dict[str, Any]], ) -> None: """ - Populate *table* from *records*. + Populate *table* from *records* and install Excel-style column filters + on the filterable columns. Parameters ---------- table : QTableWidget - The widget to fill. Column count and headers are set - automatically; any previous content is cleared. + The widget to fill. Column count, headers, and a custom + FilterHeader are installed automatically. records : list[dict] Each dict may contain any subset of the known column keys. Missing keys produce an empty cell. Extra keys are ignored. """ - # ── setup ───────────────────────────────────────────────────────────────── table.setUpdatesEnabled(False) table.clearContents() table.setRowCount(len(records)) table.setColumnCount(len(COLUMNS)) - table.setHorizontalHeaderLabels(COLUMNS) - header = table.horizontalHeader() + # Install (or reuse) the filter-aware header + existing = table.horizontalHeader() + if not isinstance(existing, _FilterHeader): + header = _FilterHeader(table) + table.setHorizontalHeader(header) + else: + header = existing + + table.setHorizontalHeaderLabels(COLUMNS) header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setStretchLastSection(True) + header.setMinimumSectionSize(60) # floor so narrow columns still show button + + # Extra right-side padding: after ResizeToContents has run, widen each + # filterable column by the button width + a small margin so the ▼ never + # overlaps the label text. + table.setUpdatesEnabled(True) # force a layout pass so sizes are computed + table.setUpdatesEnabled(False) + BTN_EXTRA = _FilterHeader._BTN_W + 8 + for col, key in enumerate(COLUMNS): + if key in FILTER_COLS: + current = header.sectionSize(col) + header.resizeSection(col, current + BTN_EXTRA) + header.setSectionResizeMode(col, QHeaderView.Interactive) # ── fill rows ───────────────────────────────────────────────────────────── + # Also collect unique values per filterable column + col_value_sets: dict[int, set[str]] = {} + for col, key in enumerate(COLUMNS): + if key in FILTER_COLS: + col_value_sets[col] = set() + for row, record in enumerate(records): group_value = record.get("Group", "") bg_hex = _GROUP_COLORS.get(group_value, _DEFAULT_COLOR) bg_brush = QBrush(QColor(bg_hex)) for col, key in enumerate(COLUMNS): - raw = record.get(key) # None when key absent - text = _format_value(key, raw) - cell = QTableWidgetItem(text) + raw = record.get(key) + text = _format_value(key, raw) + cell = QTableWidgetItem(text) cell.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) cell.setBackground(bg_brush) cell.setForeground(QBrush(_FONT_COLOR)) table.setItem(row, col, cell) + if col in col_value_sets: + col_value_sets[col].add(text) + + # ── register unique values with the header ──────────────────────────────── + for col, values_set in col_value_sets.items(): + sorted_vals = sorted(values_set, key=lambda v: (v == "", v.lower())) + header.set_column_values(col, sorted_vals) + + # reset any previous filters so all rows are visible after repopulate + header.reset_filters() table.setUpdatesEnabled(True) + + +def populate_table_dynamic( + table: QTableWidget, + records: list[dict[str, Any]], + filter_cols: set[str] | None = None, +) -> None: + """ + Like populate_table() but derives column headers from the keys of + *records* rather than the fixed COLUMNS list. Use this when the + data comes from an external source (e.g. read_excel_sheet()) whose + column names are not known in advance. + + Parameters + ---------- + table : QTableWidget + The widget to fill. + records : list[dict] + Rows as dicts. Column order follows the insertion order of keys + in the first dict; subsequent dicts may have additional or missing + keys (missing → blank cell). + filter_cols : set[str] | None + Column names that should get a ▼ filter button. If None, + defaults to the same set as populate_table(): + {"Domain", "Prefix", "Suffix", "Group", "Baugruppe"}. + + Notes + ----- + - Row background colouring still uses the "Group" column value and + the same _GROUP_COLORS palette as populate_table(). + - The "Index" zero-padding and position-column 4-decimal formatting + from _format_value() are applied as usual. + - Completely absent "Group" key → white background. + """ + if not records: + table.setRowCount(0) + table.setColumnCount(0) + return + + if filter_cols is None: + filter_cols = FILTER_COLS + + # Derive ordered column list from all keys across all records + seen_keys: dict[str, None] = {} + for rec in records: + for k in rec: + seen_keys[k] = None + columns = list(seen_keys.keys()) + + table.setUpdatesEnabled(False) + table.clearContents() + table.setRowCount(len(records)) + table.setColumnCount(len(columns)) + + # Install (or reuse) the filter-aware header + existing = table.horizontalHeader() + if not isinstance(existing, _FilterHeader): + header = _FilterHeader(table) + table.setHorizontalHeader(header) + else: + header = existing + + table.setHorizontalHeaderLabels(columns) + header.setSectionResizeMode(QHeaderView.ResizeToContents) + header.setStretchLastSection(True) + header.setMinimumSectionSize(60) + + # Collect unique values per filterable column + col_value_sets: dict[int, set[str]] = {} + for col, key in enumerate(columns): + if key in filter_cols: + col_value_sets[col] = set() + + for row, record in enumerate(records): + group_value = record.get("Group", "") + if group_value is None: + group_value = "" + bg_hex = _GROUP_COLORS.get(str(group_value), _DEFAULT_COLOR) + bg_brush = QBrush(QColor(bg_hex)) + + for col, key in enumerate(columns): + raw = record.get(key) + text = _format_value(key, raw) + cell = QTableWidgetItem(text) + cell.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + cell.setBackground(bg_brush) + cell.setForeground(QBrush(_FONT_COLOR)) + table.setItem(row, col, cell) + if col in col_value_sets: + col_value_sets[col].add(text) + + # Extra width for filterable columns so ▼ button is visible + table.setUpdatesEnabled(True) + table.setUpdatesEnabled(False) + BTN_EXTRA = _FilterHeader._BTN_W + 8 + for col, key in enumerate(columns): + if key in filter_cols: + current = header.sectionSize(col) + header.resizeSection(col, current + BTN_EXTRA) + header.setSectionResizeMode(col, QHeaderView.Interactive) + + for col, values_set in col_value_sets.items(): + sorted_vals = sorted(values_set, key=lambda v: (v == "", v.lower())) + header.set_column_values(col, sorted_vals) + + header.reset_filters() + table.setUpdatesEnabled(True) diff --git a/containers.py b/containers.py index beb5426..d0eaca5 100644 --- a/containers.py +++ b/containers.py @@ -6,7 +6,6 @@ class LineContainer: def __init__(self, namein='', Lin=0.): self.Name = namein self.LengthRes = Lin - self.LengthEnding = Lin self.Ref = {} self.Element = [] self.sRef = 0. @@ -183,7 +182,8 @@ class LineContainer: if name != 0: seq.append(ele) #print('Extend Cell',self.getResLength(), Last) - app.writeDrift(self.getResLength() - Last) + if not self.hasBranchPoint: + app.writeDrift(self.getResLength() - Last) app.writeLine(self,seq) # indicate the app that a sequence is done return seq diff --git a/main.py b/main.py index 539875b..657b34e 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import copy import sys import json import pickle @@ -7,7 +8,8 @@ from PyQt5 import QtWidgets from ui.BeamlineGUI import Ui_BeamlineGUI from beamline_editor import BeamlineEditorWidget -from beamline_editor.table_utils import populate_table +from beamline_editor.table_utils import populate_table, populate_table_dynamic +from beamline_editor.excel_utils import read_excel_sheet from elements import * from containers import * @@ -42,15 +44,21 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.actionSave.triggered.connect(self.save) self.actionLoad.triggered.connect(self.load) + self.actionCompareLayout.triggered.connect(self.compareLayout) + + # elements, lines and branch points self.elementDB={} self.savefile='' self.lines={} self.branchingID={} + self.branchingTemp={} self.PL=ProtoListe(0) self.loadFile('Layouts/SFTest2.json') self.loadTemplates() + self.verbose = False + self.plrecs=[] def saveas(self): @@ -91,6 +99,21 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.savefile=fileName self.editor.loadLayout(self.savefile.replace('.json','_gui.json')) + def compareLayout(self): + if len(self.plrecs) == 0: + return + options = QtWidgets.QFileDialog.Options() + options |= QtWidgets.QFileDialog.DontUseNativeDialog + fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open Proto-list", + "Protolist/HolyList-Latest-Phase-Current.xlsx", + "Excel File (*.xlsx)", options=options) + if not fileName: + return + + rows = read_excel_sheet(fileName, "Proto-HL") + populate_table(self.UIProtoList,rows) + print(rows[0]) + def saveTemplates(self): template=self.editor.get_templates() with (open('Settings/templates.json','w', encoding = "utf-8") as file): @@ -217,37 +240,55 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): for key in self.branchingID.keys(): print('ID:',key) - def flatten(self, node_id, case_num=1 ): - print('Flatten beamline for case:', case_num) - self.findBranchingDipoles() + def flattenSingleLine(self,node_id,branch=0,case_num=1): - self.lines.clear() + for key in self.branchingID.keys(): + self.branchingID[key]=(branch == 1) type = self.editor.getElementType(node_id) - name='XXX' + name = 'XXX' if type == 'Line': line = self.elementDB[node_id] else: if type == 'Start': name = self.elementDB[node_id].Name - line=LineContainer(namein=name,Lin=0) + line = LineContainer(namein=name, Lin=0) line.clear() print('\nUnwrapping beamline...\n') - self.unwrap(name,node_id,line,case_num) - self.PL.generateLayout(line) - plrecs = [self.PL.info[ele] for ele in self.PL.order] - populate_table(self.UIProtoList,plrecs) + self.branchingTemp.clear() + self.unwrap(name, node_id, line, case_num) + for key in self.branchingTemp.keys(): + self.elementDB[key].LengthRes=self.branchingTemp[key] + return line + def flatten(self, node_id, case_num=1 ): + print('Flatten beamline for case:', case_num) + self.findBranchingDipoles() + nbranch = len(self.branchingID.keys()) + print('Possible Number of Branches:', 2**nbranch) + self.lines.clear() + + # first write the baseline (all branching off) + + self.lines['1'] = copy.deepcopy(self.flattenSingleLine(node_id,branch=0,case_num=case_num)) + self.lines['2'] = copy.deepcopy(self.flattenSingleLine(node_id,branch=1,case_num=case_num)) + + self.PL.generateLayout(self.lines) + self.plrecs = [self.PL.info[ele] for ele in self.PL.order] + populate_table(self.UIProtoList,self.plrecs) + print(self.plrecs[0]) def unwrap(self, name, node_id,line,case_num): - print('Unwrapping Line: %s' % name) + if self.verbose: + print('Unwrapping Line: %s' % name) tree=self.editor.flatten(node_id,case_num) secondBranch = False for ele in tree.path: self.report(ele, name) type = self.editor.getElementType(ele) if type == 'Line': - print('Unwrapping- Adding a Line') + if self.verbose: + print('Unwrapping- Adding a Line') line.append(self.elementDB[ele],sRef=0,Ref='relative') subline = self.elementDB[ele] subline.clear() @@ -256,7 +297,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): if ref in self.elementDB.keys(): secondBranch = self.unwrap(name + name1, ref, subline,case_num) else: - print('Undefined reference in Line Container:', name + name1) + if self.verbose: + print('Undefined reference in Line Container:', name + name1) elif type == 'Variable Line': id = self.elementDB[ele].ID if id in self.elementDB.keys(): @@ -276,10 +318,11 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): if ele in self.branchingID.keys(): if self.branchingID[ele] is True: self.elementDB[ele].angle=self.elementDB[ele].design_angle - line.LengthRes=0 # needs to shorten the line container + line.hasBranchPoint=True return True else: self.elementDB[ele].angle=0 + line.hasBranchPoint= False if secondBranch: if tree.secondary: self.unwrap(name, tree.secondary.node_id, line, case_num) @@ -292,6 +335,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): def report(self,node_id,prefix=''): + if not self.verbose: + return type = self.editor.getElementType(node_id) if type == 'Variable Line': print('FLATTEN: Adding Variable Line') diff --git a/protoliste.py b/protoliste.py index 89a0e10..eadfe7b 100644 --- a/protoliste.py +++ b/protoliste.py @@ -17,14 +17,16 @@ class ProtoListe(ApplicationTemplate): - def generateLayout(self,line=None): + def generateLayout(self,lines=None): print('--------\nGenerating Layout for Phase: %s\n' % self.phaseName) self.order.clear() self.info.clear() self.dipole.clear() self.setRef(-0.1, -0.1) #self.iterateLine(line) - line.writeLattice(self) + for key in lines.keys(): + self.setRef(-0.1, -0.1) + lines[key].writeLattice(self) #self.SF.writeFacility(self) def setRef(self,sin=0,zin=0,xin=0,yin=1.2,exin=0,eyin=0,ezin=1): diff --git a/ui/BeamlineGUI.py b/ui/BeamlineGUI.py index 3f1be12..a3d233f 100644 --- a/ui/BeamlineGUI.py +++ b/ui/BeamlineGUI.py @@ -57,6 +57,8 @@ class Ui_BeamlineGUI(object): font.setWeight(50) self.menuFile.setFont(font) self.menuFile.setObjectName("menuFile") + self.menuLAYOUT = QtWidgets.QMenu(self.menubar) + self.menuLAYOUT.setObjectName("menuLAYOUT") BeamlineGUI.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(BeamlineGUI) self.statusbar.setObjectName("statusbar") @@ -75,6 +77,14 @@ class Ui_BeamlineGUI(object): self.actionLoad.setObjectName("actionLoad") self.actionQuit = QtWidgets.QAction(BeamlineGUI) self.actionQuit.setObjectName("actionQuit") + self.actionExportOnlineModel = QtWidgets.QAction(BeamlineGUI) + self.actionExportOnlineModel.setObjectName("actionExportOnlineModel") + self.actionCompareLayout = QtWidgets.QAction(BeamlineGUI) + self.actionCompareLayout.setObjectName("actionCompareLayout") + self.actionExportMadX = QtWidgets.QAction(BeamlineGUI) + self.actionExportMadX.setObjectName("actionExportMadX") + self.actionExportElegant = QtWidgets.QAction(BeamlineGUI) + self.actionExportElegant.setObjectName("actionExportElegant") self.menuFile.addAction(self.actionNew_Layout) self.menuFile.addAction(self.actionLoad) self.menuFile.addAction(self.actionSave) @@ -84,7 +94,12 @@ class Ui_BeamlineGUI(object): self.menuFile.addAction(self.actionSave_Templates) self.menuFile.addSeparator() self.menuFile.addAction(self.actionQuit) + self.menuLAYOUT.addAction(self.actionExportOnlineModel) + self.menuLAYOUT.addAction(self.actionCompareLayout) + self.menuLAYOUT.addAction(self.actionExportMadX) + self.menuLAYOUT.addAction(self.actionExportElegant) self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuLAYOUT.menuAction()) self.retranslateUi(BeamlineGUI) self.tabWidget.setCurrentIndex(0) @@ -97,6 +112,7 @@ class Ui_BeamlineGUI(object): self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("BeamlineGUI", "Editor")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("BeamlineGUI", "Proto List")) self.menuFile.setTitle(_translate("BeamlineGUI", "FILE")) + self.menuLAYOUT.setTitle(_translate("BeamlineGUI", "LAYOUT")) self.actionNew_Layout.setText(_translate("BeamlineGUI", "New Layout")) self.actionSave.setText(_translate("BeamlineGUI", "Save")) self.actionSave.setShortcut(_translate("BeamlineGUI", "Ctrl+S")) @@ -107,3 +123,7 @@ class Ui_BeamlineGUI(object): self.actionLoad.setShortcut(_translate("BeamlineGUI", "Ctrl+L")) self.actionQuit.setText(_translate("BeamlineGUI", "Quit")) self.actionQuit.setShortcut(_translate("BeamlineGUI", "Ctrl+Q")) + self.actionExportOnlineModel.setText(_translate("BeamlineGUI", "Export as OnlineModel Layout...")) + self.actionCompareLayout.setText(_translate("BeamlineGUI", "Compare with Protoliste...")) + self.actionExportMadX.setText(_translate("BeamlineGUI", "Export as MadX Lattice...")) + self.actionExportElegant.setText(_translate("BeamlineGUI", "Export as ELegant Lattice...")) diff --git a/ui/BeamlineGUI.ui b/ui/BeamlineGUI.ui index b7a6fcf..793d71c 100644 --- a/ui/BeamlineGUI.ui +++ b/ui/BeamlineGUI.ui @@ -82,7 +82,17 @@ background-color: rgb(13, 15, 26); + + + LAYOUT + + + + + + + @@ -129,6 +139,26 @@ background-color: rgb(13, 15, 26); Ctrl+Q + + + Export as OnlineModel Layout... + + + + + Compare with Protoliste... + + + + + Export as MadX Lattice... + + + + + Export as ELegant Lattice... + +