Skip to content

Commit b0b2fa0

Browse files
authored
Merge pull request #727 from European-XFEL/display-inst-data-counts
lsxfel / .info() option to show instrument data counts
2 parents 847c971 + 0dd24c4 commit b0b2fa0

6 files changed

Lines changed: 122 additions & 49 deletions

File tree

docs/cli.rst

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,39 @@ file:
2222
2323
.. program:: lsxfel
2424

25+
The options below are only relevant when inspecting a single run or file.
26+
27+
.. option:: --source <source-pattern>, -s <source-pattern>
28+
29+
Select which sources to show, using a pattern like ``*/BAM*:output`` or
30+
a substring like ``BAM``. Can be used several times to select different
31+
patterns.
32+
2533
.. option:: --detail <source-pattern>
2634

27-
Show more detail on the keys and data of the sources selected by a pattern
28-
like ``*/XGM/*``. Only applies when inspecting a single run or file.
29-
Can be used several times to select different patterns.
35+
Show more detail on the keys and data of the sources selected by a pattern,
36+
in the same formats as ``--source``. Can be used several times.
3037

3138
This option can make ``lsxfel`` considerably slower.
3239

40+
.. option:: --counts
41+
42+
Show data counts for instrument sources.
43+
44+
.. option:: --no-group
45+
46+
Don't try to group similar source names; show each individually.
47+
3348
.. option:: --aggregators, -g
3449

3550
Show the data aggregator in which each source is saved. This option
3651
can make ``lsxfel`` considerably slower.
3752

53+
.. option:: --auxiliary, -x
54+
55+
Include auxiliary sources alongside regular sources. This can make ``lsxfel``
56+
slower.
57+
3858
.. _cmd-validate:
3959

4060
``extra-data-validate``

extra_data/cli/lsxfel.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,7 @@
88
import sys
99

1010
from ..read_machinery import FilenameInfo
11-
from ..reader import H5File, RunDirectory
12-
13-
14-
def describe_file(path, details_for_sources=(), with_aggregators=False,
15-
with_auxiliary=False, group_sources=True):
16-
"""Describe a single HDF5 data file"""
17-
basename = os.path.basename(path)
18-
print(basename, ": Data file")
19-
20-
h5file = H5File(path)
21-
h5file.info(details_for_sources, with_aggregators, with_auxiliary, group_sources)
11+
from ..reader import DataCollection, H5File, RunDirectory
2212

2313

2414
def summarise_file(path):
@@ -29,16 +19,6 @@ def summarise_file(path):
2919
print(f" {len(f.train_ids)} trains, {len(f.all_sources)} sources")
3020

3121

32-
def describe_run(path, details_for_sources=(), with_aggregators=False,
33-
with_auxiliary=False, group_sources=True):
34-
basename = os.path.basename(path)
35-
print(basename, ": Run directory")
36-
print()
37-
38-
run = RunDirectory(path)
39-
run.info(details_for_sources, with_aggregators, with_auxiliary, group_sources)
40-
41-
4222
def summarise_run(path, indent=''):
4323
basename = os.path.basename(path)
4424

@@ -74,6 +54,10 @@ def main(argv=None):
7454
prog='lsxfel', description="Summarise XFEL data in files or folders"
7555
)
7656
ap.add_argument('paths', nargs='*', help="Files/folders to look at")
57+
ap.add_argument('--source', '-s', action='append', default=[],
58+
help="Filter the list of sources, with wildcard patterns like "
59+
"'*XGM/*:output' or partial matches like 'XGM'. Can be used more "
60+
"than once.")
7761
ap.add_argument('--detail', '-d', action='append', default=[],
7862
help="Show details on keys & data for specified sources. "
7963
"This can slow down lsxfel considerably. "
@@ -82,6 +66,10 @@ def main(argv=None):
8266
"Can be used more than once to include several patterns. "
8367
"Only used when inspecting a single run or file."
8468
)
69+
ap.add_argument('--counts', '-c', action='store_true',
70+
help="Show data counts for instrument sources.")
71+
ap.add_argument('--no-group', action='store_true',
72+
help="Don't try to group similar source names; show each individually.")
8573
ap.add_argument('--aggregators', '-g', action='store_true',
8674
help="Include the aggregator each source is saved in. "
8775
"Only used when inspecting a single run or file and can slow "
@@ -90,16 +78,32 @@ def main(argv=None):
9078
help="Include auxiliary sources alongside regular sources. "
9179
"Only used when inspecting a single run or file and can slow "
9280
"down lsxfel.")
93-
ap.add_argument('--no-group', action='store_true',
94-
help="Don't try to group similar source names; show each individually.")
9581
args = ap.parse_args(argv)
9682
paths = args.paths or [os.path.abspath(os.getcwd())]
9783

9884
# If --detail doesn't contain glob wildcards, treat it as a substring
9985
detail_patterns = [p if re.search(r'[*?[]', p) else f'*{p}*'
10086
for p in args.detail]
101-
102-
group_sources = not args.no_group
87+
source_patterns = [p if re.search(r'[*?[]', p) else f'*{p}*'
88+
for p in args.source]
89+
90+
def dc_info(dc: DataCollection):
91+
if source_patterns:
92+
try:
93+
sel = dc.select(source_patterns)
94+
except ValueError as e: # No matching sources
95+
sys.exit(str(e))
96+
print(f"Showing {len(sel.all_sources)} / {len(dc.all_sources)} sources")
97+
else:
98+
sel = dc
99+
print()
100+
sel.info(
101+
detail_patterns,
102+
counts=args.counts,
103+
group_sources=(not args.no_group),
104+
with_aggregators=args.aggregators,
105+
with_auxiliary=args.auxiliary,
106+
)
103107

104108
if len(paths) == 1:
105109
path = paths[0]
@@ -109,8 +113,8 @@ def main(argv=None):
109113
contents = sorted(os.listdir(path))
110114
if any(f.endswith('.h5') for f in contents):
111115
# Run directory
112-
describe_run(path, detail_patterns, args.aggregators,
113-
args.auxiliary, group_sources=group_sources)
116+
print(basename, ": Run directory")
117+
dc_info(RunDirectory(path))
114118
elif any(re.match(r'r\d+', f) for f in contents):
115119
# Proposal directory, containing runs
116120
print(basename, ": Proposal data directory")
@@ -131,8 +135,8 @@ def main(argv=None):
131135
print(basename, ": Unrecognised directory")
132136
elif os.path.isfile(path):
133137
if path.endswith('.h5'):
134-
describe_file(path, detail_patterns, args.aggregators,
135-
args.auxiliary, group_sources=group_sources)
138+
print(basename, ": Data file")
139+
dc_info(H5File(path))
136140
else:
137141
print(basename, ": Unrecognised file")
138142
return 2

extra_data/display.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
class InfoPrinter:
1414
def __init__(
1515
self, dc: DataCollection, details_for_sources=(), with_aggregators=False,
16-
group_sources=True,
16+
data_counts=False, group_sources=True,
1717
):
1818
self.dc = dc
1919
self.details_for_sources = details_for_sources
2020
self.details_sources_re = [re.compile(fnmatch.translate(p))
2121
for p in details_for_sources]
2222
self.with_aggregators = with_aggregators
23-
self.group_sources = group_sources and not with_aggregators
23+
self.data_counts = data_counts
24+
self.group_sources = group_sources and not (with_aggregators or data_counts)
2425

2526
# Invert aliases for faster lookup.
2627
self.src_aliases = defaultdict(set)
@@ -92,7 +93,7 @@ def source_line(self, s):
9293
agg_str = f" [{self.dc[s].aggregator}]" if self.with_aggregators else ""
9394
print(f" -{agg_str} {s} {self.src_alias_list(s)}")
9495

95-
def list_sources(self, srcs: list, detail: Callable):
96+
def list_sources(self, srcs: list, detail: Callable, data_counts=False):
9697
current_group = SourceGroup()
9798
def flush_group():
9899
nonlocal current_group
@@ -108,6 +109,8 @@ def flush_group():
108109
self.source_line(s)
109110
if show_detail:
110111
detail(s)
112+
elif data_counts:
113+
self.inst_data(s)
111114
else:
112115
if not current_group.add(s):
113116
flush_group()
@@ -116,7 +119,7 @@ def flush_group():
116119

117120
def inst_sources(self):
118121
srcs = self.dc.instrument_sources
119-
if self.details_sources_re:
122+
if (self.details_sources_re or self.data_counts):
120123
# All instrument sources with details enabled.
121124
displayed_inst_srcs = srcs - self.dc.legacy_sources.keys()
122125
print(len(displayed_inst_srcs), "instrument sources:")
@@ -127,17 +130,28 @@ def inst_sources(self):
127130
)
128131
print(len(displayed_inst_srcs), "instrument sources (excluding XTDF detectors):")
129132

130-
self.list_sources(displayed_inst_srcs, self.inst_detail)
133+
self.list_sources(displayed_inst_srcs, self.inst_detail, self.data_counts)
131134
print()
132135

136+
def inst_data(self, s):
137+
# Show data volume, without list of keys
138+
sd = self.dc[s]
139+
sif = SourceInfoFormatter(self.dc[s])
140+
index_groups = sorted(sd.index_groups)
141+
if len(index_groups) == 1:
142+
print(f" {sif.src_data_detail(index_groups[0])}")
143+
else:
144+
for group in index_groups:
145+
print(f" {group}: {sif.src_data_detail(group)}")
146+
133147
def inst_detail(self, s):
134148
# Detail for instrument sources:
135149
sd = self.dc[s]
136-
sif = SourceInfoFormatter(self.dc[s], self.srckey_aliases[s])
150+
sif = SourceInfoFormatter(sd, self.srckey_aliases[s])
137151
for group, keys in groupby(sorted(sd.keys()), key=lambda k: k.split(".")[0]):
138152
print(f" - {group}:")
139153
keys = list(keys)
140-
print(" " + sif.src_data_detail(keys))
154+
print(" " + sif.src_data_detail(group))
141155
for l in sif.keys_detail(keys):
142156
print(" " + l)
143157

@@ -182,7 +196,7 @@ def legacy_sources(self):
182196

183197
def show(self, with_auxiliary=False):
184198
self.trains()
185-
if not self.details_sources_re:
199+
if not (self.details_sources_re or self.data_counts):
186200
self.xtdf()
187201
self.inst_sources()
188202
self.ctrl_sources()
@@ -308,16 +322,17 @@ def __init__(self, src: SourceData, key_aliases=None):
308322
self.src = src
309323
self.key_aliases = key_aliases or {}
310324

311-
def src_data_detail(self, keys):
325+
def src_data_detail(self, index_group):
312326
"""Detail for how much data is present for an instrument group"""
313-
if not keys:
314-
return
315-
counts = self.src[list(keys)[0]].data_counts()
316-
ntrains_data = (counts > 0).sum()
327+
counts = self.src.data_counts(index_group=index_group)
328+
counts = counts[counts > 0]
329+
ntrains_data = len(counts)
330+
cmin, cmax = (counts.min(), counts.max()) if ntrains_data else (0, 0)
331+
count_range = f"{cmin}" if (cmin == cmax) else f"{cmin}{cmax}"
317332
return (
318333
f'data for {ntrains_data} trains '
319334
f'({ntrains_data / len(self.src.train_ids):.2%}), '
320-
f'up to {counts.max()} entries per train'
335+
f'{count_range} entries per train'
321336
)
322337

323338
def keys_detail(self, keys=None):

extra_data/reader.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,14 +1304,30 @@ def __repr__(self):
13041304
return f"<extra_data.DataCollection for {len(self.all_sources)} " \
13051305
f"sources and {len(self.train_ids)} trains>"
13061306

1307-
def info(self, details_for_sources=(), with_aggregators=False,
1308-
with_auxiliary=False, group_sources=True):
1309-
"""Show information about the selected data."""
1307+
def info(self, details_for_sources=(), *, counts=False, group_sources=True,
1308+
with_aggregators=False, with_auxiliary=False):
1309+
"""Show information about the selected data.
1310+
1311+
Parameters
1312+
----------
1313+
1314+
details_for_sources: list of str
1315+
Glob patterns selecting sources to show additional information.
1316+
counts: bool
1317+
Show data counts for all instrument sources.
1318+
group_sources: bool
1319+
Group similar source names together if possible (on by default).
1320+
with_aggregators: bool
1321+
Show which data aggregator each source was saved by.
1322+
with_auxiliary: bool
1323+
Show auxiliary sources (REDUCTION & ERRATA)
1324+
"""
13101325
from .display import InfoPrinter
13111326
InfoPrinter(
13121327
self,
13131328
details_for_sources=details_for_sources,
13141329
with_aggregators=with_aggregators,
1330+
data_counts=counts,
13151331
group_sources=group_sources,
13161332
).show(with_auxiliary)
13171333

extra_data/tests/test_display.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,11 @@ def test_show_ungrouped(mock_fxe_jungfrau_run, capsys):
109109
out, err = capsys.readouterr()
110110
assert "FXE_XAD_JF1M/DET/JNGFR{01-02}" not in out
111111
assert "FXE_XAD_JF1M/DET/JNGFR01" in out
112+
113+
114+
def test_show_counts(mock_fxe_raw_run, capsys):
115+
run = RunDirectory(mock_fxe_raw_run)
116+
run.info(counts=True)
117+
out, err = capsys.readouterr()
118+
assert "data for 0 trains" in out # FXE_XAD_GEC/CAM/CAMERA_NODATA
119+
assert "data for 480 trains" in out

extra_data/tests/test_lsxfel.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
from extra_data.cli import lsxfel
24

35

@@ -13,3 +15,11 @@ def test_lsxfel_run(mock_fxe_raw_run, capsys):
1315

1416
assert "480 trains" in out
1517
assert "16 detector files" in out
18+
19+
20+
def test_lsxfel_main(mock_fxe_raw_run, capsys):
21+
lsxfel.main([mock_fxe_raw_run, "--source", "XGM"])
22+
out, err = capsys.readouterr()
23+
assert re.search(r"trains:\s*480", out)
24+
assert "SA1_XTD2_XGM/DOOCS/MAIN:output" in out
25+
assert "FXE_XAD_GEC/CAM/CAMERA" not in out # Selected out by --source

0 commit comments

Comments
 (0)