Skip to content

Commit a0b96f5

Browse files
committed
Addition of geometry macro converter for Virtual Segmentation
1 parent b760feb commit a0b96f5

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import re
2+
import sys
3+
import math
4+
import argparse
5+
6+
def extract_virtual_segmentation(file_path):
7+
"""
8+
Reads the digitizer file and extracts target pitch values.
9+
Expects lines with names like "pitchX", "pitchY", or "pitchZ" followed by a number and unit.
10+
Returns a dictionary mapping axis letters ("X", "Y", "Z") to a value in mm.
11+
"""
12+
segmentation = {}
13+
unit_conversion = {"mm": 1.0, "cm": 10.0}
14+
with open(file_path, 'r') as f:
15+
for line in f:
16+
m = re.search(r'pitch([XYZ])\s+(\d+\.?\d*)\s*(mm|cm)?', line, re.IGNORECASE)
17+
if m:
18+
axis = m.group(1).upper()
19+
size = float(m.group(2))
20+
unit = m.group(3) if m.group(3) else "mm"
21+
size *= unit_conversion.get(unit.lower(), 1.0)
22+
segmentation[axis] = size
23+
print(f"Extracted digitizer pitch for axis {axis}: {size} mm")
24+
return segmentation
25+
26+
def calculate_pitch(crystal_size, target_pitch):
27+
"""
28+
Computes the best pitch for a given crystal size and target pitch.
29+
Iterates over possible repeat numbers and accepts only divisions with zero remainder (within tolerance).
30+
Returns a tuple (best_pitch, repeat_number).
31+
"""
32+
best_pitch = 0
33+
min_diff = 999
34+
num_pitches = 1
35+
tol = 1e-20
36+
while True:
37+
pitch = crystal_size / num_pitches
38+
if pitch <= target_pitch and math.fmod(crystal_size, pitch) < tol:
39+
diff = abs(pitch - target_pitch)
40+
if diff <= min_diff:
41+
min_diff = diff
42+
best_pitch = pitch
43+
if best_pitch != 0 and best_pitch <= target_pitch:
44+
break
45+
elif num_pitches > 1000:
46+
print(f"ERROR! No pitch found in 1000 iterations! Size = {crystal_size} and desired pitch = {pitch}")
47+
break
48+
num_pitches += 1
49+
repeat_number = int(round(crystal_size / best_pitch)) if best_pitch != 0 else 0
50+
return best_pitch, repeat_number
51+
52+
def parse_arguments():
53+
"""
54+
Parses command line arguments for the geometry processing script.
55+
56+
Expected usage (example):
57+
python script.py -c XYZ -d digitizer.mac -i geometry_input.mac -o geometry_output.mac
58+
59+
Short flags:
60+
-crp Column Row PseudoCrystal (CRP) division string (e.g., XYZ)
61+
-d, --digitizer Path to digitizer.mac file
62+
-i, --input Path to input geometry file
63+
-o, --output Path to output geometry file
64+
"""
65+
# Create the ArgumentParser object with a description.
66+
parser = argparse.ArgumentParser(description="Process geometry file with segmentation parameters.")
67+
68+
# Add the -crp argument with short flag -c. This is required.
69+
parser.add_argument("-crp", required=True,
70+
help="Column Row Layer (CRP) division string (e.g., XYZ) specifying the segmentation mapping.")
71+
72+
# Add the --digitizer argument with short flag -d. This is required.
73+
parser.add_argument("-d", "--digitizer", required=True,
74+
help="Path to the digitizer.mac file containing segmentation parameters.")
75+
76+
# Add the --input argument with short flag -i. This is required.
77+
parser.add_argument("-i", "--input", required=True,
78+
help="Path to the input geometry file.")
79+
80+
# Add the --output argument with short flag -o. This is required.
81+
parser.add_argument("-o", "--output", required=True,
82+
help="Path to the output geometry file.")
83+
84+
# Parse the command line arguments into a Namespace object.
85+
args = parser.parse_args()
86+
87+
# Return the parsed values. They can be accessed via args.crp, args.digitizer, etc.
88+
return args.crp, args.digitizer, args.input, args.output
89+
90+
91+
def build_crp_mapping(crp_str):
92+
"""
93+
Builds a mapping for element -> assigned segmentation axis.
94+
Expected: first character for column, second for row, third for layer/pseudoCrystal (if present, else None or '0').
95+
"""
96+
mapping = {}
97+
if len(crp_str) < 1:
98+
print("Error: crp_str must have at least 1 characters for column.")
99+
sys.exit(1)
100+
mapping['column'] = None if crp_str[0] == '0' else crp_str[0].upper()
101+
102+
if len(crp_str) >= 2:
103+
mapping['row'] = None if crp_str[1] == '0' else crp_str[1].upper()
104+
if len(crp_str) >= 3:
105+
mapping['pseudoCrystal'] = None if crp_str[2] == '0' else crp_str[2].upper()
106+
else:
107+
mapping['pseudoCrystal'] = None
108+
else:
109+
mapping['row'] = None
110+
mapping['pseudoCrystal'] = None
111+
print(f"Using crp mapping: {mapping}")
112+
return mapping
113+
114+
def insert_repeater_lines(crp_mapping, global_update):
115+
"""
116+
For each system (column, row, layer/pseudoCrystal) in the CRP mapping, builds the repeater lines.
117+
The repeater lines use the repeat number and the computed best pitch for the system’s assigned axis.
118+
The repeat vector is built so that only the component corresponding to the assigned axis is nonzero.
119+
Returns a list of repeater lines.
120+
"""
121+
repeater_lines = []
122+
for system in crp_mapping:
123+
assigned_axis = crp_mapping[system]
124+
if assigned_axis is None:
125+
continue
126+
if assigned_axis in global_update:
127+
best_pitch = global_update[assigned_axis][1]
128+
system_repeat = global_update[assigned_axis][2]
129+
else:
130+
best_pitch = 0
131+
system_repeat = 0
132+
# Build repeat vector: only the component corresponding to assigned_axis is nonzero.
133+
vector = {"X": "0.0", "Y": "0.0", "Z": "0.0"}
134+
if assigned_axis == "X":
135+
vector["X"] = f"{best_pitch:.6f}"
136+
elif assigned_axis == "Y":
137+
vector["Y"] = f"{best_pitch:.6f}"
138+
elif assigned_axis == "Z":
139+
vector["Z"] = f"{best_pitch:.6f}"
140+
repeat_vector = f"{vector['X']} {vector['Y']} {vector['Z']}"
141+
repeater_lines.append(f"/gate/{system}/repeaters/insert\t\t\t\tlinear\n")
142+
repeater_lines.append(f"/gate/{system}/linear/setRepeatNumber\t\t\t{system_repeat}\n")
143+
repeater_lines.append(f"/gate/{system}/linear/setRepeatVector\t\t\t{repeat_vector} mm\n")
144+
return repeater_lines
145+
146+
def modify_geometry(input_file, segmentation, crp_mapping, output_file):
147+
"""
148+
Processes the geometry file line by line.
149+
A new element block is detected when a line with '/gate/xxx/daughters/name' appears.
150+
For geometry lines (matching '/set?Length') in element blocks:
151+
- If a computed update for that axis exists in global_update, replace its numeric value.
152+
- Otherwise, if the geometry line's axis equals the assigned segmentation axis for the current block,
153+
compute the update and store it in global_update.
154+
Finally, the repeater block is inserted immediately after the LAST line that contains "/gate/pseudoCrystal/".
155+
"""
156+
157+
158+
159+
with open(input_file, 'r') as f:
160+
lines = f.readlines()
161+
162+
modified_lines = []
163+
current_element = None # "column", "row", or "layer"
164+
global_update = {} # keyed by axis letter ("X", "Y", "Z"), with value (orig_value, best_pitch, repeat_number)
165+
first_system_index = None # to track the last line index where "/gate/pseudoCrystal/" occurs
166+
167+
modified_elements = []
168+
169+
name_re = re.compile(r'/gate/[^/]+/daughters/name\s+(.*)', re.IGNORECASE)
170+
geom_re = re.compile(r'(/gate/([^/]+)/geometry/set)([XYZ])Length\s+(\d+\.?\d*)\s*(mm|cm)', re.IGNORECASE)
171+
172+
for line in lines:
173+
# Detect new element block.
174+
175+
name_match = name_re.search(line)
176+
177+
178+
if name_match:
179+
token = name_match.group(1).strip().lower()
180+
181+
if "column" in token:
182+
current_element = "column"
183+
if crp_mapping["column"] and "column" not in modified_elements:
184+
modified_elements.append("column")
185+
186+
elif "row" in token:
187+
current_element = "row"
188+
if crp_mapping["row"] and "row" not in modified_elements:
189+
modified_elements.append("row")
190+
191+
elif "pseudo" in token:
192+
current_element = "pseudoCrystal"
193+
if crp_mapping["pseudoCrystal"] and "pseudoCrystal" not in modified_elements:
194+
modified_elements.append("pseudoCrystal")
195+
196+
else:
197+
current_element = None
198+
modified_lines.append(line)
199+
200+
continue
201+
202+
# Process geometry lines if within an element block.
203+
geom_match = geom_re.search(line)
204+
if current_element and geom_match:
205+
element_in_line = geom_match.group(2).lower()
206+
if current_element.lower() not in element_in_line:
207+
modified_lines.append(line)
208+
continue
209+
210+
prefix = geom_match.group(1) # e.g., "/gate/column/geometry/set"
211+
cmd_axis = geom_match.group(3).upper() # the axis letter in the command
212+
orig_value = float(geom_match.group(4))
213+
unit = geom_match.group(5).lower()
214+
if unit == "cm":
215+
orig_value *= 10
216+
217+
if cmd_axis in global_update:
218+
new_value = global_update[cmd_axis][1]
219+
new_line = f"{prefix}{cmd_axis}Length\t\t\t{new_value:.6f} mm\n"
220+
modified_lines.append(new_line)
221+
else:
222+
assigned_axis = crp_mapping.get(current_element)
223+
if assigned_axis is not None and cmd_axis == assigned_axis:
224+
target_pitch = segmentation.get(assigned_axis)
225+
if target_pitch is None:
226+
print(f"Warning: No digitizer pitch for axis {assigned_axis} for element {current_element}. Leaving line unchanged.")
227+
modified_lines.append(line)
228+
continue
229+
best_pitch, repeat_number = calculate_pitch(orig_value, target_pitch)
230+
global_update[cmd_axis] = (orig_value, best_pitch, repeat_number)
231+
new_line = f"{prefix}{cmd_axis}Length\t\t\t{best_pitch:.6f} mm\n"
232+
print(f"Extracted pitch for axis {assigned_axis} is {target_pitch} mm, the best pitch found is {best_pitch} mm and it will be repeated {repeat_number}")
233+
modified_lines.append(new_line)
234+
else:
235+
modified_lines.append(line)
236+
else:
237+
modified_lines.append(line)
238+
239+
for element in ["column","row","pseudoCrystal"]:
240+
if crp_mapping[element] and (element not in modified_elements):
241+
print(f"Error: the macro does not contain any geometry element named '",element,"'")
242+
sys.exit(1)
243+
244+
# Build repeater lines using the global update and crp mapping.
245+
repeater_lines = insert_repeater_lines(crp_mapping, global_update)
246+
247+
# Insert the repeater block after the LAST occurrence of "/gate/pseudoCrystal/"
248+
249+
first_system_index = next((i for i, line in enumerate(modified_lines) if '/gate/systems/' in line), None)
250+
if first_system_index is not None:
251+
modified_lines = modified_lines[:first_system_index] + ["\n","\n", "#Adding repeaters\n"]+ repeater_lines +["#End of repeaters\n","\n", "\n"]+ modified_lines[first_system_index:]
252+
253+
254+
else:
255+
256+
modified_lines.extend(["\n","\n", "#Adding repeaters\n"]+ repeater_lines +["#End of repeaters\n","\n", "\n"])
257+
258+
259+
with open(output_file, 'w') as f:
260+
f.writelines(modified_lines)
261+
262+
if __name__ == "__main__":
263+
crp_str, digitizer_file, input_file, output_file = parse_arguments()
264+
crp_mapping = build_crp_mapping(crp_str)
265+
if crp_mapping['column']:
266+
print("Columns need to be modified ",crp_mapping['column'])
267+
if crp_mapping["row"]:
268+
print("rows need to be modified")
269+
if crp_mapping["pseudoCrystal"]:
270+
print("pC need to be modified")
271+
272+
segmentation_params = extract_virtual_segmentation(digitizer_file)
273+
modify_geometry(input_file, segmentation_params, crp_mapping, output_file)
274+

0 commit comments

Comments
 (0)