HPS-MC
_apply.py
Go to the documentation of this file.
1 """! applying parameter values to detector compact.xml in hps-mc jobs"""
2 
3 import shutil
4 import re
5 import os
6 import json
7 
8 from hpsmc.component import Component
9 from ._parameter import Parameter
10 from ._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_dirjava_dir = None
36 
37  # required job
38  self.detectordetector = None
39 
40  # optional job
41  self.next_detectornext_detector = None
42  self.forceforce = 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_dirjava_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_detectornext_detector is not None:
65  self.loggerlogger.info('Creating new detector directory.')
66  # deduce source directory and check that it exists
67  src_path = self._detector_dir_detector_dir(self.detectordetector)
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_detectornext_detector is None:
72  self.loggerlogger.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.detectordetector)
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_detectornext_detector = self.detectordetector.replace(f'iter{i}', f'iter{i+1}')
80 
81  self.loggerlogger.info(f'Creating new detector named "{self.next_detector}"')
82  else:
83  self.loggerlogger.info(f'Operating on assumed-existing detector "{self.detector}"')
84  self.next_detectornext_detector = self.detectordetector
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_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.loggerlogger.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.loggerlogger.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.loggerlogger.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_detector_dir(detname), 'README.md')
190  self.loggerlogger.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_fileres_file = 'millepede.res'
227  self.bumpbump = True
228 
229  # hidden job parameters
230  self.to_floatto_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_deduce_next_detector(self.bumpbump)
242 
243  # deduce destination path, and make sure it does not exist
244  dest_path = self._detector_dir_detector_dir(self.next_detectornext_detector)
245  if os.path.isdir(dest_path) and not self.forceforce:
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
249  if self.next_detectornext_detector != self.detectordetector:
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_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_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_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_fileres_file, skip_nonfloat=True)
271 
272  self.loggerlogger.debug(f'Applying pede results: {parameters}')
273 
274  self._to_compact_to_compact(parameters, self.next_detectornext_detector)
275  self._update_readme_update_readme(self.next_detectornext_detector, f"""
276 Compact 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_mapparam_map = None
307 
308  # required job
309  self.parametersparameters = None
310 
311  super().__init__('WriteMisalignedDet')
312 
313  def required_config(self):
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.parametersparameters.items()
327  ]
328 
329  full_parameters = Parameter.parse_map_file(self.param_mapparam_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 
339  self._deduce_next_detector_deduce_next_detector()
340 
341  src_det = self._detector_dir_detector_dir(self.detectordetector)
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.forceforce:
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_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.forceforce:
354  raise ValueError('{dest_det} detector already exists. Use "force" if you want to write to an existing detector.')
355 
356  self._to_compact_to_compact(parameters_to_apply, self.next_detectornext_detector, save_prev=self.forceforce)
357  self._update_readme_update_readme(self.next_detectornext_detector,
358  f"""
359 Detector 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
392  self.hps_java_bin_jarhps_java_bin_jar = None
393 
394  # optional job
395  # only used when in the same job as ApplyPedeRes
396  self.bumpbump = True
397 
398  # detector we will actuall construct
399  self.detector_to_constructdetector_to_construct = None
400 
401  super().__init__('ConstructDetector',
402  command='hps-mc-construct-detector')
403 
404  def required_config(self):
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_deduce_next_detector(self.bumpbump)
419 
420  def cmd_args(self):
421  return [self.next_detectornext_detector, '-p', self.java_dirjava_dir, '-jar', self.hps_java_bin_jarhps_java_bin_jar]
Apply a millepede.res file to a detector description.
Definition: _apply.py:200
def execute(self, log_out, log_err)
Generic component execution method.
Definition: _apply.py:240
def 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
def required_config(self)
Return a list of required configuration settings.
Definition: _apply.py:404
def optional_parameters(self)
Return a list of optional parameters.
Definition: _apply.py:407
def 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
def required_config(self)
Return a list of required configuration settings.
Definition: _apply.py:313
def required_parameters(self)
Return a list of required parameters.
Definition: _apply.py:316
def execute(self, out, err)
Generic component execution method.
Definition: _apply.py:322
Abstract component to hold shared functionality for compoents that edit the detectors in hps-java.
Definition: _apply.py:13
def required_config(self)
Return a list of required configuration settings.
Definition: _apply.py:46
def __init__(self, name, **kwargs)
Definition: _apply.py:33
def required_parameters(self)
Return a list of required parameters.
Definition: _apply.py:49
def _update_readme(self, detname, msg)
Update the readme for the passed detector name.
Definition: _apply.py:182
def optional_parameters(self)
Return a list of optional parameters.
Definition: _apply.py:52
def _detector_dir(self, det_name)
Definition: _apply.py:55
def _deduce_next_detector(self, bump=False)
deduce what the next detector should be given how the component has been configured
Definition: _apply.py:58
def _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
Pattern that can match one or more paramters.
Definition: _pattern.py:6
Base class for components in a job.
Definition: component.py:15