Good progress in adding features

This commit is contained in:
reiche committed 2026-06-15 11:16:29 +02:00
1 parent 2e782e481c
commit 61bca58382
7 files changed
+311 -48

No files matched your search

+152 -3
View File
@@ -21,7 +21,7 @@ import uuid, math
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import QRectF, QPointF, Qt
from PyQt5.QtGui import (
QBrush, QPen, QColor, QPainter,
QBrush, QPen, QColor, QPainter, QPainterPath,
QFont, QLinearGradient, QPolygonF,
)
from .ports import Port
@@ -37,6 +37,13 @@ _STYLES: dict[str, dict] = {
"Overlap": {"top": "#4A148C", "bot": "#2A0A5C", "w": 72, "h": 40},
"Line": {"top": "#263238", "bot": "#1A2428", "w": 96, "h": 38},
"Variable Line": {"top": "#1C313A", "bot": "#0F1F26", "w": 96, "h": 38},
"Solenoid": {"top": "#00838F", "bot": "#005662", "w": 60, "h": 40},
"Corrector": {"top": "#6A1B9A", "bot": "#4A148C", "w": 44, "h": 44},
"RF": {"top": "#AD1457", "bot": "#880E4F", "w": 64, "h": 44},
"Undulator": {"top": "#558B2F", "bot": "#33691E", "w": 88, "h": 36},
"Diagnostics": {"top": "#4527A0", "bot": "#311B92", "w": 64, "h": 44},
"Target": {"top": "#B71C1C", "bot": "#7B0000", "w": 36, "h": 36},
"Alignment": {"top": "#1565C0", "bot": "#0D3B8C", "w": 36, "h": 36},
"Start": {"top": "#00695C", "bot": "#004D40", "w": 54, "h": 40},
"End": {"top": "#4E342E", "bot": "#3E2723", "w": 54, "h": 40},
}
@@ -306,18 +313,160 @@ class EndNode(BeamlineNode):
min(sq, self.H - r * sq)))
class SolenoidNode(BeamlineNode):
"""Solenoid coil — teal rounded rect with circular coil symbol."""
def __init__(self, pos): super().__init__("Solenoid", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#00838F"), 1.5))
p.drawRoundedRect(QRectF(0, 0, self.W, self.H), 6, 6)
# coil loops
p.setPen(QPen(QColor("#80DEEA"), 1.2))
step = 10
y1, y2 = 8.0, float(self.H - 8)
x = 10.0
while x < self.W - 8:
p.drawEllipse(QPointF(x + step / 2, self.H / 2),
step / 2, (y2 - y1) / 2)
x += step
class CorrectorNode(BeamlineNode):
"""Corrector — purple square with crossed-arrows symbol."""
def __init__(self, pos): super().__init__("Corrector", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#7B1FA2"), 1.5))
p.drawRect(QRectF(0, 0, self.W, self.H))
cx, cy = self.W / 2, self.H / 2
r = min(cx, cy) - 5
p.setPen(QPen(QColor("#CE93D8"), 1.5))
# vertical arrow
p.drawLine(QPointF(cx, cy - r), QPointF(cx, cy + r))
p.drawLine(QPointF(cx, cy - r), QPointF(cx - 4, cy - r + 6))
p.drawLine(QPointF(cx, cy - r), QPointF(cx + 4, cy - r + 6))
# horizontal arrow
p.drawLine(QPointF(cx - r, cy), QPointF(cx + r, cy))
p.drawLine(QPointF(cx + r, cy), QPointF(cx + r - 6, cy - 4))
p.drawLine(QPointF(cx + r, cy), QPointF(cx + r - 6, cy + 4))
class RFNode(BeamlineNode):
"""RF cavity — crimson rect with sine-wave decoration."""
def __init__(self, pos): super().__init__("RF", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#C2185B"), 1.5))
p.drawRoundedRect(QRectF(0, 0, self.W, self.H), 5, 5)
# sine wave
path = QPainterPath()
cy = self.H / 2
amp = self.H / 4 - 2
pts = 40
x0, x1 = 8.0, float(self.W - 8)
for i in range(pts + 1):
t = i / pts
x = x0 + t * (x1 - x0)
y = cy - amp * math.sin(t * 2 * math.pi * 2)
if i == 0:
path.moveTo(x, y)
else:
path.lineTo(x, y)
p.setPen(QPen(QColor("#F48FB1"), 1.5))
p.setBrush(Qt.NoBrush)
p.drawPath(path)
class UndulatorNode(BeamlineNode):
"""Undulator / wiggler — olive-green rect with alternating pole marks."""
def __init__(self, pos): super().__init__("Undulator", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#558B2F"), 1.5))
p.drawRect(QRectF(0, 0, self.W, self.H))
# alternating pole blocks
pole_w = 10
n_poles = int(self.W // pole_w)
for i in range(n_poles):
col = QColor("#AED581") if i % 2 == 0 else QColor("#33691E")
p.setBrush(QBrush(col))
p.setPen(Qt.NoPen)
x = i * pole_w
# top half pole
p.drawRect(QRectF(x, 2, pole_w, self.H / 2 - 3))
# bottom half pole (opposite polarity → alternate colour swapped)
col2 = QColor("#33691E") if i % 2 == 0 else QColor("#AED581")
p.setBrush(QBrush(col2))
p.drawRect(QRectF(x, self.H / 2 + 1, pole_w, self.H / 2 - 3))
class DiagnosticsNode(BeamlineNode):
"""Diagnostics — indigo rect with oscilloscope / eye symbol."""
def __init__(self, pos): super().__init__("Diagnostics", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#512DA8"), 1.5))
p.drawRoundedRect(QRectF(0, 0, self.W, self.H), 5, 5)
cx, cy = self.W / 2, self.H / 2
# eye outline
rx, ry = self.W / 2 - 8, self.H / 2 - 6
p.setPen(QPen(QColor("#B39DDB"), 1.5))
p.setBrush(Qt.NoBrush)
p.drawEllipse(QPointF(cx, cy), rx, ry)
# pupil
p.setBrush(QBrush(QColor("#7E57C2")))
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(cx, cy), 5, 5)
class TargetNode(BeamlineNode):
"""Target marker — red circle with crosshair."""
def __init__(self, pos): super().__init__("Target", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#B71C1C"), 1.5))
p.drawEllipse(QRectF(0, 0, self.W, self.H))
cx, cy = self.W / 2, self.H / 2
r = min(cx, cy) - 4
p.setPen(QPen(QColor("#EF9A9A"), 1.2))
p.drawLine(QPointF(cx - r, cy), QPointF(cx + r, cy))
p.drawLine(QPointF(cx, cy - r), QPointF(cx, cy + r))
p.setBrush(QBrush(QColor("#EF5350")))
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(cx, cy), 4, 4)
class AlignmentNode(BeamlineNode):
"""Alignment marker — blue square with corner tick marks."""
def __init__(self, pos): super().__init__("Alignment", pos)
def _draw_body(self, p):
p.setBrush(QBrush(self._gradient()))
p.setPen(QPen(QColor("#1565C0"), 1.5))
p.drawRect(QRectF(0, 0, self.W, self.H))
t = 6.0 # tick length
p.setPen(QPen(QColor("#90CAF9"), 1.5))
for cx, cy in [(0.0, 0.0), (float(self.W), 0.0),
(0.0, float(self.H)), (float(self.W), float(self.H))]:
dx = t if cx == 0 else -t
dy = t if cy == 0 else -t
p.drawLine(QPointF(cx, cy), QPointF(cx + dx, cy))
p.drawLine(QPointF(cx, cy), QPointF(cx, cy + dy))
# ── Factory ───────────────────────────────────────────────────────────────────
_NODE_MAP: dict[str, type] = {
"Dipole": DipoleNode,
"Quadrupole": QuadrupoleNode,
"Sextupole": SextupoleNode,
"Solenoid": SolenoidNode,
"Corrector": CorrectorNode,
"RF": RFNode,
"Undulator": UndulatorNode,
"Diagnostics": DiagnosticsNode,
"Reference": ReferenceNode,
"Target": TargetNode,
"Alignment": AlignmentNode,
"Overlap": OverlapNode,
"Line": LineNode,
"Variable Line": VariableLineNode,
"Start": StartNode,
"End": EndNode,
"Line": LineNode,
"Variable Line": VariableLineNode,
}
+3 -3
View File
@@ -15,10 +15,10 @@ from PyQt5.QtCore import QRectF
from .nodes import _STYLES
ELEMENT_GROUPS: list[tuple[str, list[str]]] = [
("Elements", ["Dipole", "Quadrupole", "Sextupole"]),
("Markers", ["Reference", "Overlap", "Start", "End"]),
("Elements", ["Dipole", "Quadrupole", "Sextupole",
"Solenoid", "Corrector", "RF", "Undulator", "Diagnostics"]),
("Markers", ["Reference", "Start", "End", "Target", "Alignment"]),
("Containers", ["Line", "Variable Line"]),
("Templates", []),
]
PALETTE_SS = """
+25 -6
View File
@@ -34,6 +34,9 @@ class SimpleContainer:
if not ('OffsetS' in prop):
prop.update({'OffsetS': 0.})
if not ('relOff' in prop):
prop.update({'relOff': True})
if not ('Tilt' in prop): # prop.has_key('Tilt')):
prop.update({'Tilt': 0.})
@@ -87,6 +90,8 @@ class Magnet(SimpleContainer):
SimpleContainer.__init__(self, prop)
if not ('Group' in prop): # prop.has_key('Group')):
self.__dict__.update({'Group': 'Magnets'})
if not ('PSChannels' in prop): # prop.has_key('Group')):
self.__dict__.update({'PSChannels': 1})
class Dipole(Magnet):
def __init__(self, prop):
@@ -101,9 +106,8 @@ class Dipole(Magnet):
self.__dict__.update({'e2': 0.})
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'MBND'})
def getSettings(self):
return {'Angle':self.design_angle, 'Entrance Edge':self.e1, 'Exit Edge':self.e2}
if not ('branching' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Branch': False})
def getResLength(self):
if (self.angle == 0.):
@@ -135,8 +139,6 @@ class Quadrupole(Magnet):
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'MQUA'})
def getSettings(self):
return {'k1':self.k1}
def writeElement(self, app):
app.writeQuadrupole(self)
@@ -177,6 +179,8 @@ class Solenoid(Magnet):
class Corrector(Magnet):
def __init__(self, prop):
Magnet.__init__(self, prop)
if not ('Tag' in prop): # prop.has_key('Tag')):
self.__dict__.update({'Tag': 'MCOR'})
def writeElement(self, app):
app.writeCorrector(self)
@@ -206,6 +210,10 @@ class Undulator(SimpleContainer):
self.__dict__.update({'Group': 'ID'})
if not ('K' in prop): # prop.has_key('K')):
self.__dict__.update({'K': 0.})
if not ('helical' in prop): # prop.has_key('K')):
self.__dict__.update({'helical': False})
if not ('lambdau' in prop): # prop.has_key('K')):
self.__dict__.update({'lambdau': 0.015})
if not ('ku' in prop): # prop.has_key('ku')):
self.__dict__.update({'ku': 0.})
if not ('kx' in prop): # prop.has_key('kx')):
@@ -219,7 +227,14 @@ class Undulator(SimpleContainer):
if not ('Waist' in prop): # prop.has_key('Waist')):
self.__dict__.update({'Waist': 1.})
def checkElement(self):
if self.helical:
self.kx=0.5
self.ky=0.5
else:
self.kx=0.
self.ky=1.
self.ku = 2*np.pi/self.lambdau
def writeElement(self, app):
app.writeUndulator(self)
@@ -302,6 +317,8 @@ class Alignment(SimpleContainer):
SimpleContainer.__init__(self, prop)
if not ('Group' in prop): # prop.has_key('Group')):
self.__dict__.update({'Group': 'Alignment'})
if not ('Tag' in prop): # prop.has_key('Tag')):
prop.update({'Tag': 'QALI'})
def writeElement(self, app):
app.writeAlignment(self)
@@ -316,6 +333,8 @@ class Marker(SimpleContainer):
SimpleContainer.__init__(self, prop)
if not ('Group' in prop): # prop.has_key('Group')):
self.__dict__.update({'Group': 'Marker'})
if not ('Tag' in prop): # prop.has_key('Tag')):
prop.update({'Tag': 'QREF'})
def writeElement(self, app):
app.writeMarker(self)
+66 -28
View File
@@ -1,5 +1,6 @@
import sys
import json
import pickle
from PyQt5 import QtWidgets, QtCore, QtGui
from ui.BeamlineGUI import Ui_BeamlineGUI
@@ -18,22 +19,62 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
get_properties = self.getProperties,
on_property_changed = self.updateProperties)
# editor = BeamlineEditorWidget(
# on_element_added = fn(element_type: str, node_id: str),
# on_elements_deleted = fn(node_ids: list[str]),
# on_element_selected = fn(node_id: str | None), # None on deselect
# get_properties = fn(node_id: str) -> dict, # called on selection
# on_property_changed = fn(node_id: str, data: dict),
# )
QtWidgets.QVBoxLayout(self.UIEditorBox).addWidget(self.editor)
self.actionSave_Templates.triggered.connect(self.saveTemplates)
self.actionLoad_Templates.triggered.connect(self.loadTemplates)
self.actionSave_as.triggered.connect(self.saveas)
self.actionSave.triggered.connect(self.save)
self.actionLoad.triggered.connect(self.load)
# elements, lines and branch points
self.elementDB={}
self.savefile=''
def saveas(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
fileName, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save Layout",
"Layouts/newSetting.json",
"Json Files (*.json)", options=options)
if not fileName:
return
self.savefile = fileName
self.save()
def save(self):
self.editor.saveLayout("Layouts/lattice.json")
if len(self.savefile) < 5:
self.saveas()
if len(self.savefile) < 5:
return
with (open(self.savefile,'wb') as file):
pickle.dump(self.elementDB, file)
self.editor.saveLayout(self.savefile.replace('.json','_gui.json'))
def load(self):
self.editor.loadLayout("Layouts/lattice.json")
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open Layout",
"Layouts/ReferenceSetting.json",
"Json Files (*.json)", options=options)
if not fileName:
return
self.elementDB.clear()
self.savefile=fileName
with (open(self.savefile,'rb') as file):
self.elementDB = pickle.load(file)
self.savefile=fileName
self.editor.loadLayout(self.savefile.replace('.json','_gui.json'))
def saveTemplates(self):
template=self.editor.get_templates()
@@ -45,16 +86,9 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
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]),
# on_element_selected = fn(node_id: str | None), # None on deselect
# get_properties = fn(node_id: str) -> dict, # called on selection
# on_property_changed = fn(node_id: str, data: dict),
#)
def addElement(self,element_type: str, node_id: str):
if node_id in self.elementDB.keys():
return # this is a repopulation of the database when loading or importing
print("Adding element of type:",element_type)
match element_type:
case "Quadrupole":
@@ -63,6 +97,26 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
ele = Dipole({})
case "Sextupole":
ele = Sextupole({})
case "Corrector":
ele = Corrector({})
case "Solenoid":
ele = Solenoid({})
case "RF":
ele = RF({})
case "Undulator":
ele = Undulator({})
case "Diagnostics":
ele = Diagnostic({})
case "Reference":
ele = Marker({"Tag":"QREF"})
case "Start":
ele = Marker({"Tag":"QBEG","RefID":""})
case "Stop":
ele = Marker({"Tag","QEND"})
case "Target":
ele = Marker({"Tag":"QTAR","Label":"Dump"})
case "Alignment":
ele = Alignment({"Tag":"QALI","height":0.01})
case "Line":
ele = LineContainer()
case _:
@@ -79,8 +133,7 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
if not id in self.elementDB.keys():
return {}
ele = self.elementDB[id]
type = self.editor.getElementType(id)
proplist = getPropertyList(type)
proplist = getPropertyList(self.editor.getElementType(id))
retdict={}
for key in proplist.keys():
path = proplist[key].split('/')
@@ -91,8 +144,6 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
retdict['Name']['ID']=id
return retdict
def updateProperties(self,node_id: str, data: dict):
# make sure that the ID is not changed
data['Name']['ID']=node_id
@@ -123,19 +174,6 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
return
#print(type,elements)
#print(data)
for key in elements.keys():
field = key
prop=elements[key].split('/')
#print(prop)
if prop[0] in data.keys() and prop[1] in data[prop[0]].keys():
value = data[prop[0]][prop[1]]
print('Updating:',field,prop[0],prop[1],value)
setattr(self.elementDB[node_id], field, value)
print(type)
if __name__ == '__main__':
+34 -3
View File
@@ -1,16 +1,19 @@
from elements import Corrector
# Editor Names
Name = ['Name','Tag','Index']
Position=['Offset','Length','Reserved Length','Reference Position','Displacement in X','Displacement in Y','Tilt']
Position=['Offset','Relative Offset', 'Length','Reserved Length','Reference Position','Displacement in X','Displacement in Y','Tilt']
Type = ['Group','Baugruppe']
# element class attributes
eName = ['Name','Tag','index']
ePosition = ['OffsetS','Length','LengthRes','sRef','DisplacementX','DisplacementY','Tilt']
ePosition = ['OffsetS','relOff','Length','LengthRes','sRef','DisplacementX','DisplacementY','Tilt']
eType = ['Group','Baugruppe']
def getPropertyList(element_type: str) -> dict:
def getPropertyList(element_type: str | None) -> dict:
if element_type is None:
return {}
if element_type == 'Line':
return {'Name':'Name/Name','LengthEnding':'Position/Reserved Length','firstElementID':'Anchor/Reference'}
fields={}
@@ -23,12 +26,40 @@ def getPropertyList(element_type: str) -> dict:
match element_type:
case "Quadrupole":
fields['PSChannels'] = 'Type/PS Channels'
fields['k1']='Setting/k1'
case "Dipole":
fields['PSChannels'] = 'Type/PS Channels'
fields['design_angle']='Setting/Angle'
fields['branching'] = 'Setting/Branching Point'
fields['e1']='Setting/Entrance Angle'
fields['e2'] = 'Setting/Exit Angle'
case "Sextupole":
fields['PSChannels'] = 'Type/PS Channels'
fields['k2'] = 'Setting/k2'
case "Solenoid":
fields['PSChannels'] = 'Type/PS Channels'
fields['ks'] = 'Setting/ks'
case "Corrector":
fields['PSChannels'] = 'Type/PS Channels'
case "RF":
fields['Gradient'] = 'Setting/Gradient'
fields['Phase'] = 'Setting/Phase'
fields['Band'] = 'Setting/Band'
case "Undulator":
fields['K'] = 'Setting/K'
fields['lambdau'] = 'Setting/Undulator Period'
fields['helical'] = 'Setting/Helical'
case "Diagnostic":
fields['Seval'] = 'Position/Evaluation Point'
case "Start":
fields['RefID'] = 'Setting/Reference Element ID'
case "Target":
fields['Label'] = 'Name/Destination Name'
case "Alignment":
fields['height'] = "Settings/Height"
return fields
+14 -3
View File
@@ -15,6 +15,10 @@ class Ui_BeamlineGUI(object):
def setupUi(self, BeamlineGUI):
BeamlineGUI.setObjectName("BeamlineGUI")
BeamlineGUI.resize(1491, 641)
BeamlineGUI.setStyleSheet("font: 10pt \"Segoe UI\";\n"
"\n"
"color: rgb(79, 142, 247);\n"
"background-color: rgb(13, 15, 26);")
self.centralwidget = QtWidgets.QWidget(BeamlineGUI)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
@@ -24,9 +28,16 @@ class Ui_BeamlineGUI(object):
self.horizontalLayout.addWidget(self.UIEditorBox)
BeamlineGUI.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(BeamlineGUI)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1491, 22))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1491, 24))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
font = QtGui.QFont()
font.setFamily("Segoe UI")
font.setPointSize(10)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.menuFile.setFont(font)
self.menuFile.setObjectName("menuFile")
BeamlineGUI.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(BeamlineGUI)
@@ -58,13 +69,13 @@ class Ui_BeamlineGUI(object):
self.menubar.addAction(self.menuFile.menuAction())
self.retranslateUi(BeamlineGUI)
self.actionQuit.triggered.connect(self.centralwidget.close) # type: ignore
self.actionQuit.triggered.connect(BeamlineGUI.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.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"))
+17 -2
View File
@@ -13,6 +13,12 @@
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="styleSheet">
<string notr="true">font: 10pt &quot;Segoe UI&quot;;
color: rgb(79, 142, 247);
background-color: rgb(13, 15, 26);</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
@@ -26,12 +32,21 @@
<x>0</x>
<y>0</y>
<width>1491</width>
<height>22</height>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="font">
<font>
<family>Segoe UI</family>
<pointsize>10</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="title">
<string>File</string>
<string>FILE</string>
</property>
<addaction name="actionNew_Layout"/>
<addaction name="actionLoad"/>