diff --git a/Protolist/HolyList-Latest-Phase-Final.xlsx b/Protolist/HolyList-Latest-Phase-Final.xlsx new file mode 100644 index 0000000..7141ac4 Binary files /dev/null and b/Protolist/HolyList-Latest-Phase-Final.xlsx differ diff --git a/Protolist/HolyList-Latest-Phase-Planned.xlsx b/Protolist/HolyList-Latest-Phase-Planned.xlsx new file mode 100644 index 0000000..cb106b4 Binary files /dev/null and b/Protolist/HolyList-Latest-Phase-Planned.xlsx differ diff --git a/beamline_editor/table_utils.py b/beamline_editor/table_utils.py index cab3b88..34fb4df 100644 --- a/beamline_editor/table_utils.py +++ b/beamline_editor/table_utils.py @@ -606,3 +606,229 @@ def populate_table_dynamic( header.reset_filters() table.setUpdatesEnabled(True) + + +# ── Comparison helpers ──────────────────────────────────────────────────────── + +# Columns that uniquely identify an element across both lists +_MATCH_KEYS = ("Domain", "Prefix", "Suffix", "Index") + +# Columns that are compared but never diffed (they define identity) +_SKIP_DIFF = set(_MATCH_KEYS) + + +def _match_key(record: dict[str, Any]) -> tuple: + """Return the identity tuple used to match records across both lists.""" + def _norm(val: Any) -> str: + if val is None: + return "" + # Normalise Index the same way it is displayed + return _format_value("Index", val) if False else str(val).strip() + return tuple(_norm(record.get(k)) for k in _MATCH_KEYS) + + +def _values_equal(a: Any, b: Any) -> bool: + """True when two cell values should be considered identical.""" + if a is None and b is None: + return True + if a is None or b is None: + return False + # Numeric: compare with a small tolerance for floats + try: + fa, fb = float(a), float(b) + import math as _math + if _math.isfinite(fa) and _math.isfinite(fb): + return abs(fa - fb) < 1e-5 + except (TypeError, ValueError): + pass + return str(a).strip() == str(b).strip() + + +def _diff_text(key: str, val_a: Any, val_b: Any) -> str: + """ + Return the display string for a differing cell. + + For numeric columns: 'formatted_a / formatted_b' + For text columns: 'a / b' + """ + ta = _format_value(key, val_a) if val_a is not None else "" + tb = _format_value(key, val_b) if val_b is not None else "" + return f"{ta} / {tb}" + + +def _make_cell(text: str, bg_brush: QBrush, bold: bool = False) -> QTableWidgetItem: + cell = QTableWidgetItem(text) + cell.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + cell.setBackground(bg_brush) + cell.setForeground(QBrush(_FONT_COLOR)) + if bold: + f = cell.font() + f.setBold(True) + cell.setFont(f) + return cell + + +def populate_table_comparison( + table: QTableWidget, + list_a: list[dict[str, Any]], + list_b: list[dict[str, Any]], +) -> None: + """ + Compare two lists of element dicts and display the result in *table*. + + Identity match + -------------- + Two records are considered the same element when they agree on ALL of: + Domain, Prefix, Suffix, Index + (Index comparison is string-based after stripping whitespace so that + "001" and 1 both match.) + + Row rendering rules + ------------------- + Element only in list_a (not found in list_b): + → All columns filled from list_a; Comment = "New Element" in bold. + + Element in both lists, all remaining columns identical: + → Values from list_a; normal font. + + Element in both lists, some columns differ: + → Identity columns (Domain/Prefix/Suffix/Index): value from list_a. + → Identical columns: value from list_a, normal font. + → Differing columns: + • Numeric (int/float): "list_a_value / list_b_value" in bold + • Text: "list_a_value / list_b_value" in bold + → Comment column: "Difference" in bold (unless already set by list_a). + + Column set + ---------- + Uses the fixed COLUMNS list (same as populate_table), with filter + buttons on the filterable columns. Row background follows the + "Group" value from list_a. + + Parameters + ---------- + table : QTableWidget + list_a : list[dict] — primary / reference list (fixed column names) + list_b : list[dict] — secondary list (e.g. from read_excel_sheet) + """ + # Index list_b by identity key for O(1) lookup + index_b: dict[tuple, dict[str, Any]] = {} + for rec in list_b: + k = _match_key(rec) + index_b[k] = rec # last-wins if duplicates + + # ── table setup ─────────────────────────────────────────────────────────── + table.setUpdatesEnabled(False) + table.clearContents() + table.setRowCount(len(list_a)) + table.setColumnCount(len(COLUMNS)) + + 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) + + col_value_sets: dict[int, set[str]] = {} + for col, key in enumerate(COLUMNS): + if key in FILTER_COLS: + col_value_sets[col] = set() + + # ── pre-filter: drop rows that are identical in both lists ─────────────── + def _row_has_diff(rec_a, rec_b): + """True when at least one non-identity column differs.""" + if rec_b is None: + return True # only in list_a → always show + return any( + not _values_equal(rec_a.get(c), rec_b.get(c)) + for c in COLUMNS + if c not in _SKIP_DIFF and c != "Comment" + ) + + display_rows = [ + (rec_a, index_b.get(_match_key(rec_a))) + for rec_a in list_a + if _row_has_diff(rec_a, index_b.get(_match_key(rec_a))) + ] + table.setRowCount(len(display_rows)) + + # ── fill rows ───────────────────────────────────────────────────────────── + for row, (rec_a, rec_b) in enumerate(display_rows): + group_value = rec_a.get("Group", "") or "" + bg_hex = _GROUP_COLORS.get(str(group_value), _DEFAULT_COLOR) + bg_brush = QBrush(QColor(bg_hex)) + + for col, col_name in enumerate(COLUMNS): + val_a = rec_a.get(col_name) + + if rec_b is None: + # ── element only in list_a ──────────────────────────────────── + if col_name == "Comment": + cell = _make_cell("New Element", bg_brush, bold=True) + else: + cell = _make_cell(_format_value(col_name, val_a), + bg_brush, bold=False) + + else: + # ── element in both lists ───────────────────────────────────── + if col_name in _SKIP_DIFF: + # Identity columns — always from list_a, no diff + cell = _make_cell(_format_value(col_name, val_a), + bg_brush, bold=False) + + elif col_name == "Comment": + # Comment: check if anything differed in this row + # We compute it at the end; use a sentinel for now + cell = None # filled in below + + else: + val_b = rec_b.get(col_name) + if _values_equal(val_a, val_b): + cell = _make_cell(_format_value(col_name, val_a), + bg_brush, bold=False) + else: + text = _diff_text(col_name, val_a, val_b) + cell = _make_cell(text, bg_brush, bold=True) + + # Fill the Comment column + if col_name == "Comment" and rec_b is not None: + # Check whether any non-identity column differs + has_diff = any( + not _values_equal(rec_a.get(c), rec_b.get(c)) + for c in COLUMNS + if c not in _SKIP_DIFF and c != "Comment" + ) + if has_diff: + comment_text = "Difference" + cell = _make_cell(comment_text, bg_brush, bold=True) + else: + comment_text = _format_value("Comment", + rec_a.get("Comment")) + cell = _make_cell(comment_text, bg_brush, bold=False) + + table.setItem(row, col, cell) + if col in col_value_sets and cell is not None: + col_value_sets[col].add(cell.text()) + + # ── header filter widths ────────────────────────────────────────────────── + 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/main.py b/main.py index 657b34e..524d600 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from PyQt5 import QtWidgets from ui.BeamlineGUI import Ui_BeamlineGUI from beamline_editor import BeamlineEditorWidget -from beamline_editor.table_utils import populate_table, populate_table_dynamic +from beamline_editor.table_utils import populate_table, populate_table_comparison from beamline_editor.excel_utils import read_excel_sheet from elements import * @@ -111,8 +111,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): return rows = read_excel_sheet(fileName, "Proto-HL") - populate_table(self.UIProtoList,rows) - print(rows[0]) + populate_table_comparison(self.UIProtoList,self.plrecs,rows) + def saveTemplates(self): template=self.editor.get_templates() @@ -275,7 +275,7 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): 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):