ATLAS Offline Software
Loading...
Searching...
No Matches
subdetector.py
Go to the documentation of this file.
1# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
2
3from DQUtils import process_iovs
4from DQUtils.sugar import IOVSet, define_iov_type
5from DQUtils.ext import tally
6
7import DCSCalculator2.config as config
8from .variable import GoodIOV, DCSC_Variable_With_Mapping, DefectIOV
9from .consts import (WHITE, GREY, RED, YELLOW, GREEN,
10 EMPTY, OUT_OF_CONFIG, BAD, GOOD)
11
12import logging
13log = logging.getLogger("DCSCalculator2.subdetector")
14logEnabledFor = log.isEnabledFor
15
16
17@define_iov_type
18def DCSOFL_IOV(channel, Code, deadFrac, Thrust, NConfig, NWorking):
19 "DCS calculator result iov type"
20
22 """
23 A defect calculator for one subdetector.
24 """
25
26 def __init__(self):
27
28 # calculate the inverse mapping if appropriate
29 if not hasattr(self, "input_to_output_map") and hasattr(self, "mapping"):
30 # NOTE: this breaks silently if an input channel was accidentally
31 # mapped to more than one output channel. Maybe I should add a check.
32 inverse = {value: key
33 for key, values in self.mapping.items()
34 for value in values}
35
36 self.input_to_output_map = inverse
37
38 def __repr__(self):
39 return self.__class__.__name__
40
41 def get_variable(self, name):
42 """
43 Get a DCS_Variable by name.
44 """
45 for variable in self.variables:
46 if variable.folder_name == name:
47 return variable
48
49 raise RuntimeError("Folder '%s' not found" % name)
50
51 def set_input_mapping(self, what, mapping):
52 """
53 Set mapping of input channels for a DCSC_Variable_With_Mapping.
54 Some input folders have different channel numbering conventions.
55 """
56 variable = self.get_variable(what)
57 # Mapping only makes sense for DCSC_Variable_With_Mapping
58 if not isinstance(variable, DCSC_Variable_With_Mapping):
59 raise RuntimeError("'%s' is not a DCSC_Variable_With_Mapping!")
60
61 variable.input_channel_map = mapping
62
63 def evaluate_inputs(self, lbtime):
64 """
65 Read the cool database and determine the state of the input channels
66 by luminosity block
67 """
68
69 # inputs is literally the same as self.variables
70 # # Is this true?? I don't think it is
71 # calculate_good_iovs will calculate the goodness of all input channels
72 # and remap the channel ids if necessary.
73 # TODO: This could be rewritten to be more clear.
74 inputs = [v.calculate_good_iovs(lbtime, self) for v in self.variables]
75
76 # Why do we care what the hash value is for the variables??
77 if log.isEnabledFor(logging.INFO):
78 log.info("lbtime hash = % 09x, inputs hash = %09x",
79 hash(lbtime), hash(tuple(self.variables)))
80 return inputs
81
82 def merge_variable_states(self, states):
83 """
84 Merge input channel states across variables, taking the worst.
85
86 For detector configuration variables, it is assumed that if no IoV
87 exists for that channel in this variable, then it is in a good state.
88 """
89
90 # Start off with a good result.
91 result = True
92
93 # This assumes that the states array is in sync with the variables array..
94 # Maybe I could add an assert statement here.
95 # WOA! That's not cool! TODO: MAKE THIS BETTER
96 # I don't think it has broken anything before but depending on how the variables
97 # are defined in a subdetector I think this could be very bad!!!
98
99 # loop over states
100 for state, variable in zip(states, self.variables):
101 state = state.good if state else None
102 if state == OUT_OF_CONFIG:
103 assert variable.is_config_variable, "OOC without is_config_variable!"
104 # If any state is out of config, we know the result.
105 return state
106
107 elif state is None or state < result:
108 if state is WHITE and variable.is_config_variable:
109 # Empty config variables are equivalent to "GOOD", so these
110 # states should be skipped.
111 continue
112 result = state
113
114 # More simplistic way of doing the above, but can't handle config vars:
115 # return min(None if not state else state.good for state in states)
116
117 return result
118
119
120 def merge_inputs(self, channel, *inputs):
121 """
122 Merge multiple variables together for one input channel.
123 Each 'inputs' arg is an IOVSet that corresponds to this input channel.
124 """
125 # inputs must correspond to and be in sync with subdetector.variables...?
126
127 result = IOVSet()
128 # combine the IOVSets into smallest chunks using process_iovs
129 for since, until, states in process_iovs(*inputs):
130 # Get the worst state for the list of vectors
131 state = self.merge_variable_states(states)
132 result.add(since, until, channel, state)
133
134 return result.solidify(GoodIOV)
135
136 def merge_input_information(self, channel, *inputs):
137 """
138 Join up the information which was used to make a decision across
139 multiple variables.
140 """
141 result = IOVSet()
142 for since, until, states in process_iovs(*inputs):
143 info = tuple(state._orig_iov[3:] for state in states)
144 result.add(since, until, channel, info)
145
146 return result.solidify(GoodIOV)
147
148 # inputs is a USELESS name for such an object
149 def merge_input_variables(self, inputs):
150 """
151 Merge multiple variables together for many channels.
152 Takes a list of IOVSets, one for each DCSC_Variable.
153 """
154
155 result = []
156 info_states = IOVSet(iov_type=GoodIOV)
157
158 # Reassign inputs to be a list of dictionaries with
159 # Key=channel_id and Val=IOVSet
160 inputs = [iovs.by_channel for iovs in inputs]
161
162 # set of channel ids
163 all_channels = sorted(set(y for x in inputs for y in x.keys()))
164
165 tally_system_states = config.opts.tally_system_states
166
167 for channel in all_channels:
168
169 # Handle one channel at a time for variable merging
170 c_inputs = [x[channel] for x in inputs]
171
172 # Merge "good" state across multiple variables
173 result.append(self.merge_inputs(channel, *c_inputs))
174
175 if tally_system_states:
176 # Merge Input information across multiple variables
177 info_state = self.merge_input_information(channel, *c_inputs)
178 info_states.extend(info_state)
179
180
181 if tally_system_states:
182 # Print a tally of the states for the different systems
183 from DQUtils.ext import tally
184 def pretty(state):
185 return "/".join(x[0] for x in state)
186
187 chans, iovs = zip(*sorted(info_states.by_channel.items()))
188 for since, until, states in process_iovs(self.run_iovs, *iovs):
189 if states[0]._is_empty:
190 # Not inside a run
191 continue
192
193 statetally = tally(pretty(x.good) for x in states[1:])
194
195 print(since, until, statetally)
196
197 return result
198
199 def map_inputs_to_outputs(self, inputs):
200 """
201 Determine which input channels belong to which output channels.
202 inputs is a list of IOVSets, exactly one per channel.
203 """
204 # Per output object, store a list of iov lists.
205 result = {}
206
207 # Keep a record of the channels we have seen, and what their index
208 # will be in the states list. This is so that we can determine later
209 # which channel a state belongs to. (For empty IoVs, for instance)
211 seen_channels = set()
212
213 empty_iovset_types = [iovs.empty_maker() for iovs in inputs]
214
215 # input to output map is the inverse map with
216 # key=input_channel and val=output_channel
217 mapping = self.input_to_output_map
218
219 # Loop over channels
220 for iovs in inputs:
221 if not iovs: continue # Can this happen?
222 input_channel = iovs[0].channel
223 if input_channel not in mapping:
224 raise RuntimeError("channel not found in mapping: " + str(input_channel))
225 seen_channels.add(input_channel)
226 output_channel = mapping[input_channel]
227 result.setdefault(output_channel, []).append(iovs)
228 self.channel_indices.setdefault(output_channel, []).append(input_channel)
229
230 missing_channels = self.input_channel_set - seen_channels
231
232 for channel, make_iovset in zip(missing_channels, empty_iovset_types):
233 # No IoVs for this channel. Append an empty IoV range.
234 output_channel = mapping[channel]
235 result.setdefault(output_channel, []).append(make_iovset())
236 (self.channel_indices.setdefault(output_channel, [])
237 .append(channel))
238
239 return result
240
241 @property
243 """
244 Return a set containing the all input channel IDs for this subdetector
245 """
246 return set(v for vals in self.mapping.values() for v in vals)
247
248 def get_name_for_input_channel(self, input_channel):
249 """
250 If it is possible to give a logical name for an input channel, return
251 it here. These numbers are used for debugging purposes.
252
253 By default, do nothing. Over-ridden by subdetectors
254 """
255 return input_channel
256
257 def get_ids_which_are(self, output_channel, states, what):
258 indices = [i for i, x in enumerate(states) if x is what]
259 chan_indices = self.channel_indices[output_channel]
260 input_chan_name = self.get_name_for_input_channel
261 return [input_chan_name(chan_indices[i]) for i in indices]
262
263 def calculate_dead_fraction(self, since, until, output_channel, states,
264 state_iovs):
265 """
266 Calculate the dead fraction and the resulting traffic light code.
267 """
268
269 #states = [s.good if s is not None else None for s in state_iovs]
270
271 n_total = len(states)
272 n_working = states.count(GOOD)
273 n_bad = states.count(BAD)
274 n_ooc = states.count(OUT_OF_CONFIG)
275 n_unfilled = states.count(EMPTY)
276
277 log.debug("%s -> %s tot:%4s working:%4s bad:%4s ooc:%4s empty:%4s",
278 since, until, n_total, n_working, n_bad, n_ooc, n_unfilled)
279
280 # Reminder to self:
281 # Need to take total from the right hand side here, ultimately.
282 # Perhaps the correct method is to insert "empty IoVs" for missing
283 # channels
284 assert n_total == len(self.mapping[output_channel])
285
286 if logEnabledFor(logging.DEBUG):
287 if n_unfilled:
288 args = output_channel, states, EMPTY
289 unfilled_chans = sorted(self.get_ids_which_are(*args))
290 log.debug("WARNING: the following channelids are unfilled: "
291 "%r", unfilled_chans)
292
293 if n_ooc:
294 ooc_ids = self.get_ids_which_are(output_channel, states, OUT_OF_CONFIG)
295 log.debug("OOC ids: %s", sorted(ooc_ids))
296 if n_bad:
297 bad_ids = self.get_ids_which_are(output_channel, states, BAD)
298 log.debug("BAD ids: %s", sorted(bad_ids))
299
300 assert not n_total - n_working - n_bad - n_ooc - n_unfilled, (
301 "Some states unaccounted for? This is a bug.")
302
303 n_config = n_total - n_ooc
304 dead_fraction = 1. - n_working / n_total
305
306 code = GREEN
307 if self.dead_fraction_caution is not None and (
308 self.dead_fraction_caution < dead_fraction <= self.dead_fraction_bad):
309 code = YELLOW
310
311 elif dead_fraction > self.dead_fraction_bad:
312 code = RED
313
314 # If the number of unfilled channels is sufficient to send us over the
315 # caution threshold, then set the code to GREY to indicate a problem.
316 unfilled_fraction = n_unfilled / n_total
317 threshold = (self.dead_fraction_caution
318 if self.dead_fraction_caution is not None else
320 if unfilled_fraction > threshold:
321 code = GREY
322
323 if n_unfilled and config.opts.mark_unfilled_grey:
324 code = GREY
325
326 # what the heck is thrust?
327 thrust = 0.
328 return code, dead_fraction, thrust, n_config, n_working
329
330 def debug_what_changed(self, runlb, prev_states, states):
331 """Apparently not used"""
332 changes = [(a, b) for a, b in zip(prev_states, states) if a != b]
333 log.debug("Changes at %s: %i: %s", runlb, len(changes), tally(changes))
334
335 def calculate_dead_fraction_all(self, output_channel, local_variables):
336
337 log.debug("Calculating dead fractions for output: %i", output_channel)
338
339 # local_variables is a list of IOVSets (one for each input channel),
340 # for this output channel.
341 # Why would you call it local_variables?
342
343 #prev_states = []
344
345 dead_frac_iovs = IOVSet()
346 calc_dead_frac = self.calculate_dead_fraction
347 # loop over smallest IOV chunks for this output channel
348 for since, until, states in process_iovs(self.run_iovs, *local_variables):
349 run_iov = states[0]
350 # state_iovs is now a list of iovs, one for each input channel mapped
351 # to this output channel
352 state_iovs = states[1:]
353
354 states = [s.good for s in state_iovs]
355
356 if run_iov._is_empty:
357 # Ignore regions outside runs.
358 continue
359
360 iov_state = calc_dead_frac(since, until, output_channel,
361 states, state_iovs)
362
363 #dead_frac_iovs.add(since, until, output_channel, *iov_state)
364 result_iov = DCSOFL_IOV(since, until, output_channel, *iov_state)
365 result_iov._orig_iovs = state_iovs
366 dead_frac_iovs.append(result_iov)
367
368 return dead_frac_iovs#.solidify(DCSOFL_IOV)
369
370 def dq_worst(self, states):
371 """
372 Make a DQ-colour decision based on `states`
373 """
374 states = set([s.Code for s in states])
375 if RED in states: return RED
376 elif YELLOW in states: return YELLOW
377 elif WHITE in states: return WHITE
378 elif GREEN in states: return GREEN
379 return RED
380
381 def merge_globals(self, output_channel, dead_frac_iovs, global_variables):
382 """
383 Merge together global states to decide a final code
384
385 If the dead fraction is unavailable, writes -1.
386 """
387 result_iovs = IOVSet()
388 if self.run_iovs is not None:
389 # run_iovs are used to constrain to what runs the calculator will
390 # write. If there is a hole in `run_iovs`, then no records are emitted.
391 state_ranges = process_iovs(self.run_iovs, dead_frac_iovs, *global_variables)
392 else:
393 state_ranges = process_iovs(dead_frac_iovs, *global_variables)
394
395 for since, until, states in state_ranges:
396 if self.run_iovs:
397 # No run_iovs were specified, so run for all available input data
398 run_iov, dead_frac_iov = states[:2]
399
400 if run_iov._is_empty:
401 # We're outside of a run, don't write an IoV.
402 continue
403
404 states = states[1:]
405 else:
406 dead_frac_iov = states[0]
407
408 if not dead_frac_iov._is_empty:
409 dead_fraction, thrust, n_config, n_working = dead_frac_iov[4:]
410 else:
411 dead_fraction, thrust, n_config, n_working = -1., 0., -1, -1
412 states = states[1:]
413
414 code = self.dq_worst(states)
415 state = dead_fraction, thrust, n_config, n_working
416
417 if code is WHITE:
418 # Don't write empty regions
419 continue
420
421 result_iovs.add(since, until, output_channel, code, *state)
422
423 return result_iovs.solidify(DCSOFL_IOV)
424
425 def calculate_result_for_output(self, output_channel,
426 local_variables, global_variables):
427 """
428 Calculate the iov extents and dead fractions for one output channel
429
430 * If there are 'non-global' variables, evaluate the dead fraction, which
431 effectively becomes a new global variable.
432 * If there are no global variables, return the above as a result
433 * If there are global variables, merge them together.
434 """
435
436 dead_frac_iovs = IOVSet(iov_type=DCSOFL_IOV)
437
438 if local_variables:
439 dead_frac_iovs = self.calculate_dead_fraction_all(output_channel,
440 local_variables)
441
442 if not global_variables:
443 # There are no globals, we're done.
444 return dead_frac_iovs
445
446 return self.merge_globals(output_channel, dead_frac_iovs, global_variables)
447
448 def select_globals(self, output_channel, input_globals):
449 """
450 Returns a list where each element is a list of (single channel) iovs.
451
452 The `input_globals` may contain a list of iovs which has multiple
453 channels. This function may be over-ridden by inheriting classes to
454 select channels for this output channel.
455 """
456 global_iov_sets = []
457 for input_global in input_globals:
458 for channel, iovs in sorted(input_global.by_channel.items()):
459 global_iov_sets.append(iovs)
460
461 return global_iov_sets
462
463 def calculate_result(self, inputs_by_output, global_variables):
464 """
465 Terrible name for a method.
466 Calculate the iov extents and dead fractions for all output channels.
467 In other words, the IoVs to be written to DCSOFL for this subdetector.
468 """
469 result = IOVSet(iov_type=DCSOFL_IOV)
470
471 # loop over output channel dictionary
472 for channel, input_iovs in sorted(inputs_by_output.items()):
473 these_globals = self.select_globals(channel, global_variables)
474 args = channel, input_iovs, these_globals
475 result.extend(self.calculate_result_for_output(*args))
476
477 return result
478
479 def run(self, lbtime, run_iovs=None):
480 """
481 Run the DCSC for this subdetector.
482
483 * Evaluate inputs
484 * Merge input variables together
485 * Calculate resulting IoVs to be written
486 """
487
488 self.run_iovs = run_iovs
489
490 self.start()
491
492 # evaluate_inputs will calculate the goodness of all channels
493 # for each variable, remapping channel ids if necessary.
494 # this 'variables' object is the same as self.variables
495 # -> hold this thought, I'm not sure this is correct
496 variables = self.evaluate_inputs(lbtime)
497
498 # separate variables into global and local.
499 # They are lists of IOVSets
500 # Each element of the list corresponds to a DCSC_Variable
501 local_variables = [v.iovs for v in variables if not v.is_global]
502 global_variables = [v.iovs for v in variables if v.is_global]
503
504 # Merge IOVSets into a list with one IOVSet per channel
505 input_channel_states = self.merge_input_variables(local_variables)
506
507 # We only have merged input_channel_states if there are local variables
508 if input_channel_states:
509 inputs_by_output = self.map_inputs_to_outputs(input_channel_states)
510
511 else:
512 # If there are no locals, we need an empty locals list per output
513 inputs_by_output = dict((cid, []) for cid in self.mapping.keys())
514
515 # Calculate the final output IOVs
516 result_iovs = self.calculate_result(inputs_by_output, global_variables)
517
518 self.done()
519
520 return result_iovs
521
522 def start(self):
523 "Empty function called at start"
524
525 def done(self):
526 """
527 An empty function which can be overloaded to do any needed
528 post-processing
529 """
530
531class DCSC_DefectTranslate_Subdetector(DCSC_Subdetector):
532 """
533 A defect calculator for one subsystem that still works in terms of color
534 flags. The colors need to be translated into defects by building translators
535 with the color_to_defect_translator static method.
536 """
537
538 def __init__(self, keep_dcsofl=False):
539 super(DCSC_DefectTranslate_Subdetector, self).__init__()
540 self.translators = []
541 self.keep_dcsofl = keep_dcsofl
542
543 def run(self, lbtime, run_iovs=None):
544 """
545 Run the DCSC for this subdetector.
546
547 * Evaluate inputs
548 * Merge input variables together
549 * Calculate resulting IoVs to be written
550 """
551 flag_iovs = super(DCSC_DefectTranslate_Subdetector, self).run(lbtime, run_iovs)
552 translated = sum((func(flag_iovs) for func in self.translators), [])
553 if self.keep_dcsofl:
554 translated += flag_iovs
555 return translated
556
557 @staticmethod
558 def color_to_defect_translator(inflag, outdefect, badcolors=[RED]):
559 def translator_core(flag_iovs):
560 rv = [DefectIOV(since=iov.since, until=iov.until,
561 channel=outdefect, present=True,
562 comment='Bad Fraction: %.3f' % iov.deadFrac)
563 for iov in flag_iovs if iov.channel == inflag
564 and iov.Code in badcolors]
565
566 return rv
567 return translator_core
568
570 """
571 This calculator can be used if the calculator is only using defects
572 """
573
574 def run(self, lbtime, run_iovs):
575 self.evaluate_inputs(lbtime)
576 result = IOVSet()
577 for variable in self.variables:
578 result.extend(variable.iovs)
579 return result
void print(char *figname, TCanvas *c1)
color_to_defect_translator(inflag, outdefect, badcolors=[RED])
debug_what_changed(self, runlb, prev_states, states)
set_input_mapping(self, what, mapping)
merge_globals(self, output_channel, dead_frac_iovs, global_variables)
calculate_dead_fraction(self, since, until, output_channel, states, state_iovs)
calculate_result(self, inputs_by_output, global_variables)
get_ids_which_are(self, output_channel, states, what)
calculate_dead_fraction_all(self, output_channel, local_variables)
merge_input_information(self, channel, *inputs)
calculate_result_for_output(self, output_channel, local_variables, global_variables)
merge_inputs(self, channel, *inputs)
get_name_for_input_channel(self, input_channel)
select_globals(self, output_channel, input_globals)
STL class.
DCSOFL_IOV(channel, Code, deadFrac, Thrust, NConfig, NWorking)
Definition run.py:1