Building up the lattice

This commit is contained in:
2026-07-16 17:21:38 +02:00
parent 1c8e74d6a6
commit 0b23aedbd7
4 changed files with 303 additions and 20 deletions
+125
View File
@@ -359,6 +359,23 @@ class BeamlineScene(QGraphicsScene):
none_act.setEnabled(False)
tmpl_menu.addAction(none_act)
# Align / Distribute — shown when 2+ nodes are selected
selected_nodes = [i for i in self.selectedItems()
if isinstance(i, BeamlineNode)]
if len(selected_nodes) >= 2:
menu.addSeparator()
align_menu = menu.addMenu("Align && Distribute")
align_menu.setStyleSheet(menu.styleSheet())
for label, slot in (
("Align Horizontally", lambda: self._align(True)),
("Align Vertically", lambda: self._align(False)),
("Distribute Horizontally", lambda: self._distribute(True)),
("Distribute Vertically", lambda: self._distribute(False)),
):
act = QAction(label, align_menu)
act.triggered.connect(slot)
align_menu.addAction(act)
menu.exec_(screen_pos)
def _ctx_copy(self, node: BeamlineNode):
@@ -503,8 +520,116 @@ class BeamlineScene(QGraphicsScene):
act_paste.setEnabled(bool(self._clipboard))
act_paste.triggered.connect(self.paste)
menu.addAction(act_paste)
# Align / Distribute — only when 2+ nodes are selected
selected_nodes = [i for i in self.selectedItems()
if isinstance(i, BeamlineNode)]
if len(selected_nodes) >= 2:
menu.addSeparator()
align_menu = menu.addMenu("Align && Distribute")
align_menu.setStyleSheet(menu.styleSheet())
for label, slot in (
("Align Horizontally", lambda: self._align(True)),
("Align Vertically", lambda: self._align(False)),
("Distribute Horizontally", lambda: self._distribute(True)),
("Distribute Vertically", lambda: self._distribute(False)),
):
act = QAction(label, align_menu)
act.triggered.connect(slot)
align_menu.addAction(act)
menu.exec_(screen_pos)
# ── Align / Distribute helpers ────────────────────────────────────────────
def _selected_nodes(self) -> list[BeamlineNode]:
return [i for i in self.selectedItems() if isinstance(i, BeamlineNode)]
def _node_center(self, node: BeamlineNode) -> QPointF:
"""Scene-coordinate centre of a node."""
return node.pos() + QPointF(node.W / 2, node.H / 2)
def _align(self, horizontal: bool):
"""
Align all selected nodes so their centres share a common axis.
horizontal=True → align to the mean Y (same horizontal line)
horizontal=False → align to the mean X (same vertical line)
"""
nodes = self._selected_nodes()
if len(nodes) < 2:
return
centers = [self._node_center(n) for n in nodes]
if horizontal:
mean_y = sum(c.y() for c in centers) / len(centers)
for node in nodes:
cx = self._node_center(node).x()
node.setPos(cx - node.W / 2, mean_y - node.H / 2)
self.status_changed.emit(
f"Aligned {len(nodes)} elements horizontally."
)
else:
mean_x = sum(c.x() for c in centers) / len(centers)
for node in nodes:
cy = self._node_center(node).y()
node.setPos(mean_x - node.W / 2, cy - node.H / 2)
self.status_changed.emit(
f"Aligned {len(nodes)} elements vertically."
)
# Update all connections attached to moved nodes
for node in nodes:
for port in node._all_ports():
for conn in port.connections:
conn.update_path()
def _distribute(self, horizontal: bool):
"""
Space all selected nodes evenly along one axis so their centres
are equidistant.
horizontal=True → distribute along X axis
horizontal=False → distribute along Y axis
"""
nodes = self._selected_nodes()
if len(nodes) < 3:
self.status_changed.emit(
"Select at least 3 elements to distribute."
)
return
if horizontal:
nodes_sorted = sorted(nodes, key=lambda n: self._node_center(n).x())
x_min = self._node_center(nodes_sorted[0]).x()
x_max = self._node_center(nodes_sorted[-1]).x()
step = (x_max - x_min) / (len(nodes_sorted) - 1)
for i, node in enumerate(nodes_sorted):
cx = x_min + i * step
cy = self._node_center(node).y()
node.setPos(cx - node.W / 2, cy - node.H / 2)
self.status_changed.emit(
f"Distributed {len(nodes)} elements horizontally."
)
else:
nodes_sorted = sorted(nodes, key=lambda n: self._node_center(n).y())
y_min = self._node_center(nodes_sorted[0]).y()
y_max = self._node_center(nodes_sorted[-1]).y()
step = (y_max - y_min) / (len(nodes_sorted) - 1)
for i, node in enumerate(nodes_sorted):
cx = self._node_center(node).x()
cy = y_min + i * step
node.setPos(cx - node.W / 2, cy - node.H / 2)
self.status_changed.emit(
f"Distributed {len(nodes)} elements vertically."
)
for node in nodes:
for port in node._all_ports():
for conn in port.connections:
conn.update_path()
def _ctx_save_template(self, node: BeamlineNode):
props = None
if self._get_properties:
+48 -18
View File
@@ -52,8 +52,11 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
self.elementDB={}
self.savefile=''
self.lines={}
self.branchingID={}
self.branchingID=[]
self.branching={}
self.branchingTemp={}
self.branchnames={}
self.targetReached=False
self.PL=ProtoListe(0)
self.loadFile('Layouts/SFTest2.json')
@@ -235,7 +238,7 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
retdict['Position']['Reserved Length'] = float(retdict['Position']['Reserved Length'])
return retdict
def updateProperties(self,node_id: str, data: dict):
def updateProperties(self,node_id: str, data: dict):
# make sure that the ID is not changed
data['Name']['ID']=node_id
type = self.editor.getElementType(node_id)
@@ -275,17 +278,23 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
self.branchingID.clear()
for key in self.elementDB.keys():
ele = self.elementDB[key]
if isinstance(ele,Dipole) and ele.branching:
self.branchingID[key] = False
self.branchingID.append(key)
print('Branching elements:')
for key in self.branchingID.keys():
print('ID:',key)
for key in self.branchingID:
print(' ID:',key)
def flattenSingleLine(self,node_id,branch=0,case_num=1):
for key in self.branchingID.keys():
self.branchingID[key]=(branch == 1)
self.targetReached=False
self.currentBranch=''
self.branching.clear()
for ib, id in enumerate(self.branchingID):
ibit = 2**ib
if branch & ibit > 0:
self.branching[id]=True
else:
self.branching[id]=False
type = self.editor.getElementType(node_id)
name = 'XXX'
if type == 'Line':
@@ -295,7 +304,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
name = self.elementDB[node_id].Name
line = LineContainer(namein=name, Lin=0)
line.clear()
print('\nUnwrapping beamline...\n')
if self.verbose:
print('\nUnwrapping beamline...\n')
self.branchingTemp.clear()
self.unwrap(name, node_id, line, case_num)
for key in self.branchingTemp.keys():
@@ -305,15 +315,22 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
def flatten(self, node_id, case_num=1 ):
print('Flatten beamline for case:', case_num)
self.findBranchingDipoles()
nbranch = len(self.branchingID.keys())
nbranch = len(self.branchingID)
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))
# for ib in range(2**nbranch):
for ib in range(0,4):
line=self.flattenSingleLine(node_id,branch=ib,case_num=case_num)
if self.currentBranch=='':
self.currentBranch='%d' % ib
if not self.currentBranch in self.lines.keys():
self.lines[self.currentBranch]=copy.deepcopy(line)
else:
print('Skipping redundant line:',self.currentBranch)
print('Writing Layout for the lines:')
for key in self.lines.keys():
print(' ',key)
self.PL.generateLayout(self.lines)
self.plrecs = [self.PL.info[ele] for ele in self.PL.order if len(self.PL.info[ele]['Prefix'])> 5]
populate_table(self.UIProtoList,self.plrecs)
@@ -328,6 +345,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
tree=self.editor.flatten(node_id,case_num)
secondBranch = False
for ele in tree.path:
if self.targetReached:
return
self.report(ele, name)
type = self.editor.getElementType(ele)
if type == 'Line':
@@ -340,6 +359,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
ref = subline.firstElementID
if ref in self.elementDB.keys():
secondBranch = self.unwrap(name + name1, ref, subline,case_num)
if secondBranch:
break # do not continue with any other elements
else:
if self.verbose:
print('Undefined reference in Line Container:', name + name1)
@@ -352,6 +373,12 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
line.append(self.elementDB[ele],sRef=0,Ref='relative')
elif type == 'Case' or type == 'Merge':
continue
elif type == 'Target':
target=self.elementDB[ele].Name
if not target in self.lines.keys():
print('New Target reached:',self.elementDB[ele].Name)
self.targetReached=True
self.currentBranch=self.elementDB[ele].Name
else:
sRef=self.elementDB[ele].OffsetS
Ref='absolute'
@@ -359,8 +386,9 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
Ref='relative'
line.append(self.elementDB[ele],sRef=sRef,Ref=Ref)
self.elementDB[ele].Prefix=name
if ele in self.branchingID.keys():
if self.branchingID[ele] is True:
if ele in self.branching.keys():
if self.branching[ele] is True:
print('Branching at element:', name+'-'+self.elementDB[ele].Name,'with angle:',self.elementDB[ele].design_angle)
self.elementDB[ele].angle=self.elementDB[ele].design_angle
line.hasBranchPoint=True
return True
@@ -370,10 +398,12 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
if secondBranch:
if tree.secondary:
self.unwrap(name, tree.secondary.node_id, line, case_num)
else:
return secondBranch
else:
if tree.primary:
self.unwrap(name,tree.primary.node_id,line,case_num)
return False
return secondBranch
+33 -1
View File
@@ -24,7 +24,6 @@ class ProtoListe(ApplicationTemplate):
self.dipole.clear()
self.setRef(-0.1, -0.1)
#self.iterateLine(line)
keys=['1']
for key in lines.keys():
print('Writing Line for:',key,lines[key].Name)
self.setRef(-0.1, -0.1)
@@ -258,6 +257,37 @@ class ProtoListe(ApplicationTemplate):
def writeMarker(self,ele):
if not ele.Name in self.info.keys() and len(ele.Name) > 4:
name = ele.Prefix + '-' + ele.Name
domain = self.getDomain(name)
align = 'Entrance'
angh = np.arctan2(self.ev[0], self.ev[2]) * 180 / np.pi
rh = np.sqrt(self.ev[2] * self.ev[2] + self.ev[0] * self.ev[0])
angv = np.arctan2(self.ev[1], rh) * 180 / np.pi
if ele.Group == 'Start':
group = 'Section'
bg ='Marker'
prefix= ele.Prefix
suffix=''
index=''
elif ele.Group == 'Marker' and 'MREF' in ele.Name:
group = 'Marker'
bg = 'Reference Point'
prefix=ele.Prefix
suffix = 'MREF'
index=ele.index
else:
return
info = {'Domain': domain, 'Prefix': prefix, 'Suffix': suffix, 'Index': index,
's (m)': self.s, 'z (m)': self.z, 'x (m)': self.x, 'y (m)': self.y, 'L (m)': 0,
'Reference': 'Start', 'Group': group, 'Baugruppe': bg, 'Variante': 'A', 'Roll': 0,
'PS-Ch.': 0, 'P1': self.p1, 'PV': self.p1, 'P2': self.p1, 'EV1': self.ev1,
'EV2': self.ev1, 'Yaw': angh, 'Pitch': angv, 'Alignment': align}
self.info[ele.Name] = info
self.order.append(ele.Name)
return
if ele.Group == 'Marker':
if 'MREF' in ele.Name:
self.write((ele.Name, ele.LengthRes, 'Start', ele.Group, ele.Tag, ele.Baugruppe, 0, 0))
@@ -302,6 +332,8 @@ class ProtoListe(ApplicationTemplate):
channels=0
if 'PSChannels' in ele.__dict__:
channels=ele['PSChannels']
if ele.Baugruppe=='U15':
channels=channels+1
if ele.Baugruppe=='QFU':
channels=channels-1
if ele.Baugruppe=='QFUE':
+97 -1
View File
@@ -24,7 +24,7 @@ background-color: rgb(13, 15, 26);</string>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
<number>2</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
@@ -46,6 +46,102 @@ background-color: rgb(13, 15, 26);</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>Settings</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="UIFlattenOffsetFlag">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Offset for Flatten</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="UIFlattenOffset">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>-0.1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="lineEdit_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Tolerance</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="UITolerances">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>1e-5</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1944</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>