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.
- {pytesprocess-0.1.2/pytesprocess.egg-info → pytesprocess-0.1.3}/PKG-INFO +2 -2
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pyproject.toml +1 -1
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/_version.py +1 -1
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/data_source.py +44 -8
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_errors.py +31 -3
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/features.py +6 -3
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/randoms.py +8 -1
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/triggers.py +7 -12
- pytesprocess-0.1.3/pytesprocess/utils/shard_output.py +94 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/workflows/salting.py +2 -1
- {pytesprocess-0.1.2 → pytesprocess-0.1.3/pytesprocess.egg-info}/PKG-INFO +2 -2
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/SOURCES.txt +2 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/requires.txt +1 -1
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_on_error.py +52 -9
- pytesprocess-0.1.3/tests/test_shard_output.py +151 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/LICENSE +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/README.md +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/event.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/feature.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/filter.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/ivsweep.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/randoms.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/trigger.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/commands/vibration.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/common.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/cli/main.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/loader.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/manager.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/common.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/feature.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/salting.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/trigger.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/resolvers/vibration.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/selectors.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/validation.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/config/warnings.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/algorithms.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/didv.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/eventbuilder.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/filter_metadata.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/filterbuilder.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/filterdata.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/ivsweep.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/noise.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/noisemodel.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/oftrigger.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/template.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/core/trigger_utils.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/event_context.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_memory.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_plan.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_resources.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_schema.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_windows.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/feature_work.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/filterprocess.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/ivprocess.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/processing_data.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/randoms_work.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/trigger_partitions.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/trigger_work.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/process/work_plan.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/generator.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/injector.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/salting/sampling.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/arg_utils.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/channels.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/dataframe_output.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/downsampling.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/event_metadata.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/filter_hdf5.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/utils/utils.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/analyzer.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/estimators.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/plotting.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/vibration/processing.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/workflows/__init__.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess/workflows/processing.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/dependency_links.txt +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/entry_points.txt +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/pytesprocess.egg-info/top_level.txt +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/setup.cfg +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_channel_expressions.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_cli_exit_status.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_cli_steps_v16.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_config_resolvers.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_dataframe_output.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_downsampled_filters.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_downsampling.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_empty_and_selected_features.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_eventbuilder_coincidence.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_downsampling.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_memory.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_schema.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_tasks.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_unit_execution.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_window_errors.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_feature_work_units.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_fft_bin.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_filter_compatibility.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_filter_hdf5.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_filter_metadata.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_memory_limit_per_worker.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_metadata_roundtrip.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_native_features.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_processing_data_v15.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_processing_guards.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_randoms_work_units.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_restricted_processing.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_signed_feature_windows.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_template_metadata.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_alignment.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_downsampling.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_filter_reuse.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_partition_groups.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_trigger_work_units.py +0 -0
- {pytesprocess-0.1.2 → pytesprocess-0.1.3}/tests/test_vibration_estimators.py +0 -0
- {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.
|
|
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.
|
|
38
|
+
Requires-Dist: qetpy>=1.8.9
|
|
39
39
|
Requires-Dist: pandas
|
|
40
40
|
Requires-Dist: tables
|
|
41
41
|
Requires-Dist: pytesdaqx
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# special file for defining the current version of the package
|
|
2
|
-
__version__ = "0.1.
|
|
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.
|
|
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.
|
|
399
|
-
if
|
|
400
|
-
return
|
|
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=
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
187
|
-
|
|
188
|
-
channel, name = 'Z1P1', 'always_fails'
|
|
190
|
+
class _AlwaysFails:
|
|
191
|
+
channel, name = 'Z1P1', 'always_fails'
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|