pytesprocess 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {pytesprocess-0.1.2/pytesprocess.egg-info → pytesprocess-0.1.3}/PKG-INFO +2 -2
  2. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pyproject.toml +1 -1
  3. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/_version.py +1 -1
  4. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/data_source.py +44 -8
  5. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_errors.py +31 -3
  6. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/features.py +6 -3
  7. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/randoms.py +8 -1
  8. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/triggers.py +7 -12
  9. pytesprocess-0.1.3/pytesprocess/utils/shard_output.py +94 -0
  10. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/workflows/salting.py +2 -1
  11. {pytesprocess-0.1.2 → pytesprocess-0.1.3/pytesprocess.egg-info}/PKG-INFO +2 -2
  12. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/SOURCES.txt +2 -0
  13. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/requires.txt +1 -1
  14. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_on_error.py +52 -9
  15. pytesprocess-0.1.3/tests/test_shard_output.py +151 -0
  16. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/LICENSE +0 -0
  17. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/README.md +0 -0
  18. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/__init__.py +0 -0
  19. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/__init__.py +0 -0
  20. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/__init__.py +0 -0
  21. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/event.py +0 -0
  22. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/feature.py +0 -0
  23. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/filter.py +0 -0
  24. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/ivsweep.py +0 -0
  25. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/randoms.py +0 -0
  26. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/trigger.py +0 -0
  27. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/vibration.py +0 -0
  28. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/common.py +0 -0
  29. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/main.py +0 -0
  30. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/__init__.py +0 -0
  31. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/loader.py +0 -0
  32. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/manager.py +0 -0
  33. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/__init__.py +0 -0
  34. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/common.py +0 -0
  35. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/feature.py +0 -0
  36. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/salting.py +0 -0
  37. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/trigger.py +0 -0
  38. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/vibration.py +0 -0
  39. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/selectors.py +0 -0
  40. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/validation.py +0 -0
  41. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/warnings.py +0 -0
  42. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/__init__.py +0 -0
  43. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/algorithms.py +0 -0
  44. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/didv.py +0 -0
  45. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/eventbuilder.py +0 -0
  46. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/filter_metadata.py +0 -0
  47. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/filterbuilder.py +0 -0
  48. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/filterdata.py +0 -0
  49. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/ivsweep.py +0 -0
  50. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/noise.py +0 -0
  51. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/noisemodel.py +0 -0
  52. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/oftrigger.py +0 -0
  53. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/template.py +0 -0
  54. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/trigger_utils.py +0 -0
  55. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/__init__.py +0 -0
  56. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/event_context.py +0 -0
  57. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_memory.py +0 -0
  58. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_plan.py +0 -0
  59. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_resources.py +0 -0
  60. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_schema.py +0 -0
  61. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_windows.py +0 -0
  62. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_work.py +0 -0
  63. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/filterprocess.py +0 -0
  64. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/ivprocess.py +0 -0
  65. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/processing_data.py +0 -0
  66. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/randoms_work.py +0 -0
  67. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/trigger_partitions.py +0 -0
  68. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/trigger_work.py +0 -0
  69. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/work_plan.py +0 -0
  70. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/__init__.py +0 -0
  71. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/generator.py +0 -0
  72. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/injector.py +0 -0
  73. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/sampling.py +0 -0
  74. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/__init__.py +0 -0
  75. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/arg_utils.py +0 -0
  76. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/channels.py +0 -0
  77. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/dataframe_output.py +0 -0
  78. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/downsampling.py +0 -0
  79. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/event_metadata.py +0 -0
  80. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/filter_hdf5.py +0 -0
  81. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/utils.py +0 -0
  82. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/__init__.py +0 -0
  83. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/analyzer.py +0 -0
  84. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/estimators.py +0 -0
  85. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/plotting.py +0 -0
  86. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/processing.py +0 -0
  87. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/workflows/__init__.py +0 -0
  88. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/workflows/processing.py +0 -0
  89. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/dependency_links.txt +0 -0
  90. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/entry_points.txt +0 -0
  91. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/top_level.txt +0 -0
  92. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/setup.cfg +0 -0
  93. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_channel_expressions.py +0 -0
  94. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_cli_exit_status.py +0 -0
  95. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_cli_steps_v16.py +0 -0
  96. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_config_resolvers.py +0 -0
  97. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_dataframe_output.py +0 -0
  98. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_downsampled_filters.py +0 -0
  99. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_downsampling.py +0 -0
  100. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_empty_and_selected_features.py +0 -0
  101. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_eventbuilder_coincidence.py +0 -0
  102. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_downsampling.py +0 -0
  103. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_memory.py +0 -0
  104. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_schema.py +0 -0
  105. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_tasks.py +0 -0
  106. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_unit_execution.py +0 -0
  107. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_window_errors.py +0 -0
  108. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_work_units.py +0 -0
  109. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_fft_bin.py +0 -0
  110. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_filter_compatibility.py +0 -0
  111. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_filter_hdf5.py +0 -0
  112. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_filter_metadata.py +0 -0
  113. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_memory_limit_per_worker.py +0 -0
  114. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_metadata_roundtrip.py +0 -0
  115. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_native_features.py +0 -0
  116. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_processing_data_v15.py +0 -0
  117. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_processing_guards.py +0 -0
  118. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_randoms_work_units.py +0 -0
  119. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_restricted_processing.py +0 -0
  120. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_signed_feature_windows.py +0 -0
  121. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_template_metadata.py +0 -0
  122. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_alignment.py +0 -0
  123. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_downsampling.py +0 -0
  124. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_filter_reuse.py +0 -0
  125. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_partition_groups.py +0 -0
  126. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_work_units.py +0 -0
  127. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_vibration_estimators.py +0 -0
  128. {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_vibration_processing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytesprocess
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Detector data processing for pytesdaqx acquisitions
5
5
  Author-email: Bruno Serfass <serfass@berkeley.edu>, Samuel Watkins <samwatkins@berkeley.edu>
6
6
  License: MIT License
@@ -35,7 +35,7 @@ Requires-Dist: numpy>=1.26
35
35
  Requires-Dist: scipy
36
36
  Requires-Dist: matplotlib
37
37
  Requires-Dist: PyYAML
38
- Requires-Dist: qetpy>=1.8.6
38
+ Requires-Dist: qetpy>=1.8.9
39
39
  Requires-Dist: pandas
40
40
  Requires-Dist: tables
41
41
  Requires-Dist: pytesdaqx
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "scipy",
20
20
  "matplotlib",
21
21
  "PyYAML",
22
- "qetpy>=1.8.6",
22
+ "qetpy>=1.8.9",
23
23
  "pandas",
24
24
  "tables",
25
25
  "pytesdaqx",
@@ -1,2 +1,2 @@
1
1
  # special file for defining the current version of the package
2
- __version__ = "0.1.2"
2
+ __version__ = "0.1.3"
@@ -311,7 +311,18 @@ class RawDataSource:
311
311
 
312
312
 
313
313
  class TriggerDataframeSource:
314
- """Iterate independent trigger-dataframe tasks."""
314
+ """Iterate independent trigger-dataframe tasks.
315
+
316
+ Rows are read a block at a time rather than one at a time. Asking vaex
317
+ for a single row makes it re-derive and re-hash its whole column set on
318
+ every call: profiling a 100-event unit showed nearly four million hash
319
+ operations, about 39,000 an event, dwarfing the feature extraction
320
+ itself. One read per block removes almost all of it.
321
+ """
322
+
323
+ #: Rows fetched per vaex call. A trigger dataframe row is a few hundred
324
+ #: scalars, so a block of these is small; the win is in the call count.
325
+ CHUNK_ROWS = 2000
315
326
 
316
327
  def __init__(self, trigger_files, selected_streams=None):
317
328
  self._trigger_files = trigger_files or {}
@@ -323,6 +334,12 @@ class TriggerDataframeSource:
323
334
  self._task_id = None
324
335
  self._segments = None
325
336
  self._segment_index = -1
337
+ self._buffer = []
338
+ self._buffer_start = 0
339
+
340
+ def _reset_buffer(self):
341
+ self._buffer = []
342
+ self._buffer_start = 0
326
343
 
327
344
  @property
328
345
  def task_id(self):
@@ -345,6 +362,7 @@ class TriggerDataframeSource:
345
362
  self._dataframe = vx.open_many(self._trigger_files[task_id])
346
363
  self._index = -1
347
364
  self._row_stop = len(self._dataframe)
365
+ self._reset_buffer()
348
366
 
349
367
  def select_segments(self, segments):
350
368
  """Iterate an explicit, ordered list of dataframe row ranges.
@@ -362,6 +380,7 @@ class TriggerDataframeSource:
362
380
  self._segment_index = -1
363
381
  self._index = -1
364
382
  self._row_stop = 0
383
+ self._reset_buffer()
365
384
 
366
385
  def _open_next_segment(self):
367
386
  """Open the next segment's shard. Return False when none remain."""
@@ -381,23 +400,40 @@ class TriggerDataframeSource:
381
400
  )
382
401
  self._index = segment.row_start - 1
383
402
  self._row_stop = segment.row_stop
403
+ self._reset_buffer()
384
404
  return True
385
405
 
406
+ def _fill_buffer(self):
407
+ """Read the next block of rows with a single vaex call."""
408
+ start = self._index
409
+ stop = min(start + self.CHUNK_ROWS, self._row_stop)
410
+ self._buffer = self._dataframe[start:stop].to_records()
411
+ self._buffer_start = start
412
+
413
+ def _buffered_row(self):
414
+ """Return the next row of the open shard, or None at its end."""
415
+ self._index += 1
416
+ if self._index >= self._row_stop:
417
+ return None
418
+
419
+ offset = self._index - self._buffer_start
420
+ if not 0 <= offset < len(self._buffer):
421
+ self._fill_buffer()
422
+ offset = self._index - self._buffer_start
423
+ return self._buffer[offset]
424
+
386
425
  def _next_raw_row(self):
387
426
  """Return the next row of the current selection, before filtering."""
388
427
  if self._segments is None:
389
428
  if self._dataframe is None:
390
429
  return None
391
- self._index += 1
392
- if self._index >= self._row_stop:
393
- return None
394
- return self._dataframe.to_records(index=self._index)
430
+ return self._buffered_row()
395
431
 
396
432
  while True:
397
433
  if self._dataframe is not None:
398
- self._index += 1
399
- if self._index < self._row_stop:
400
- return self._dataframe.to_records(index=self._index)
434
+ row = self._buffered_row()
435
+ if row is not None:
436
+ return row
401
437
  if not self._open_next_segment():
402
438
  return None
403
439
 
@@ -21,15 +21,22 @@ class FeatureErrorHandler:
21
21
  A failed algorithm's features are set to ``FEATURE_ERROR_VALUE``. Output
22
22
  names are registered before processing for built-ins and declared external
23
23
  algorithms. Legacy external names can still be learned on first success.
24
+
25
+ Errors are reported once per algorithm at the end of the worker, not once
26
+ per event: a window or noise-model problem usually repeats for thousands
27
+ of consecutive events and the per-event lines bury everything else. Set
28
+ ``max_logged`` above zero to also log that many individual failures while
29
+ they happen.
24
30
  """
25
31
 
26
- def __init__(self, node_label='', max_logged=10):
32
+ def __init__(self, node_label='', max_logged=0):
27
33
  self._node_label = node_label
28
34
  self._max_logged = max_logged
29
35
  self._output_names = {}
30
36
  self._declared = set()
31
37
  self._pending_rows = {}
32
38
  self._counts = {}
39
+ self._first_errors = {}
33
40
 
34
41
  @property
35
42
  def total(self):
@@ -72,6 +79,13 @@ class FeatureErrorHandler:
72
79
  key = self._key(plan)
73
80
  self._counts[key] = self._counts.get(key, 0) + 1
74
81
 
82
+ # One failing algorithm usually fails for many events in a row, so
83
+ # only the first error of each algorithm is kept; report() prints it
84
+ # once with the total count instead of one line per event.
85
+ self._first_errors.setdefault(
86
+ key, (f'{type(error).__name__}: {error}',
87
+ self._event_label(event_features, event_index)))
88
+
75
89
  if self.total <= self._max_logged:
76
90
  print(f'WARNING{self._node_label}: Feature algorithm "{key}" failed '
77
91
  f'for {self._event_label(event_features, event_index)}: '
@@ -92,6 +106,15 @@ class FeatureErrorHandler:
92
106
  details = ', '.join(f'{key}: {count}' for key, count in self._counts.items())
93
107
  print(f'WARNING{self._node_label}: {self.total} feature algorithm '
94
108
  f'error(s) skipped ({details})')
109
+ # The same problem usually hits several channels on the same event,
110
+ # so identical messages are reported once for all of them.
111
+ grouped = {}
112
+ for key in self._counts:
113
+ grouped.setdefault(self._first_errors[key], []).append(key)
114
+ for (message, event_label), keys in grouped.items():
115
+ who = 'all' if len(keys) == len(self._counts) > 1 else ', '.join(keys)
116
+ print(f'WARNING{self._node_label}: first error ({who}, '
117
+ f'{event_label}): {message}')
95
118
  never_succeeded = sorted(self._pending_rows)
96
119
  if never_succeeded:
97
120
  print(f'WARNING{self._node_label}: Algorithm(s) {never_succeeded} never '
@@ -106,8 +129,13 @@ class FeatureErrorHandler:
106
129
  parts = []
107
130
  if event_features.get('stream_id') is not None:
108
131
  parts.append(f'stream {event_features["stream_id"]}')
109
- parts.extend(f'{field}={event_features[field]}' for field in _EVENT_ID_FIELDS
110
- if event_features.get(field) is not None)
132
+ for field in _EVENT_ID_FIELDS:
133
+ value = event_features.get(field)
134
+ if value is None:
135
+ continue
136
+ if field == 'global_segment_number' and int(value) < 0:
137
+ continue # continuous Zarr has no segment number
138
+ parts.append(f'{field}={value}')
111
139
  if event_index is not None:
112
140
  parts.append(f'worker event #{event_index}')
113
141
  return ', '.join(parts) or 'event'
@@ -38,6 +38,7 @@ from pytesprocess.utils import create_dataframe_group
38
38
  from pytesprocess.utils.dataframe_output import (
39
39
  attach_dataframe_group, export_empty_processing_dataframe,
40
40
  )
41
+ from pytesprocess.utils.shard_output import export_shard
41
42
 
42
43
  warnings.filterwarnings("ignore")
43
44
 
@@ -435,7 +436,9 @@ class FeatureProcessing:
435
436
 
436
437
  # processing done
437
438
  if self._verbose:
438
- print('INFO: Feature processing done!')
439
+ print('INFO: Feature processing done!')
440
+ if output_group_path is not None:
441
+ print(f'INFO: Processing output group path: {output_group_path}')
439
442
 
440
443
 
441
444
  if lgc_output:
@@ -873,9 +876,9 @@ class FeatureProcessing:
873
876
  vx.settings.main.thread_count_io = 1
874
877
  pa.set_cpu_count(1)
875
878
  feature_vx = vx.from_pandas(feature_df, copy_index=False)
876
- feature_vx.export_hdf5(partial_file, mode='w')
879
+ export_shard(feature_vx, output_file,
880
+ verbose=self._verbose)
877
881
  feature_vx.close()
878
- os.replace(partial_file, output_file)
879
882
  del feature_df, feature_vx
880
883
  gc.collect()
881
884
  feature_df = pd.DataFrame()
@@ -26,6 +26,7 @@ from pytesprocess.process.work_plan import (
26
26
  )
27
27
  from pytesprocess.utils import create_dataframe_group
28
28
  from pytesprocess.utils.dataframe_output import attach_dataframe_group
29
+ from pytesprocess.utils.shard_output import export_shard
29
30
  from pytesprocess.utils.event_metadata import coordinate_metadata, metadata_value, raw_provenance
30
31
 
31
32
 
@@ -473,6 +474,11 @@ class Randoms:
473
474
  "INFO: Randoms acquisition done! "
474
475
  f"Generated {target_nrandoms} randoms."
475
476
  )
477
+ if output_path is not None:
478
+ print(
479
+ "INFO: Processing output acquisition path: "
480
+ f"{output_path}"
481
+ )
476
482
 
477
483
  if lgc_output:
478
484
  return output_df
@@ -895,7 +901,8 @@ class Randoms:
895
901
 
896
902
  def _save_dataframe(self, dataframe, *, dataframe_group):
897
903
  """Save the generated random-trigger dataframe as one F0001 shard."""
898
- dataframe.export_hdf5(dataframe_group.file_path(1), mode="w")
904
+ export_shard(dataframe, dataframe_group.file_path(1),
905
+ verbose=self._verbose)
899
906
 
900
907
  def _initialize_feature_dict(self, *, include_hdf5, include_partition):
901
908
  feature_dict = {
@@ -27,6 +27,7 @@ from pytesprocess.process.work_plan import (
27
27
  count_rows, find_unit, load_plan, utc_now, write_plan, write_unit_status,
28
28
  )
29
29
  from pytesprocess.utils.event_metadata import coordinate_metadata
30
+ from pytesprocess.utils.shard_output import export_shard
30
31
  from pytesprocess.utils.dataframe_output import (
31
32
  attach_dataframe_group,
32
33
  empty_processing_dataframe, export_empty_processing_dataframe,
@@ -387,7 +388,9 @@ class TriggerProcessing:
387
388
 
388
389
  # processing done
389
390
  if self._verbose:
390
- print('INFO: Trigger processing done!')
391
+ print('INFO: Trigger processing done!')
392
+ if output_group_path is not None:
393
+ print(f'INFO: Processing output group path: {output_group_path}')
391
394
 
392
395
  if lgc_output:
393
396
  return output_df
@@ -909,17 +912,9 @@ class TriggerProcessing:
909
912
  export_empty_processing_dataframe(partial_file, dataframe_metadata)
910
913
  os.replace(partial_file, output_file)
911
914
  elif lgc_save:
912
- try:
913
- process_df.export_hdf5(partial_file, mode='w')
914
- process_df.close()
915
- except Exception as e:
916
- print('WARNING: Export failed with error: ', e)
917
- print('Will try again...')
918
- df_pandas = process_df.to_pandas_df()
919
- process_df = vx.from_pandas(df_pandas, copy_index=False)
920
- process_df.export_hdf5(partial_file, mode='w')
921
- process_df.close()
922
- os.replace(partial_file, output_file)
915
+ export_shard(process_df, output_file,
916
+ verbose=self._verbose)
917
+ process_df.close()
923
918
  del process_df
924
919
  process_df = None
925
920
 
@@ -0,0 +1,94 @@
1
+ """Writing one dataframe shard, atomically, on filesystems that resist it.
2
+
3
+ Two problems are handled here, and they are unrelated to each other.
4
+
5
+ Vaex's HDF5 writer memory-maps its output file with ``PROT_WRITE``. Some
6
+ shared filesystems refuse that: NERSC's CFS, reached from a compute node,
7
+ raises ``OSError(ENOSYS)``, and there is no vaex option to avoid the mmap.
8
+ When that happens the shard is written somewhere local instead and copied
9
+ into place, because an ordinary sequential write works where mmap does not.
10
+
11
+ Separately, a dataframe vaex cannot write directly sometimes survives a
12
+ round trip through pandas. That fallback predates this module and is kept.
13
+
14
+ Either way the shard reaches its final name by rename, so a job killed
15
+ mid-write never leaves a truncated file that looks complete.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from pathlib import Path
22
+ import shutil
23
+ import tempfile
24
+
25
+
26
+ __all__ = ['STAGING_ENV', 'export_shard', 'staging_directory']
27
+
28
+
29
+ STAGING_ENV = 'PYTESPROCESS_STAGING_DIR'
30
+
31
+
32
+ def staging_directory():
33
+ """Where to write a shard the destination filesystem cannot mmap.
34
+
35
+ Defaults to whatever the batch system set as the job's temporary
36
+ directory, which on a cluster is normally node-local.
37
+ """
38
+ return os.environ.get(STAGING_ENV) or os.environ.get('TMPDIR') or None
39
+
40
+
41
+ def export_shard(dataframe, output_file, verbose=False):
42
+ """Write ``dataframe`` to ``output_file``, atomically.
43
+
44
+ Return
45
+ ------
46
+ output_file : str
47
+ """
48
+ output_file = str(output_file)
49
+ partial = output_file + '.partial'
50
+
51
+ try:
52
+ _export(dataframe, partial, verbose=verbose)
53
+ except Exception as error:
54
+ if verbose:
55
+ print(f'WARNING: Export failed with error: {error}')
56
+ print('Will try again via pandas...')
57
+ import vaex as vx
58
+
59
+ rebuilt = vx.from_pandas(dataframe.to_pandas_df(), copy_index=False)
60
+ try:
61
+ _export(rebuilt, partial, verbose=verbose)
62
+ finally:
63
+ rebuilt.close()
64
+
65
+ os.replace(partial, output_file)
66
+ return output_file
67
+
68
+
69
+ def _export(dataframe, partial, verbose=False):
70
+ """Export to ``partial``, staging locally if the destination cannot mmap."""
71
+ try:
72
+ dataframe.export_hdf5(partial, mode='w')
73
+ return
74
+ except OSError as error:
75
+ if verbose:
76
+ print(f'INFO: Direct export failed ({error}); this filesystem '
77
+ 'does not support the memory mapping vaex writes through. '
78
+ 'Writing locally and copying in.')
79
+ _discard(partial)
80
+
81
+ directory = tempfile.mkdtemp(prefix='pytesprocess-', dir=staging_directory())
82
+ try:
83
+ staged = os.path.join(directory, Path(partial).name)
84
+ dataframe.export_hdf5(staged, mode='w')
85
+ shutil.copyfile(staged, partial)
86
+ finally:
87
+ shutil.rmtree(directory, ignore_errors=True)
88
+
89
+
90
+ def _discard(path):
91
+ try:
92
+ os.remove(path)
93
+ except OSError:
94
+ pass
@@ -8,6 +8,7 @@ import vaex as vx
8
8
  from pytesprocess.config import ProcessingConfig
9
9
  from pytesprocess.core import FilterData
10
10
  from pytesprocess.salting import SaltGenerator
11
+ from pytesprocess.utils.shard_output import export_shard
11
12
  from pytesprocess.utils import (
12
13
  get_trigger_template_info, create_dataframe_group,
13
14
  add_dataframe_group_columns,
@@ -127,7 +128,7 @@ def generate_salting_dataframes(
127
128
  )
128
129
  add_dataframe_group_columns(final, group, 1)
129
130
  file_name = group.file_path(1)
130
- final.export_hdf5(file_name, mode="w")
131
+ export_shard(final, file_name)
131
132
  results.append(file_name)
132
133
 
133
134
  return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytesprocess
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Detector data processing for pytesdaqx acquisitions
5
5
  Author-email: Bruno Serfass <serfass@berkeley.edu>, Samuel Watkins <samwatkins@berkeley.edu>
6
6
  License: MIT License
@@ -35,7 +35,7 @@ Requires-Dist: numpy>=1.26
35
35
  Requires-Dist: scipy
36
36
  Requires-Dist: matplotlib
37
37
  Requires-Dist: PyYAML
38
- Requires-Dist: qetpy>=1.8.6
38
+ Requires-Dist: qetpy>=1.8.9
39
39
  Requires-Dist: pandas
40
40
  Requires-Dist: tables
41
41
  Requires-Dist: pytesdaqx
@@ -76,6 +76,7 @@ pytesprocess/utils/dataframe_output.py
76
76
  pytesprocess/utils/downsampling.py
77
77
  pytesprocess/utils/event_metadata.py
78
78
  pytesprocess/utils/filter_hdf5.py
79
+ pytesprocess/utils/shard_output.py
79
80
  pytesprocess/utils/utils.py
80
81
  pytesprocess/vibration/__init__.py
81
82
  pytesprocess/vibration/analyzer.py
@@ -113,6 +114,7 @@ tests/test_processing_data_v15.py
113
114
  tests/test_processing_guards.py
114
115
  tests/test_randoms_work_units.py
115
116
  tests/test_restricted_processing.py
117
+ tests/test_shard_output.py
116
118
  tests/test_signed_feature_windows.py
117
119
  tests/test_template_metadata.py
118
120
  tests/test_trigger_alignment.py
@@ -2,7 +2,7 @@ numpy>=1.26
2
2
  scipy
3
3
  matplotlib
4
4
  PyYAML
5
- qetpy>=1.8.6
5
+ qetpy>=1.8.9
6
6
  pandas
7
7
  tables
8
8
  pytesdaqx
@@ -160,8 +160,12 @@ def test_skip_fills_failed_features_and_counts_errors(tmp_path, monkeypatch, cap
160
160
  assert list(df['feature_error_count'].values) == [2, 2, 0, 0, 2, 0]
161
161
  assert df['maximum_Z1P1'].notna().all()
162
162
 
163
- assert 'Z1P1/flaky" failed for stream I1_D20260101_T000000, event_number=4' in output
164
163
  assert '6 feature algorithm error(s) skipped (Z1P1/flaky: 3, Z1P1/other_flaky: 3)' in output
164
+ # both algorithms fail the same way, so the reason is reported once
165
+ assert ('first error (all, stream I1_D20260101_T000000, '
166
+ 'event_number=0') in output
167
+ assert 'RuntimeError: bad event 0' in output
168
+ assert 'bad event 4' not in output
165
169
 
166
170
 
167
171
  def test_skip_only_for_configured_algorithm(tmp_path, monkeypatch):
@@ -183,19 +187,58 @@ def test_no_error_column_without_skip(tmp_path, monkeypatch):
183
187
  assert 'feature_error_count' not in df.columns
184
188
 
185
189
 
186
- def test_handler_limits_logged_errors_and_reports_never_succeeded(capsys):
187
- class Plan:
188
- channel, name = 'Z1P1', 'always_fails'
190
+ class _AlwaysFails:
191
+ channel, name = 'Z1P1', 'always_fails'
189
192
 
190
- handler = FeatureErrorHandler(node_label=' Node #1', max_logged=3)
191
- rows = [{'stream_id': 's'} for _ in range(5)]
193
+
194
+ class _AlsoFails:
195
+ channel, name = 'Z2P1', 'also_fails'
196
+
197
+
198
+ def test_repeated_failures_report_once_per_algorithm(capsys):
199
+ """Thousands of identical failures must not produce thousands of lines."""
200
+ handler = FeatureErrorHandler(node_label=' Node #1')
201
+ rows = [{'stream_id': 's', 'trigger_index': index} for index in range(500)]
192
202
  for index, row in enumerate(rows):
193
- handler.record_failure(Plan, row, ValueError('boom'), event_index=index)
203
+ handler.record_failure(_AlwaysFails, row, ValueError(f'boom {index}'),
204
+ event_index=index)
205
+ handler.report()
206
+ output = capsys.readouterr().out
207
+
208
+ # nothing is printed per event, only the end-of-worker report
209
+ assert '" failed for' not in output
210
+ assert '500 feature algorithm error(s) skipped (Z1P1/always_fails: 500)' in output
211
+ # the first error is still shown once, with the event that produced it
212
+ assert 'first error (Z1P1/always_fails, ' in output
213
+ assert 'trigger_index=0, worker event #0' in output
214
+ assert 'ValueError: boom 0' in output
215
+ assert 'boom 1' not in output
216
+ assert len(output.strip().splitlines()) == 3 # summary, detail, never-succeeded
217
+ assert "['Z1P1/always_fails'] never succeeded" in output
218
+ assert rows[0] == {'stream_id': 's', 'trigger_index': 0}
219
+
220
+
221
+ def test_each_failing_algorithm_is_reported_separately(capsys):
222
+ handler = FeatureErrorHandler(node_label=' Node #1')
223
+ handler.record_failure(_AlwaysFails, {'stream_id': 's'}, ValueError('bad window'))
224
+ handler.record_failure(_AlsoFails, {'stream_id': 's'}, KeyError('no csd'))
225
+ handler.record_failure(_AlsoFails, {'stream_id': 's'}, KeyError('no csd'))
226
+ handler.report()
227
+ output = capsys.readouterr().out
228
+
229
+ assert '3 feature algorithm error(s) skipped (Z1P1/always_fails: 1, Z2P1/also_fails: 2)' in output
230
+ assert 'first error (Z1P1/always_fails, stream s): ValueError: bad window' in output
231
+ assert "first error (Z2P1/also_fails, stream s): KeyError: 'no csd'" in output
232
+
233
+
234
+ def test_max_logged_restores_per_event_lines(capsys):
235
+ handler = FeatureErrorHandler(node_label=' Node #1', max_logged=3)
236
+ for index in range(5):
237
+ handler.record_failure(_AlwaysFails, {'stream_id': 's'}, ValueError('boom'),
238
+ event_index=index)
194
239
  handler.report()
195
240
  output = capsys.readouterr().out
196
241
 
197
242
  assert output.count('" failed for') == 3
198
243
  assert 'Further feature algorithm errors are counted but not displayed' in output
199
244
  assert '5 feature algorithm error(s) skipped (Z1P1/always_fails: 5)' in output
200
- assert "['Z1P1/always_fails'] never succeeded" in output
201
- assert rows == [{'stream_id': 's'}] * 5
@@ -0,0 +1,151 @@
1
+ """Writing a shard where the destination filesystem will not be memory-mapped.
2
+
3
+ Vaex's HDF5 writer mmaps its output with PROT_WRITE. NERSC's CFS refuses
4
+ that from a compute node with OSError(ENOSYS), so the writer has to be able
5
+ to produce the file somewhere else and copy it in.
6
+ """
7
+
8
+ import errno
9
+ import os
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ import pytest
14
+ import vaex as vx
15
+
16
+ from pytesprocess.utils.shard_output import (
17
+ STAGING_ENV,
18
+ export_shard,
19
+ staging_directory,
20
+ )
21
+
22
+
23
+ @pytest.fixture
24
+ def frame():
25
+ return vx.from_arrays(
26
+ event_number=np.arange(5, dtype=np.int64),
27
+ value=np.linspace(0.0, 1.0, 5),
28
+ )
29
+
30
+
31
+ def read_back(path):
32
+ frame = vx.open(str(path))
33
+ try:
34
+ return frame.event_number.tolist()
35
+ finally:
36
+ frame.close()
37
+
38
+
39
+ def refusing_export(refuse_under, refused):
40
+ """Stand in for a filesystem that will not be mmapped, like CFS.
41
+
42
+ A plain function, so monkeypatch binds it as a method and the dataframe
43
+ still arrives as self.
44
+ """
45
+ real = vx.dataframe.DataFrameLocal.export_hdf5
46
+
47
+ def export(self, path, *args, **kwargs):
48
+ if str(path).startswith(str(refuse_under)):
49
+ refused.append(str(path))
50
+ raise OSError(errno.ENOSYS, "Function not implemented")
51
+ return real(self, path, *args, **kwargs)
52
+
53
+ return export
54
+
55
+
56
+ def test_a_shard_is_written_and_readable(tmp_path, frame):
57
+ output = tmp_path / 'trigger_F0001.hdf5'
58
+
59
+ export_shard(frame, output)
60
+
61
+ assert read_back(output) == [0, 1, 2, 3, 4]
62
+
63
+
64
+ def test_no_partial_survives_a_successful_write(tmp_path, frame):
65
+ export_shard(frame, tmp_path / 'out.hdf5')
66
+
67
+ assert list(tmp_path.glob('*.partial')) == []
68
+
69
+
70
+ def test_the_shard_appears_only_when_complete(tmp_path, frame, monkeypatch):
71
+ """A killed job must not leave a file that looks finished."""
72
+ output = tmp_path / 'out.hdf5'
73
+ seen = {}
74
+
75
+ real = vx.dataframe.DataFrameLocal.export_hdf5
76
+
77
+ def export(self, path, *args, **kwargs):
78
+ result = real(self, path, *args, **kwargs)
79
+ # Once vaex has written, but before the rename.
80
+ seen['during'] = sorted(p.name for p in tmp_path.iterdir())
81
+ return result
82
+
83
+ monkeypatch.setattr(vx.dataframe.DataFrameLocal, 'export_hdf5', export)
84
+ export_shard(frame, output)
85
+
86
+ assert seen['during'] == ['out.hdf5.partial'], \
87
+ 'the final name must not exist until the shard is complete'
88
+ assert output.is_file()
89
+
90
+
91
+ def test_a_destination_that_cannot_mmap_is_staged(tmp_path, frame, monkeypatch):
92
+ destination = tmp_path / 'cfs'
93
+ destination.mkdir()
94
+ staging = tmp_path / 'local'
95
+ staging.mkdir()
96
+ monkeypatch.setenv(STAGING_ENV, str(staging))
97
+
98
+ refused = []
99
+ monkeypatch.setattr(vx.dataframe.DataFrameLocal, 'export_hdf5',
100
+ refusing_export(destination, refused))
101
+
102
+ output = destination / 'out.hdf5'
103
+ export_shard(frame, output)
104
+
105
+ assert refused, 'the direct write should have been attempted first'
106
+ assert read_back(output) == [0, 1, 2, 3, 4]
107
+
108
+
109
+ def test_staging_leaves_nothing_behind(tmp_path, frame, monkeypatch):
110
+ destination = tmp_path / 'cfs'
111
+ destination.mkdir()
112
+ staging = tmp_path / 'local'
113
+ staging.mkdir()
114
+ monkeypatch.setenv(STAGING_ENV, str(staging))
115
+ monkeypatch.setattr(vx.dataframe.DataFrameLocal, 'export_hdf5',
116
+ refusing_export(destination, []))
117
+
118
+ export_shard(frame, destination / 'out.hdf5')
119
+
120
+ assert list(staging.iterdir()) == []
121
+ assert sorted(p.name for p in destination.iterdir()) == ['out.hdf5']
122
+
123
+
124
+ def test_staging_directory_follows_the_environment(monkeypatch):
125
+ monkeypatch.delenv(STAGING_ENV, raising=False)
126
+ monkeypatch.setenv('TMPDIR', '/scratch/job')
127
+ assert staging_directory() == '/scratch/job'
128
+
129
+ monkeypatch.setenv(STAGING_ENV, '/node/local')
130
+ assert staging_directory() == '/node/local'
131
+
132
+
133
+ def test_a_dataframe_vaex_cannot_write_goes_via_pandas(tmp_path, frame,
134
+ monkeypatch):
135
+ """The older fallback, kept for a different class of failure."""
136
+ calls = []
137
+ real = vx.dataframe.DataFrameLocal.export_hdf5
138
+
139
+ def export(self, path, *args, **kwargs):
140
+ calls.append(str(path))
141
+ if len(calls) == 1:
142
+ raise ValueError('cannot export column of type')
143
+ return real(self, path, *args, **kwargs)
144
+
145
+ monkeypatch.setattr(vx.dataframe.DataFrameLocal, 'export_hdf5', export)
146
+ output = tmp_path / 'out.hdf5'
147
+
148
+ export_shard(frame, output)
149
+
150
+ assert len(calls) == 2, 'the pandas round trip should have retried'
151
+ assert read_back(output) == [0, 1, 2, 3, 4]
File without changes
File without changes
File without changes