HPS-MC
 
Loading...
Searching...
No Matches
_apply.py
Go to the documentation of this file.
1"""! applying parameter values to detector compact.xml in hps-mc jobs"""
2
3import shutil
4import re
5import os
6import json
7
8from hpsmc.component import Component
9from ._parameter import Parameter
10from ._pattern import Pattern
11
12
14 """! Abstract component to hold shared functionality for
15 compoents that edit the detectors in hps-java
16
17 **This component should never be used directly.**
18
19 Required Config:
20 ```
21 [<Component>]
22 java_dir = /full/path/to/hps-java
23 ```
24
25 Required Job:
26 - **detector**: name of detector we are starting from
27
28 Optional Parameters:
29 - **next_detector**: name of detector to write to
30 - **force**: ignore if the detector we are writing to already exists
31 """
32
33 def __init__(self, name, **kwargs):
34 # config
35 self.java_dir = None
36
37 # required job
38 self.detector = None
39
40 # optional job
41 self.next_detector = None
42 self.force = False
43
44 super().__init__(name, **kwargs)
45
46 def required_config(self):
47 return ['java_dir']
48
50 return ['detector']
51
53 return ['force', 'next_detector']
54
55 def _detector_dir(self, det_name):
56 return os.path.join(self.java_dir, 'detector-data', 'detectors', det_name)
57
58 def _deduce_next_detector(self, bump=False):
59 """! deduce what the next detector should be given how the component has been configured
60
61 The component parameter **bump** is an argument here since it is only
62 a valid parameter for some components inheriting from this function.
63 """
64 if bump or self.next_detector is not None:
65 self.logger.info('Creating new detector directory.')
66 # deduce source directory and check that it exists
67 src_path = self._detector_dir(self.detector)
68 if not os.path.isdir(src_path):
69 raise ValueError(f'Detector {self.detector} is not in hps-java ({src_path} not found)')
70
71 if self.next_detector is None:
72 self.logger.info('Deducing next detector name from current name')
73 # deduce iter value, using iter0 if there is no iter suffix
74 matches = re.search('.*iter([0-9]*)', self.detector)
75 if matches is None:
76 raise ValueError('No "iterN" suffix on detector name.')
77 else:
78 i = int(matches.group(1))
79 self.next_detector = self.detector.replace(f'iter{i}', f'iter{i+1}')
80
81 self.logger.info(f'Creating new detector named "{self.next_detector}"')
82 else:
83 self.logger.info(f'Operating on assumed-existing detector "{self.detector}"')
84 self.next_detector = self.detector
85
86 def _to_compact(self, parameter_set, detname, save_prev=True, prev_ext='prev'):
87 """! write the input parameter set into the input compact.xml file
88
89 Update the millepede parameters in the destination compact.xml with the
90 parameters stored in the parameter_set map.
91
92 Parameters
93 ----------
94 parameter_set : dict
95 dict mapping parameter ID number to Parameter instance
96 detname : str
97 name of detector whose compact.xml we should edit
98 save_prev : bool, optional
99 whether to save a copy of the original compact.xml before we edited it
100 prev_ext : str, optional
101 extension to add to the original compact.xml if it is being saved
102 """
103
104 def _change_xml_value(line, key, new_val, append=True):
105 """!change an XML line to have a new value
106
107 Assuming that the key and value are on the same line,
108 we can do some simple string arithmetic to find which
109 part of the string needs to be replaced.
110
111 Format:
112
113 xml-stuff key="value" other-xml-stuff
114
115 We make the replacement by finding the location of 'key'
116 in the line, then finding the next two quote characters.
117 The stuff in between those two quote characters is replaced
118 or appended with new_val and everything else in the line
119 is left the same.
120
121 The updated line is returned as a new string.
122 """
123
124 i_key = line.find(key)
125 pre_value = line[:i_key]
126 post_value = line[i_key:]
127
128 quote_open = post_value.find('"')+1
129 pre_value += post_value[:quote_open]
130 post_value = post_value[quote_open:]
131
132 quote_close = post_value.find('"')
133 og_value = post_value[:quote_close]
134 post_value = post_value[quote_close:]
135
136 new_value = f'{new_val}'
137 if append:
138 new_value = f'{og_value} {new_val}'
139
140 return f'{pre_value}{new_value}{post_value}'
141
142 # modify file in place
143 dest = os.path.join(self._detector_dir(detname), 'compact.xml')
144 if not os.path.isfile(dest):
145 raise ValueError(f'{detname} does not have a compact.xml to modify.')
146 self.logger.info(f'Writing compact.xml at {dest}')
147 original_cp = dest + '.' + prev_ext
148 shutil.copy2(dest, original_cp)
149 f = open(dest, 'w')
150 with open(dest, 'w') as f:
151 with open(original_cp) as og:
152 for line in og:
153 if 'info name' in line:
154 # update detector name
155 self.logger.debug(f'Changing detector name to {detname}')
156 f.write(_change_xml_value(line, 'name', detname, append=False))
157 line_edited = True
158 continue
159
160 if 'millepede_constant' not in line:
161 f.write(line)
162 continue
163
164 line_edited = False
165 for i in parameter_set:
166 if str(i) in line:
167 # the parameter with ID i is being set on this line
168 self.logger.debug(f'Changing parameter {i}')
169 f.write(_change_xml_value(
170 line, 'value', parameter_set[i].compact_value(), append=True
171 ))
172 line_edited = True
173 break
174
175 if not line_edited:
176 f.write(line)
177
178 # remove original copy if bumped since the previous iteration will have the previous version
179 if not save_prev:
180 os.remove(original_cp)
181
182 def _update_readme(self, detname, msg):
183 """! Update the readme for the passed detector name
184
185 Includes a timestamp at the end of the passed message.
186 """
187
188 # update/create a README to log how this detector has evolved
189 log_path = os.path.join(self._detector_dir(detname), 'README.md')
190 self.logger.info(f'Updating README.md at {log_path}')
191 with open(log_path, 'a') as log:
192 from datetime import datetime
193 log.write(f'\n# {detname}\n')
194 log.write(msg)
195 log.write(f'_auto-generated note on {str(datetime.now())}_\n')
196 log.flush() # need manual flush since we leave after this
197 return
198
199
201 """! Apply a millepede.res file to a detector description
202
203 This job component loads a result file into memory and the
204 goes line-by-line through a detector description, updating
205 the lines with any parameters that have updated values in the
206 result file.
207
208 Required Config:
209 ```
210 [ApplyPedeRes]
211 java_dir = /full/path/to/hps-java
212 ```
213
214 Required Parameters:
215 - **detector**: name of detector to apply parameters to
216
217 Optional Parameters:
218 - **res\\_file**: path to millepede results file (default: 'millepede.res')
219 - **bump**: generate the next detector name by incrementing the iter number of the input detector (default: True)
220 - **force**: override the next detector path (default: False)
221 - **next\\_detector**: provide name of next detector, preferred over **bump** if provided (default: None)
222 """
223
224 def __init__(self):
225 # optional job
226 self.res_file = 'millepede.res'
227 self.bump = True
228
229 # hidden job parameters
230 self.to_float = 'UNKNOWN'
231
232 super().__init__('ApplyPedeRes')
233
235 return super().optional_parameters() + ['res_file', 'bump', 'to_float']
236
237 def cmd_line_str(self):
238 return 'custom python execute'
239
240 def execute(self, log_out, log_err):
241 self._deduce_next_detector(self.bump)
242
243 # deduce destination path, and make sure it does not exist
244 dest_path = self._detector_dir(self.next_detectornext_detector)
245 if os.path.isdir(dest_path) and not self.force:
246 raise ValueError(f'Detector {self.next_detector} already exists and so it cannot be created. Use "force" to overwrite an existing detector.')
247
248 # make copy if the destination is not the same as the origin
250 # we already checked if the destination exists and the dirs_exist_ok parameter
251 # to shutil.copytree is only available in newer python versions
252 # so we remove the destination here now that we know (1) we can if it exists
253 # and (2) it is not the same as the source
254 if os.path.isdir(dest_path):
255 shutil.rmtree(dest_path)
256 shutil.copytree(self._detector_dir(self.detectordetector), dest_path)
257
258 # remove invalid copies of LCDD from next_detector path
259 for detname in (self.detectordetector, self.next_detectornext_detector):
260 lcdd_file = os.path.join(self._detector_dir(self.next_detectornext_detector), f'{detname}.lcdd')
261 if os.path.isfile(lcdd_file):
262 os.remove(lcdd_file)
263
264 # remove invalid properties file
265 properties_file = os.path.join(self._detector_dir(self.next_detectornext_detector), 'detector.properties')
266 if os.path.isfile(properties_file):
267 os.remove(properties_file)
268
269 # get list of parameters and their MP values
270 parameters = Parameter.parse_pede_res(self.res_file, skip_nonfloat=True)
271
272 self.logger.debug(f'Applying pede results: {parameters}')
273
274 self._to_compact(parameters, self.next_detectornext_detector)
276Compact updated by applying results from a run of pede
277
278### Parameters Floated
279```json
280{json.dumps(self.to_float, indent = 2)}
281```
282
283""")
284 return 0
285
286
288 """! write a detector intentionally misaligned relative to another one
289
290 Required Config:
291 ```
292 [WriteMisalignedDet]
293 java_dir = /full/path/to/hps-java
294 param_map = /full/path/to/parameter/map.txt
295 ```
296
297 Required Job:
298 - **detector** : name of detector to base our misalignment on
299 (and write to if no **next\\_detector** is given)
300 - **parameters** : dictionary of parameters to the change that should be applied
301 - each key in this dictionary is a hpsmc.alignment._pattern.Pattern so it can specify a single parameter or a group of parameters
302 """
303
304 def __init__(self):
305 # required config
306 self.param_map = None
307
308 # required job
309 self.parameters = None
310
311 super().__init__('WriteMisalignedDet')
312
314 return super().required_config() + ['param_map']
315
317 return super().required_parameters() + ['parameters']
318
319 def cmd_line_str(self):
320 return 'custom python execute'
321
322 def execute(self, out, err):
323 # translate pattern strings from JSON into Pattern objects
324 patterns = [
325 (Pattern(parameter_str), val_change)
326 for parameter_str, val_change in self.parameters.items()
327 ]
328
329 full_parameters = Parameter.parse_map_file(self.param_map)
330
331 parameters_to_apply = {}
332 for idn, param in full_parameters.items():
333 for pattern, val_change in patterns:
334 if pattern.match(param):
335 param._val = val_change
336 parameters_to_apply[idn] = param
337 break
338
340
341 src_det = self._detector_dir(self.detector)
342 if not os.path.isdir(src_det):
343 raise ValueError(f'{src_det} detector does not exist.')
344
345 dest_same_as_src = (self.next_detectornext_detector is None)
346 if dest_same_as_src and not self.force:
347 raise ValueError(f'Need to explicitly use the "force" parameter if you want to write to an existing detector.')
348
349 if not dest_same_as_src:
350 dest_det = self._detector_dir(self.next_detectornext_detector)
351 if not os.path.isdir(dest_det):
352 shutil.copytree(src_det, dest_det)
353 elif not self.force:
354 raise ValueError('{dest_det} detector already exists. Use "force" if you want to write to an existing detector.')
355
356 self._to_compact(parameters_to_apply, self.next_detectornext_detector, save_prev=self.force)
358 f"""
359Detector written by applying an intentional misalignment to {self.detector}.
360
361### Misalignment Applied
362```json
363{json.dumps(self.parameters, indent=2)}
364```
365
366""")
367
368
370 """! construct an LCDD from a compact.xml and recompile necessary parts of hps-java
371
372 This is a Component interface to the hps-mc-construct-detector script.
373
374 Required Config:
375 ```
376 [ConstructDetector]
377 java_dir = /full/path/to/hps-java
378 hps_java_bin_jar = /full/path/to/hps-java/bin.jar
379 ```
380
381 Required Parameters:
382 - **detector**: name of detector to construct (unless next\\_detector is provided)
383
384 Optional Parameters:
385 - **bump**: generate the next detector name by incrementing the iter number of the input detector (default: True)
386 - **force**: override the next detector path (default: False)
387 - **next\\_detector**: provide name of next detector, preferred over **bump** if provided (default: None)
388 """
389
390 def __init__(self):
391 # config
393
394 # optional job
395 # only used when in the same job as ApplyPedeRes
396 self.bump = True
397
398 # detector we will actuall construct
400
401 super().__init__('ConstructDetector',
402 command='hps-mc-construct-detector')
403
405 return super().required_config() + ['hps_java_bin_jar']
406
408 return super().optional_parameters() + ['bump']
409
410 def setup(self):
411 """Called after configured but before running
412
413 We deduce which detector we will be running with,
414 attempting to mimic the logic in ApplyPedeRes.execute
415 so that we compile the same detector that pede results
416 were written into.
417 """
418 self._deduce_next_detector(self.bump)
419
420 def cmd_args(self):
421 return [self.next_detectornext_detector, '-p', self.java_dirjava_dir, '-jar', self.hps_java_bin_jar]
Apply a millepede.res file to a detector description.
Definition _apply.py:200
execute(self, log_out, log_err)
Generic component execution method.
Definition _apply.py:240
optional_parameters(self)
Return a list of optional parameters.
Definition _apply.py:234
construct an LCDD from a compact.xml and recompile necessary parts of hps-java
Definition _apply.py:369
optional_parameters(self)
Return a list of optional parameters.
Definition _apply.py:407
required_config(self)
Return a list of required configuration settings.
Definition _apply.py:404
cmd_args(self)
Return the command arguments of this component.
Definition _apply.py:420
write a detector intentionally misaligned relative to another one
Definition _apply.py:287
required_parameters(self)
Return a list of required parameters.
Definition _apply.py:316
execute(self, out, err)
Generic component execution method.
Definition _apply.py:322
required_config(self)
Return a list of required configuration settings.
Definition _apply.py:313
Abstract component to hold shared functionality for compoents that edit the detectors in hps-java.
Definition _apply.py:13
_to_compact(self, parameter_set, detname, save_prev=True, prev_ext='prev')
write the input parameter set into the input compact.xml file
Definition _apply.py:86
_deduce_next_detector(self, bump=False)
deduce what the next detector should be given how the component has been configured
Definition _apply.py:58
optional_parameters(self)
Return a list of optional parameters.
Definition _apply.py:52
required_parameters(self)
Return a list of required parameters.
Definition _apply.py:49
required_config(self)
Return a list of required configuration settings.
Definition _apply.py:46
__init__(self, name, **kwargs)
Definition _apply.py:33
_update_readme(self, detname, msg)
Update the readme for the passed detector name.
Definition _apply.py:182
Pattern that can match one or more paramters.
Definition _pattern.py:6
Base class for components in a job.
Definition component.py:15