Skip to content

Commit 8b138e9

Browse files
authored
Merge pull request #12 from JPTS2/0.1.9
0.1.9
2 parents b4b62f3 + 29b4803 commit 8b138e9

20 files changed

+436
-24
lines changed

sad2xs/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ class Config:
148148
default_factory = lambda: {
149149
"Marker", "Drift",
150150
"Bend", "Quadrupole", "Sextupole", "Octupole", "Multipole", "Solenoid",
151-
"Cavity", "XYShift", "ZetaShift", "XRotation", "YRotation", "SRotation"})
151+
"Cavity", "XYShift", "ZetaShift", "XRotation", "YRotation", "SRotation",
152+
"LimitEllipse", "LimitRect"})
152153

153154
########################################
154155
# Marker Insertion Tolerance

sad2xs/converter/_004_element_converter.py

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
=============================================
44
Author(s): John P T Salvesen
55
6-
Date: 09-10-2025
6+
Date: 18-11-2025
77
"""
88

99
################################################################################
@@ -1300,25 +1300,132 @@ def convert_apertures(parsed_elements, environment):
13001300
########################################
13011301
# Initialise parameters
13021302
########################################
1303-
a = 1.0
1304-
b = 1.0
1303+
offset_x = 0.0
1304+
offset_y = 0.0
1305+
a = None
1306+
b = None
1307+
dx1 = None
1308+
dx2 = None
1309+
dy1 = None
1310+
dy2 = None
1311+
aper_type = None
13051312

13061313
########################################
13071314
# Read values
13081315
########################################
1316+
if 'dx' in ele_vars:
1317+
offset_x = parse_expression(ele_vars['dx'])
1318+
if 'dy' in ele_vars:
1319+
offset_y = parse_expression(ele_vars['dy'])
13091320
if 'ax' in ele_vars:
13101321
a = parse_expression(ele_vars['ax'])
13111322
if 'ay' in ele_vars:
13121323
b = parse_expression(ele_vars['ay'])
1324+
if "dx1" in ele_vars:
1325+
dx1 = parse_expression(ele_vars['dx1'])
1326+
if "dx2" in ele_vars:
1327+
dx2 = parse_expression(ele_vars['dx2'])
1328+
if "dy1" in ele_vars:
1329+
dy1 = parse_expression(ele_vars['dy1'])
1330+
if "dy2" in ele_vars:
1331+
dy2 = parse_expression(ele_vars['dy2'])
1332+
1333+
########################################
1334+
# Determine type of aperture
1335+
########################################
1336+
if any(v is not None for v in [dx1, dx2, dy1, dy2]) and \
1337+
any(v is not None for v in [a, b]):
1338+
raise ValueError(
1339+
f"Error! Aperture {ele_name} has both rectangular and elliptical definitions. This is not supported.")
1340+
elif any(v is not None for v in [dx1, dx2, dy1, dy2]):
1341+
aper_type = 'LimitRect'
1342+
1343+
if dx1 is None and dx2 is None:
1344+
dx1 = -1.0
1345+
dx2 = +1.0
1346+
elif dx1 is None and isinstance(dx2, float):
1347+
if dx2 < 0:
1348+
dx1 = dx2
1349+
dx2 = +1.0
1350+
else:
1351+
dx1 = -1.0
1352+
elif dx2 is None and isinstance(dx1, float):
1353+
if dx1 < 0:
1354+
dx2 = +1.0
1355+
else:
1356+
dx2 = dx1
1357+
dx1 = -1.0
1358+
elif isinstance(dx1, float) and isinstance(dx2, float):
1359+
if dx1 > dx2:
1360+
tmp = dx1
1361+
dx1 = dx2
1362+
dx2 = tmp
1363+
else:
1364+
# At least one is expression, cannot compare
1365+
# This might cause issues
1366+
pass
1367+
1368+
if dy1 is None and dy2 is None:
1369+
dy1 = -1.0
1370+
dy2 = +1.0
1371+
elif dy1 is None and isinstance(dy2, float):
1372+
if dy2 < 0:
1373+
dy1 = dy2
1374+
dy2 = +1.0
1375+
else:
1376+
dy1 = -1.0
1377+
elif dy2 is None and isinstance(dy1, float):
1378+
if dy1 < 0:
1379+
dy2 = +1.0
1380+
else:
1381+
dy2 = dy1
1382+
dy1 = -1.0
1383+
elif isinstance(dy1, float) and isinstance(dy2, float):
1384+
if dy1 > dy2:
1385+
tmp = dy1
1386+
dy1 = dy2
1387+
dy2 = tmp
1388+
else:
1389+
# At least one is expression, cannot compare
1390+
# This might cause issues
1391+
pass
1392+
1393+
1394+
elif any(v is not None for v in [a, b]):
1395+
aper_type = 'LimitEllipse'
1396+
1397+
if a is None:
1398+
a = 1.0
1399+
if b is None:
1400+
b = 1.0
1401+
1402+
else:
1403+
raise ValueError(f"Error! Aperture {ele_name} has no valid definition.")
13131404

13141405
########################################
13151406
# Create Element
13161407
########################################
1317-
environment.new(
1318-
name = ele_name,
1319-
parent = xt.LimitEllipse,
1320-
a = a,
1321-
b = b)
1408+
if aper_type == 'LimitRect':
1409+
environment.new(
1410+
name = ele_name,
1411+
parent = xt.LimitRect,
1412+
min_x = dx1,
1413+
max_x = dx2,
1414+
min_y = dy1,
1415+
max_y = dy2,
1416+
shift_x = offset_x,
1417+
shift_y = offset_y)
1418+
continue
1419+
elif aper_type == 'LimitEllipse':
1420+
environment.new(
1421+
name = ele_name,
1422+
parent = xt.LimitEllipse,
1423+
a = a,
1424+
b = b,
1425+
shift_x = offset_x,
1426+
shift_y = offset_y)
1427+
else:
1428+
raise ValueError(f"Error! Aperture {ele_name} has unsupported definition.")
13221429
continue
13231430

13241431
################################################################################

sad2xs/converter/_005_line_converter.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,24 @@ def convert_lines(
162162
# Line and not from the importer: generated line
163163
# This is done to handle solenoids, ref shifts, thick cavities etc
164164
if '-' in component \
165-
and component[1:] not in parsed_lines\
165+
and component[1:] not in parsed_lines \
166166
and component[1:] in environment.lines:
167-
167+
# Checks for:
168+
# - negative sign
169+
# - The line is generated, not imported (parsed lines)
170+
# - The line exists in the environment (to be reversed)
171+
168172
reversed_line_name = component[1:] + '_reversed'
173+
174+
# Check if the line hasn't already been reversed (duplicate element)
175+
if reversed_line_name in environment.lines:
176+
components[i] = reversed_line_name
177+
continue
178+
169179
reversed_line_elements = environment.lines[component[1:]].element_names
170180

171181
# If it is a generated subline, do not reverse the order of the elements
172-
173-
# Negate the individual elements
182+
# Just negate the individual elements
174183
reversed_line_elements = [f'-{elem}' for elem in reversed_line_elements]
175184

176185
reverse_handled_components = []

sad2xs/converter/_010_write_lattice.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@
2424
from ..output_writer._008_sol import create_solenoid_lattice_file_information
2525
from ..output_writer._009_cavity import create_cavity_lattice_file_information
2626
from ..output_writer._010_refshift import create_refshift_lattice_file_information
27-
from ..output_writer._011_marker import create_marker_lattice_file_information
28-
from ..output_writer._012_line import create_line_lattice_file_information
29-
from ..output_writer._013_model import create_model_lattice_file_information
30-
from ..output_writer._014_offset_markers import create_offset_marker_lattice_file_information
27+
from ..output_writer._011_aperture import create_aperture_lattice_file_information
28+
from ..output_writer._012_marker import create_marker_lattice_file_information
29+
from ..output_writer._013_line import create_line_lattice_file_information
30+
from ..output_writer._014_model import create_model_lattice_file_information
31+
from ..output_writer._015_offset_markers import create_offset_marker_lattice_file_information
3132

3233
today = date.today()
3334

@@ -131,6 +132,24 @@ def write_lattice(
131132
########################################
132133
line_table = line.get_table(attr = True)
133134

135+
########################################
136+
# Prepare for removal of - signs where not needed
137+
########################################
138+
element_names = line_table.name
139+
140+
minus_elements = line_table.rows["-.*"].name
141+
for minus_element in minus_elements:
142+
root_name = minus_element.split("::")[0][1:]
143+
plus_eles = [name.startswith(root_name) for name in element_names]
144+
145+
if any(plus_eles):
146+
plus_name = element_names[plus_eles][0]
147+
type_minus = line_table["element_type", minus_element]
148+
type_plus = line_table["element_type", plus_name]
149+
150+
assert type_minus == type_plus, \
151+
f"Element types for element and its negative do not match"
152+
134153
########################################
135154
# Drifts
136155
########################################
@@ -210,6 +229,14 @@ def write_lattice(
210229
line = line,
211230
line_table = line_table,
212231
config = config)
232+
233+
########################################
234+
# Apertures
235+
########################################
236+
lattice_file_string += create_aperture_lattice_file_information(
237+
line = line,
238+
line_table = line_table,
239+
config = config)
213240

214241
########################################
215242
# Markers

sad2xs/main.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def convert_sad_to_xsuite(
4040
reverse_element_order: bool = False,
4141
reverse_bend_direction: bool = False,
4242
reverse_charge: bool = False,
43+
install_apertures_as_markers: bool = False,
4344
**kwargs):
4445

4546
############################################################################
@@ -74,6 +75,25 @@ def convert_sad_to_xsuite(
7475
parsed_lattice_data = parsed_lattice_data,
7576
excluded_elements = excluded_elements,
7677
config = config)
78+
79+
############################################################################
80+
# Check if apertures should become markers
81+
############################################################################
82+
if install_apertures_as_markers:
83+
if config._verbose:
84+
print_section_heading("Converting apertures to markers", mode = 'section')
85+
86+
if "apert" in parsed_lattice_data['elements']:
87+
if "mark" in parsed_lattice_data['elements']:
88+
merged = {
89+
**parsed_lattice_data['elements']["apert"],
90+
**parsed_lattice_data['elements']["mark"]} # Mark takes precedence
91+
parsed_lattice_data['elements']["mark"] = merged
92+
else:
93+
parsed_lattice_data['elements']["mark"] = \
94+
parsed_lattice_data['elements']["apert"]
95+
96+
parsed_lattice_data['elements'].pop("apert")
7797

7898
############################################################################
7999
# Build Environment

sad2xs/output_writer/_002_bend.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ def create_bend_lattice_file_information(
2727
########################################
2828
# Get information
2929
########################################
30-
hbends, vbends, sbends, unique_bend_variables, bend_name_dict = extract_bend_information(line, line_table)
30+
hbends, vbends, sbends, unique_bend_variables, bend_name_dict = \
31+
extract_bend_information(line, line_table)
3132

3233
hbend_lengths = np.array(sorted(hbends.keys()))
3334
hbend_names = generate_magnet_for_replication_names(hbends, "hbend")
@@ -85,6 +86,12 @@ def create_bend_lattice_file_information(
8586
for replica_name in hbends[hbend_length]:
8687
replica_variable = bend_name_dict[replica_name]
8788

89+
# Remove the minus sign if no non minus version exists
90+
if replica_name.startswith("-"):
91+
root_name = replica_name[1:]
92+
if root_name not in hbends[hbend_length]:
93+
replica_name = root_name
94+
8895
# If simple try to make it more compact
8996
if check_is_simple_bend_corr(line, replica_name):
9097
bend_generation = f"""
@@ -129,6 +136,12 @@ def create_bend_lattice_file_information(
129136
for replica_name in vbends[vbend_length]:
130137
replica_variable = bend_name_dict[replica_name]
131138

139+
# Remove the minus sign if no non minus version exists
140+
if replica_name.startswith("-"):
141+
root_name = replica_name[1:]
142+
if root_name not in vbends[vbend_length]:
143+
replica_name = root_name
144+
132145
# If simple try to make it more compact
133146
if check_is_simple_bend_corr(line, replica_name):
134147
bend_generation = f"""
@@ -172,6 +185,12 @@ def create_bend_lattice_file_information(
172185
for replica_name in sbends[sbend_length]:
173186
replica_variable = bend_name_dict[replica_name]
174187

188+
# Remove the minus sign if no non minus version exists
189+
if replica_name.startswith("-"):
190+
root_name = replica_name[1:]
191+
if root_name not in sbends[sbend_length]:
192+
replica_name = root_name
193+
175194
# If simple try to make it more compact
176195
if check_is_simple_bend_corr(line, replica_name):
177196
bend_generation = f"""

sad2xs/output_writer/_003_corr.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ def create_corrector_lattice_file_information(
8686
for replica_name in hcorrs[hcorr_length]:
8787
replica_variable = corr_name_dict[replica_name]
8888

89+
# Remove the minus sign if no non minus version exists
90+
if replica_name.startswith("-"):
91+
root_name = replica_name[1:]
92+
if root_name not in hcorrs[hcorr_length]:
93+
replica_name = root_name
94+
8995
# If simple try to make it more compact
9096
if check_is_simple_bend_corr(line, replica_name):
9197
corr_generation = f"""
@@ -129,6 +135,12 @@ def create_corrector_lattice_file_information(
129135
for replica_name in vcorrs[vcorr_length]:
130136
replica_variable = corr_name_dict[replica_name]
131137

138+
# Remove the minus sign if no non minus version exists
139+
if replica_name.startswith("-"):
140+
root_name = replica_name[1:]
141+
if root_name not in vcorrs[vcorr_length]:
142+
replica_name = root_name
143+
132144
# If simple try to make it more compact
133145
if check_is_simple_bend_corr(line, replica_name):
134146
corr_generation = f"""
@@ -171,6 +183,12 @@ def create_corrector_lattice_file_information(
171183
for replica_name in scorrs[scorr_length]:
172184
replica_variable = corr_name_dict[replica_name]
173185

186+
# Remove the minus sign if no non minus version exists
187+
if replica_name.startswith("-"):
188+
root_name = replica_name[1:]
189+
if root_name not in scorrs[scorr_length]:
190+
replica_name = root_name
191+
174192
# If simple try to make it more compact
175193
if check_is_simple_bend_corr(line, replica_name):
176194
corr_generation = f"""

sad2xs/output_writer/_004_quad.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ def create_quadrupole_lattice_file_information(
7575
for quad, quad_length in zip(quad_names, quad_lengths):
7676
for replica_name in quads[quad_length]:
7777

78+
# Remove the minus sign if no non minus version exists
79+
if replica_name.startswith("-"):
80+
root_name = replica_name[1:]
81+
if root_name not in quads[quad_length]:
82+
replica_name = root_name
83+
7884
if check_is_simple_quad_sext_oct(line, replica_name, "Quadrupole"):
7985

8086
if not check_is_skew_quad_sext_oct(line, replica_name, "Quadrupole"):

sad2xs/output_writer/_005_sext.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ def create_sextupole_lattice_file_information(
7575
for sext, sext_length in zip(sext_names, sext_lengths):
7676
for replica_name in sexts[sext_length]:
7777

78+
# Remove the minus sign if no non minus version exists
79+
if replica_name.startswith("-"):
80+
root_name = replica_name[1:]
81+
if root_name not in sexts[sext_length]:
82+
replica_name = root_name
83+
7884
if check_is_simple_quad_sext_oct(line, replica_name, "Sextupole"):
7985

8086
if not check_is_skew_quad_sext_oct(line, replica_name, "Sextupole"):

0 commit comments

Comments
 (0)