Working on comparison with protoliste

This commit is contained in:
reiche committed 2026-07-10 16:17:10 +02:00
1 parent fc60ef8c89
commit b0434d2b0c
8 files changed
+752 -50

No files matched your search

Binary file not shown.
+136
View File
@@ -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
+500 -31
View File
@@ -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)
+2 -2
View File
@@ -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
+60 -15
View File
@@ -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')
+4 -2
View File
@@ -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):
+20
View File
@@ -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..."))
+30
View File
@@ -82,7 +82,17 @@ background-color: rgb(13, 15, 26);</string>
<addaction name="separator"/>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuLAYOUT">
<property name="title">
<string>LAYOUT</string>
</property>
<addaction name="actionExportOnlineModel"/>
<addaction name="actionCompareLayout"/>
<addaction name="actionExportMadX"/>
<addaction name="actionExportElegant"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuLAYOUT"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionNew_Layout">
@@ -129,6 +139,26 @@ background-color: rgb(13, 15, 26);</string>
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionExportOnlineModel">
<property name="text">
<string>Export as OnlineModel Layout...</string>
</property>
</action>
<action name="actionCompareLayout">
<property name="text">
<string>Compare with Protoliste...</string>
</property>
</action>
<action name="actionExportMadX">
<property name="text">
<string>Export as MadX Lattice...</string>
</property>
</action>
<action name="actionExportElegant">
<property name="text">
<string>Export as ELegant Lattice...</string>
</property>
</action>
</widget>
<resources/>
<connections>