SOme iteration with CLaude to improve the program

This commit is contained in:
reiche committed 2026-06-12 15:49:48 +02:00
1 parent 9cf907d978
commit 2e782e481c
5 files changed
+367 -13

No files matched your search

+216
View File
@@ -311,3 +311,219 @@ class BeamlineEditorWidget(QWidget):
def connections(self):
from .connections import Connection
return [i for i in self.scene.items() if isinstance(i, Connection)]
# ── layout persistence ────────────────────────────────────────────────────
def saveLayout(self, path: str) -> None:
"""
Save the current canvas to a JSON file.
Saved data
----------
version : int — file-format version (1)
nodes : list — one dict per node:
id, element_type, display_name, x, y
connections : list — one dict per wire:
src_id, src_port_index,
dst_id, dst_port_index
Parameters
----------
path : str
File path to write (created or overwritten).
"""
import json
from .nodes import BeamlineNode
from .connections import Connection
nodes_data = []
for item in self.scene.items():
if isinstance(item, BeamlineNode):
nodes_data.append({
"id": item.node_id,
"element_type": item.element_type,
"display_name": item.display_name(),
"x": item.pos().x(),
"y": item.pos().y(),
})
conns_data = []
for item in self.scene.items():
if isinstance(item, Connection):
conns_data.append({
"src_id": item.src_port.node.node_id,
"src_port_index": item.src_port.index,
"dst_id": item.dst_port.node.node_id,
"dst_port_index": item.dst_port.index,
})
payload = {
"version": 1,
"nodes": nodes_data,
"connections": conns_data,
}
with open(path, "w", encoding="utf-8") as fh:
json.dump(payload, fh, indent=2)
self.scene.status_changed.emit(f"Layout saved → {path}")
def loadLayout(self, path: str) -> None:
"""
Clear the canvas and restore a layout previously saved with
saveLayout().
on_element_added is fired for every restored node so the host
program can rebuild its own data structures.
Parameters
----------
path : str
Path to a JSON file written by saveLayout().
"""
self.clear()
self._load_layout_file(path, id_map=None, offset_y=0.0)
self.scene.status_changed.emit(f"Layout loaded ← {path}")
def importLayout(self, path: str) -> dict[str, str]:
"""
Import a saved layout onto the canvas *without* clearing existing
elements. The imported nodes are placed below all current nodes
so they do not overlap.
ID conflicts
------------
If any node_id in the file already exists on the canvas, a new
UUID is assigned automatically. The mapping is returned so the
host program can update its own records.
Parameters
----------
path : str
Path to a JSON file written by saveLayout().
Returns
-------
dict[str, str]
{ old_id: new_id } for every node whose ID was remapped.
Empty dict if there were no conflicts.
"""
existing_ids = {
item.node_id
for item in self.scene.items()
if hasattr(item, "node_id")
}
# Compute vertical offset: place imported layout below current content
from .nodes import BeamlineNode
current_nodes = [i for i in self.scene.items() if isinstance(i, BeamlineNode)]
if current_nodes:
max_y = max(n.pos().y() + n.H for n in current_nodes)
offset_y = max_y + 80 # 80 px gap between layouts
else:
offset_y = 0.0
id_map = self._load_layout_file(
path,
id_map=existing_ids,
offset_y=offset_y,
)
conflicts = {old: new for old, new in id_map.items() if old != new}
msg = (
f"Imported layout ← {path}"
+ (f" ({len(conflicts)} ID(s) remapped)" if conflicts else "")
)
self.scene.status_changed.emit(msg)
return conflicts
# ── internal loader ───────────────────────────────────────────────────────
def _load_layout_file(
self,
path: str,
id_map, # set[str] of existing IDs to check, or None
offset_y: float,
) -> dict[str, str]:
"""
Parse the JSON file and instantiate nodes + connections.
Returns a dict mapping every node_id in the file to the id actually
used on the canvas (identical unless a conflict forced a remap).
"""
import json, uuid as _uuid
from PyQt5.QtCore import QPointF
from .nodes import create_node, BeamlineNode
from .connections import Connection
with open(path, "r", encoding="utf-8") as fh:
payload = json.load(fh)
if payload.get("version", 1) != 1:
raise ValueError(
f"Unsupported layout file version: {payload.get('version')}"
)
# ── Pass 1: create nodes, remap conflicting IDs ───────────────────────
existing_ids: set[str] = set(id_map) if id_map is not None else set()
remap: dict[str, str] = {} # file_id → canvas_id
for nd in payload.get("nodes", []):
file_id = nd["id"]
element_type = nd["element_type"]
display_name = nd.get("display_name", element_type)
x = float(nd["x"])
y = float(nd["y"]) + offset_y
node = create_node(element_type, QPointF(x, y))
if file_id in existing_ids:
# Conflict: keep the freshly generated UUID the node already has
canvas_id = node.node_id
else:
# No conflict: reuse the saved ID so external records still match
node.node_id = file_id
canvas_id = file_id
remap[file_id] = canvas_id
existing_ids.add(canvas_id) # prevent later nodes conflicting too
if display_name != element_type:
node.set_display_name(display_name)
self.scene.addItem(node)
# Notify host so it can register the element
if self.scene.on_element_added:
self.scene.on_element_added(element_type, canvas_id)
# ── Pass 2: rebuild connections using remapped IDs ────────────────────
# Build a fast lookup: canvas_id → node
node_lookup: dict[str, BeamlineNode] = {}
for item in self.scene.items():
if isinstance(item, BeamlineNode):
node_lookup[item.node_id] = item
def _get_port(node: BeamlineNode, port_type: str, index: int):
"""Return the Port matching port_type + index, or None."""
candidates = (
node._all_out_ports() if port_type == "out"
else node._all_in_ports()
)
for p in candidates:
if p.index == index:
return p
return None
for cd in payload.get("connections", []):
src_id = remap.get(cd["src_id"], cd["src_id"])
dst_id = remap.get(cd["dst_id"], cd["dst_id"])
src_node = node_lookup.get(src_id)
dst_node = node_lookup.get(dst_id)
if src_node is None or dst_node is None:
continue
src_port = _get_port(src_node, "out", cd.get("src_port_index", 0))
dst_port = _get_port(dst_node, "in", cd.get("dst_port_index", 0))
if src_port is None or dst_port is None:
continue
conn = Connection(src_port, dst_port)
self.scene.addItem(conn)
return remap
+12 -12
View File
@@ -131,7 +131,7 @@ class Quadrupole(Magnet):
def __init__(self, prop):
Magnet.__init__(self, prop)
if not ('k1' in prop): # prop.has_key('k1')):
self.__dict__.update({'k1': 0})
self.__dict__.update({'k1': 0.})
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'MQUA'})
@@ -146,7 +146,7 @@ class Sextupole(Magnet):
def __init__(self, prop):
Magnet.__init__(self, prop)
if not ('k2' in prop): # prop.has_key('k2')):
self.__dict__.update({'k2': 0})
self.__dict__.update({'k2': 0.})
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'MSEX'})
@@ -162,7 +162,7 @@ class Solenoid(Magnet):
def __init__(self, prop):
Magnet.__init__(self, prop)
if not ('ks' in prop): # prop.has_key('ks')):
self.__dict__.update({'ks': 0})
self.__dict__.update({'ks': 0.})
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'MSOL'})
@@ -205,19 +205,19 @@ class Undulator(SimpleContainer):
if not ('Group' in prop): # prop.has_key('Group')):
self.__dict__.update({'Group': 'ID'})
if not ('K' in prop): # prop.has_key('K')):
self.__dict__.update({'K': 0})
self.__dict__.update({'K': 0.})
if not ('ku' in prop): # prop.has_key('ku')):
self.__dict__.update({'ku': 0})
self.__dict__.update({'ku': 0.})
if not ('kx' in prop): # prop.has_key('kx')):
self.__dict__.update({'kx': 0})
self.__dict__.update({'kx': 0.})
if not ('ky' in prop): # prop.has_key('ky')):
self.__dict__.update({'ky': 1})
self.__dict__.update({'ky': 1.})
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'UIND'})
if not ('Power' in prop): # prop.has_key('Power')):
self.__dict__.update({'Power': 0})
self.__dict__.update({'Power': 0.})
if not ('Waist' in prop): # prop.has_key('Waist')):
self.__dict__.update({'Waist': 1})
self.__dict__.update({'Waist': 1.})
@@ -232,9 +232,9 @@ class RF(SimpleContainer):
def __init__(self, prop):
SimpleContainer.__init__(self, prop)
if not ('Gradient' in prop): # prop.has_key('Gradient')):
self.__dict__.update({'Gradient': 0})
self.__dict__.update({'Gradient': 0.})
if not ('Phase' in prop): # prop.has_key('Phase')):
self.__dict__.update({'Phase': 0})
self.__dict__.update({'Phase': 0.})
if not ('Band' in prop): # prop.has_key('Band')):
self.__dict__.update({'Band': 'S'})
if not ('Frequency' in prop): # prop.has_key('Frequency')):
@@ -251,7 +251,7 @@ class RF(SimpleContainer):
# Enable flag for TDS as well as in the diagnostics
if 'TDS' in prop['Baugruppe']:
self.__dict__.update({'enable': 0})
self.__dict__.update({'enable': 0.})
def writeElement(self, app):
app.writeRF(self)
+24
View File
@@ -1,4 +1,5 @@
import sys
import json
from PyQt5 import QtWidgets, QtCore, QtGui
from ui.BeamlineGUI import Ui_BeamlineGUI
@@ -19,9 +20,32 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
QtWidgets.QVBoxLayout(self.UIEditorBox).addWidget(self.editor)
self.actionSave_Templates.triggered.connect(self.saveTemplates)
self.actionLoad_Templates.triggered.connect(self.loadTemplates)
self.actionSave.triggered.connect(self.save)
self.actionLoad.triggered.connect(self.load)
# elements, lines and branch points
self.elementDB={}
def save(self):
self.editor.saveLayout("Layouts/lattice.json")
def load(self):
self.editor.loadLayout("Layouts/lattice.json")
def saveTemplates(self):
template=self.editor.get_templates()
with (open('Settings/templates.json','w', encoding = "utf-8") as file):
json.dump(template, file, indent=2)
def loadTemplates(self):
with (open('Settings/templates.json','r', encoding = "utf-8") as file):
template=json.load(file)
self.editor.set_templates(template)
#editor = BeamlineEditorWidget(
# on_element_added = fn(element_type: str, node_id: str),
# on_elements_deleted = fn(node_ids: list[str]),
+38
View File
@@ -26,14 +26,52 @@ class Ui_BeamlineGUI(object):
self.menubar = QtWidgets.QMenuBar(BeamlineGUI)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1491, 22))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
BeamlineGUI.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(BeamlineGUI)
self.statusbar.setObjectName("statusbar")
BeamlineGUI.setStatusBar(self.statusbar)
self.actionNew_Layout = QtWidgets.QAction(BeamlineGUI)
self.actionNew_Layout.setObjectName("actionNew_Layout")
self.actionSave = QtWidgets.QAction(BeamlineGUI)
self.actionSave.setObjectName("actionSave")
self.actionSave_as = QtWidgets.QAction(BeamlineGUI)
self.actionSave_as.setObjectName("actionSave_as")
self.actionSave_Templates = QtWidgets.QAction(BeamlineGUI)
self.actionSave_Templates.setObjectName("actionSave_Templates")
self.actionLoad_Templates = QtWidgets.QAction(BeamlineGUI)
self.actionLoad_Templates.setObjectName("actionLoad_Templates")
self.actionLoad = QtWidgets.QAction(BeamlineGUI)
self.actionLoad.setObjectName("actionLoad")
self.actionQuit = QtWidgets.QAction(BeamlineGUI)
self.actionQuit.setObjectName("actionQuit")
self.menuFile.addAction(self.actionNew_Layout)
self.menuFile.addAction(self.actionLoad)
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSave_as)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionLoad_Templates)
self.menuFile.addAction(self.actionSave_Templates)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionQuit)
self.menubar.addAction(self.menuFile.menuAction())
self.retranslateUi(BeamlineGUI)
self.actionQuit.triggered.connect(self.centralwidget.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(BeamlineGUI)
def retranslateUi(self, BeamlineGUI):
_translate = QtCore.QCoreApplication.translate
BeamlineGUI.setWindowTitle(_translate("BeamlineGUI", "MainWindow"))
self.menuFile.setTitle(_translate("BeamlineGUI", "File"))
self.actionNew_Layout.setText(_translate("BeamlineGUI", "New Layout"))
self.actionSave.setText(_translate("BeamlineGUI", "Save"))
self.actionSave.setShortcut(_translate("BeamlineGUI", "Ctrl+S"))
self.actionSave_as.setText(_translate("BeamlineGUI", "Save as.."))
self.actionSave_Templates.setText(_translate("BeamlineGUI", "Save Templates"))
self.actionLoad_Templates.setText(_translate("BeamlineGUI", "Load Templates"))
self.actionLoad.setText(_translate("BeamlineGUI", "Load"))
self.actionLoad.setShortcut(_translate("BeamlineGUI", "Ctrl+L"))
self.actionQuit.setText(_translate("BeamlineGUI", "Quit"))
self.actionQuit.setShortcut(_translate("BeamlineGUI", "Ctrl+Q"))
+77 -1
View File
@@ -29,9 +29,85 @@
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionNew_Layout"/>
<addaction name="actionLoad"/>
<addaction name="actionSave"/>
<addaction name="actionSave_as"/>
<addaction name="separator"/>
<addaction name="actionLoad_Templates"/>
<addaction name="actionSave_Templates"/>
<addaction name="separator"/>
<addaction name="actionQuit"/>
</widget>
<addaction name="menuFile"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionNew_Layout">
<property name="text">
<string>New Layout</string>
</property>
</action>
<action name="actionSave">
<property name="text">
<string>Save</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionSave_as">
<property name="text">
<string>Save as..</string>
</property>
</action>
<action name="actionSave_Templates">
<property name="text">
<string>Save Templates</string>
</property>
</action>
<action name="actionLoad_Templates">
<property name="text">
<string>Load Templates</string>
</property>
</action>
<action name="actionLoad">
<property name="text">
<string>Load</string>
</property>
<property name="shortcut">
<string>Ctrl+L</string>
</property>
</action>
<action name="actionQuit">
<property name="text">
<string>Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
</widget>
<resources/>
<connections/>
<connections>
<connection>
<sender>actionQuit</sender>
<signal>triggered()</signal>
<receiver>BeamlineGUI</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>745</x>
<y>320</y>
</hint>
</hints>
</connection>
</connections>
</ui>