cosmic-metrics 0.1.1__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.
- cosmic_metrics-0.1.1/PKG-INFO +178 -0
- cosmic_metrics-0.1.1/README.md +135 -0
- cosmic_metrics-0.1.1/pyproject.toml +72 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/__init__.py +12 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/analysis.py +266 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/blocks.py +244 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/cache.py +398 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/cli.py +369 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/dataset.py +348 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/metrics/__init__.py +19 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/metrics/base.py +73 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/metrics/iono.py +87 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/metrics/pspec.py +204 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/metrics/quality.py +67 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/metrics/solutions.py +132 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/report.py +469 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/selection.py +209 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/sources/__init__.py +10 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/sources/base.py +135 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/sources/iono.py +148 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/sources/pspec.py +191 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/sources/quality.py +64 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/sources/solutions.py +98 -0
- cosmic_metrics-0.1.1/src/cosmic_metrics/table.py +325 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cosmic-metrics
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Per-block quality metrics of NenuFAR Cosmic Dawn data, for block selection and combination weights
|
|
5
|
+
Author: Eric Le Saux
|
|
6
|
+
Author-email: eric.ar.saos@gmail.com
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Provides-Extra: docs
|
|
15
|
+
Provides-Extra: iono
|
|
16
|
+
Provides-Extra: report
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: aoquality (>=0.6)
|
|
19
|
+
Requires-Dist: astropy
|
|
20
|
+
Requires-Dist: bokeh (>=3.8) ; extra == "report"
|
|
21
|
+
Requires-Dist: click (>=8.0)
|
|
22
|
+
Requires-Dist: h5py
|
|
23
|
+
Requires-Dist: libpipe (>=0.9)
|
|
24
|
+
Requires-Dist: matplotlib
|
|
25
|
+
Requires-Dist: myst-parser (>=3.0) ; extra == "docs"
|
|
26
|
+
Requires-Dist: numpy
|
|
27
|
+
Requires-Dist: pandas
|
|
28
|
+
Requires-Dist: ps_eor (>=1.0)
|
|
29
|
+
Requires-Dist: pspipe (>=0.14)
|
|
30
|
+
Requires-Dist: pydata-sphinx-theme (>=0.16) ; extra == "docs"
|
|
31
|
+
Requires-Dist: pytest ; extra == "test"
|
|
32
|
+
Requires-Dist: ruff ; extra == "test"
|
|
33
|
+
Requires-Dist: sphinx (>=7.0) ; extra == "docs"
|
|
34
|
+
Requires-Dist: spinifex (>=2.0) ; extra == "iono"
|
|
35
|
+
Requires-Dist: threadpoolctl (>=3.1)
|
|
36
|
+
Requires-Dist: tomli ; python_version < "3.11"
|
|
37
|
+
Project-URL: Documentation, https://cosmic-metrics.readthedocs.io
|
|
38
|
+
Project-URL: Homepage, https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics
|
|
39
|
+
Project-URL: Issues, https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics/-/issues
|
|
40
|
+
Project-URL: Repository, https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# cosmic-metrics
|
|
44
|
+
|
|
45
|
+
Per-observation quality metrics for NenuFAR Cosmic Dawn data. Read QUALITY
|
|
46
|
+
statistics, calibration solutions, power spectra, horizon-filtered variance and
|
|
47
|
+
optional GNSS ionosphere maps; inspect the resulting table, reject poor blocks
|
|
48
|
+
and compute pspipe weights.
|
|
49
|
+
|
|
50
|
+
**Documentation:** [Read the Docs](https://cosmic-metrics.readthedocs.io/en/latest/)
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
[How the objects work together](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/overview.html)
|
|
55
|
+
explains the diagram.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
Requires Python 3.10+ and the NenuFAR processing stack (`aoquality`, `ps_eor`,
|
|
60
|
+
`pspipe`), installed with it from PyPI:
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
python -m pip install cosmic-metrics
|
|
64
|
+
python -m pip install 'cosmic-metrics[report]' # Optional interactive HTML report
|
|
65
|
+
python -m pip install 'cosmic-metrics[iono]' # Optional spinifex map reader
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
From a checkout, use `python -m pip install -e .` with the same extras.
|
|
69
|
+
|
|
70
|
+
## Python workflow
|
|
71
|
+
|
|
72
|
+
A `Dataset` contains observation blocks, named source readers and a managed raw
|
|
73
|
+
cache. Metric objects define calculations. `compute_metrics()` returns an
|
|
74
|
+
independent `MetricTable` containing scalars and observation metadata.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import cosmic_metrics as cm
|
|
78
|
+
from cosmic_metrics import metrics as m
|
|
79
|
+
|
|
80
|
+
dataset = cm.Dataset.from_toml('metrics.toml')
|
|
81
|
+
noise = m.RegionPower('dt_V', **m.REG_B)
|
|
82
|
+
phase = m.PhaseChange(120, source='solutions_l2a')
|
|
83
|
+
table = dataset.compute_metrics([noise, phase], n_workers=1)
|
|
84
|
+
print(table.df)
|
|
85
|
+
print(table.missing_reasons)
|
|
86
|
+
|
|
87
|
+
# Example threshold only; choose science cuts for your dataset.
|
|
88
|
+
selection = table.select_blocks([
|
|
89
|
+
cm.Rule(phase.name, '>', 3, scale='mad'),
|
|
90
|
+
], missing='reject')
|
|
91
|
+
weights = table.compute_weights(noise.name, selection=selection)
|
|
92
|
+
table.save('metrics.csv')
|
|
93
|
+
selection.save('selection.json', weights=weights)
|
|
94
|
+
|
|
95
|
+
print(dataset.cache.inspect())
|
|
96
|
+
preview = dataset.cache.clear(stale_only=True, dry_run=True)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Adapt [examples/nt04_l3.toml](https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics/-/blob/main/examples/nt04_l3.toml) to your input paths and save it
|
|
100
|
+
as `metrics.toml`. QUALITY and calibration templates are resolved per block;
|
|
101
|
+
source names choose which reader a metric uses. All block times are UTC MJD
|
|
102
|
+
seconds. Cache validity uses exact time bounds and dependency signatures.
|
|
103
|
+
For scripts with `n_workers > 1`, use a main guard.
|
|
104
|
+
|
|
105
|
+
Prepare the expensive raw arrays using only the dataset TOML, without defining
|
|
106
|
+
metrics:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
cosmic-metrics cache populate metrics.toml --dry-run
|
|
110
|
+
cosmic-metrics cache populate metrics.toml
|
|
111
|
+
cosmic-metrics cache status metrics.toml
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The example uses Slurm/libpipe; set `[worker].nodes="localhost"` for local
|
|
115
|
+
population, or use SSH host names/ranges. The CLI accepts the same host and worker
|
|
116
|
+
overrides as nenucal, for example `--hosts localhost --max-concurrent 4`. It runs
|
|
117
|
+
in the foreground; use a persistent terminal session for unattended work.
|
|
118
|
+
Use `--slurm-nodes 4` to override the Slurm allocation size for one run while
|
|
119
|
+
retaining the account, partition and other Slurm options from the TOML.
|
|
120
|
+
Valid cache entries are skipped; use `--force` to rebuild them explicitly.
|
|
121
|
+
`[cache].require=true` makes later notebook computations raise on
|
|
122
|
+
missing/stale PS or variance entries. QUALITY and calibration remain uncached.
|
|
123
|
+
Run population before the Python example when using that configuration. See
|
|
124
|
+
[cache preparation](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/cache_population.html)
|
|
125
|
+
for worker settings, progress logs and resuming interrupted jobs.
|
|
126
|
+
|
|
127
|
+
## Command-line workflow
|
|
128
|
+
|
|
129
|
+
The dataset TOML can also define the metrics (`[[metrics]]`), selection stages
|
|
130
|
+
(`[[selection]]`) and an output directory, so that no Python is needed:
|
|
131
|
+
|
|
132
|
+
```console
|
|
133
|
+
cosmic-metrics compute metrics.toml --dry-run # list blocks and metric columns
|
|
134
|
+
cosmic-metrics compute metrics.toml # metrics.csv
|
|
135
|
+
cosmic-metrics select metrics.toml # selection.json
|
|
136
|
+
cosmic-metrics report metrics.toml # interactive report.html
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The report shows every metric against LST or date, with linked selection across
|
|
140
|
+
metric panels, the profile of a selected block in all metrics and a night × LST
|
|
141
|
+
grid. See the
|
|
142
|
+
[command-line guide](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/command_line.html).
|
|
143
|
+
|
|
144
|
+
For RFI checks in a specific frequency band, configure a `variance` source and
|
|
145
|
+
compute `m.BandVariance((67, 68))` or `m.BandVarianceRatio((67, 68))`. These reduce
|
|
146
|
+
the horizon-filtered channel variance around 67–68 MHz; the ratio uses `dt_V`
|
|
147
|
+
as its noise reference. See the
|
|
148
|
+
[band-variance guide](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/metrics.html#rfi-diagnostics-in-a-frequency-band)
|
|
149
|
+
for units, filtering and interpretation.
|
|
150
|
+
|
|
151
|
+
## Documentation
|
|
152
|
+
|
|
153
|
+
Start with the
|
|
154
|
+
[complete synthetic example](https://cosmic-metrics.readthedocs.io/en/latest/getting_started.html),
|
|
155
|
+
which needs no input data. The guide and generated API reference cover the current
|
|
156
|
+
implementation:
|
|
157
|
+
|
|
158
|
+
- [Dataset configuration, blocks and paths](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/datasets.html)
|
|
159
|
+
- [Metric definitions, array shapes and physical units](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/metrics.html)
|
|
160
|
+
- [Selection, persistence and pspipe weights](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/selection_weights.html)
|
|
161
|
+
- [Cache inspection, invalidation and cleanup](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/cache.html)
|
|
162
|
+
- [Cache preparation locally and on Slurm](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/cache_population.html)
|
|
163
|
+
- [Command-line analysis and report](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/command_line.html)
|
|
164
|
+
- [Custom readers and metrics](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/extending.html)
|
|
165
|
+
- [Python API reference](https://cosmic-metrics.readthedocs.io/en/latest/api/index.html)
|
|
166
|
+
- [Development and documentation builds](https://cosmic-metrics.readthedocs.io/en/latest/development.html)
|
|
167
|
+
|
|
168
|
+
The earlier metrics prototype is retained on the `eric-prototype` branch.
|
|
169
|
+
|
|
170
|
+
## Validate and build docs
|
|
171
|
+
|
|
172
|
+
```console
|
|
173
|
+
python -m pip install -e '.[test,docs,report]'
|
|
174
|
+
python -m pytest
|
|
175
|
+
python -m ruff check src tests docs/conf.py
|
|
176
|
+
python -m sphinx -W --keep-going -b html docs docs/_build/html
|
|
177
|
+
```
|
|
178
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# cosmic-metrics
|
|
2
|
+
|
|
3
|
+
Per-observation quality metrics for NenuFAR Cosmic Dawn data. Read QUALITY
|
|
4
|
+
statistics, calibration solutions, power spectra, horizon-filtered variance and
|
|
5
|
+
optional GNSS ionosphere maps; inspect the resulting table, reject poor blocks
|
|
6
|
+
and compute pspipe weights.
|
|
7
|
+
|
|
8
|
+
**Documentation:** [Read the Docs](https://cosmic-metrics.readthedocs.io/en/latest/)
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
[How the objects work together](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/overview.html)
|
|
13
|
+
explains the diagram.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Requires Python 3.10+ and the NenuFAR processing stack (`aoquality`, `ps_eor`,
|
|
18
|
+
`pspipe`), installed with it from PyPI:
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
python -m pip install cosmic-metrics
|
|
22
|
+
python -m pip install 'cosmic-metrics[report]' # Optional interactive HTML report
|
|
23
|
+
python -m pip install 'cosmic-metrics[iono]' # Optional spinifex map reader
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
From a checkout, use `python -m pip install -e .` with the same extras.
|
|
27
|
+
|
|
28
|
+
## Python workflow
|
|
29
|
+
|
|
30
|
+
A `Dataset` contains observation blocks, named source readers and a managed raw
|
|
31
|
+
cache. Metric objects define calculations. `compute_metrics()` returns an
|
|
32
|
+
independent `MetricTable` containing scalars and observation metadata.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import cosmic_metrics as cm
|
|
36
|
+
from cosmic_metrics import metrics as m
|
|
37
|
+
|
|
38
|
+
dataset = cm.Dataset.from_toml('metrics.toml')
|
|
39
|
+
noise = m.RegionPower('dt_V', **m.REG_B)
|
|
40
|
+
phase = m.PhaseChange(120, source='solutions_l2a')
|
|
41
|
+
table = dataset.compute_metrics([noise, phase], n_workers=1)
|
|
42
|
+
print(table.df)
|
|
43
|
+
print(table.missing_reasons)
|
|
44
|
+
|
|
45
|
+
# Example threshold only; choose science cuts for your dataset.
|
|
46
|
+
selection = table.select_blocks([
|
|
47
|
+
cm.Rule(phase.name, '>', 3, scale='mad'),
|
|
48
|
+
], missing='reject')
|
|
49
|
+
weights = table.compute_weights(noise.name, selection=selection)
|
|
50
|
+
table.save('metrics.csv')
|
|
51
|
+
selection.save('selection.json', weights=weights)
|
|
52
|
+
|
|
53
|
+
print(dataset.cache.inspect())
|
|
54
|
+
preview = dataset.cache.clear(stale_only=True, dry_run=True)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Adapt [examples/nt04_l3.toml](https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics/-/blob/main/examples/nt04_l3.toml) to your input paths and save it
|
|
58
|
+
as `metrics.toml`. QUALITY and calibration templates are resolved per block;
|
|
59
|
+
source names choose which reader a metric uses. All block times are UTC MJD
|
|
60
|
+
seconds. Cache validity uses exact time bounds and dependency signatures.
|
|
61
|
+
For scripts with `n_workers > 1`, use a main guard.
|
|
62
|
+
|
|
63
|
+
Prepare the expensive raw arrays using only the dataset TOML, without defining
|
|
64
|
+
metrics:
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
cosmic-metrics cache populate metrics.toml --dry-run
|
|
68
|
+
cosmic-metrics cache populate metrics.toml
|
|
69
|
+
cosmic-metrics cache status metrics.toml
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The example uses Slurm/libpipe; set `[worker].nodes="localhost"` for local
|
|
73
|
+
population, or use SSH host names/ranges. The CLI accepts the same host and worker
|
|
74
|
+
overrides as nenucal, for example `--hosts localhost --max-concurrent 4`. It runs
|
|
75
|
+
in the foreground; use a persistent terminal session for unattended work.
|
|
76
|
+
Use `--slurm-nodes 4` to override the Slurm allocation size for one run while
|
|
77
|
+
retaining the account, partition and other Slurm options from the TOML.
|
|
78
|
+
Valid cache entries are skipped; use `--force` to rebuild them explicitly.
|
|
79
|
+
`[cache].require=true` makes later notebook computations raise on
|
|
80
|
+
missing/stale PS or variance entries. QUALITY and calibration remain uncached.
|
|
81
|
+
Run population before the Python example when using that configuration. See
|
|
82
|
+
[cache preparation](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/cache_population.html)
|
|
83
|
+
for worker settings, progress logs and resuming interrupted jobs.
|
|
84
|
+
|
|
85
|
+
## Command-line workflow
|
|
86
|
+
|
|
87
|
+
The dataset TOML can also define the metrics (`[[metrics]]`), selection stages
|
|
88
|
+
(`[[selection]]`) and an output directory, so that no Python is needed:
|
|
89
|
+
|
|
90
|
+
```console
|
|
91
|
+
cosmic-metrics compute metrics.toml --dry-run # list blocks and metric columns
|
|
92
|
+
cosmic-metrics compute metrics.toml # metrics.csv
|
|
93
|
+
cosmic-metrics select metrics.toml # selection.json
|
|
94
|
+
cosmic-metrics report metrics.toml # interactive report.html
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The report shows every metric against LST or date, with linked selection across
|
|
98
|
+
metric panels, the profile of a selected block in all metrics and a night × LST
|
|
99
|
+
grid. See the
|
|
100
|
+
[command-line guide](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/command_line.html).
|
|
101
|
+
|
|
102
|
+
For RFI checks in a specific frequency band, configure a `variance` source and
|
|
103
|
+
compute `m.BandVariance((67, 68))` or `m.BandVarianceRatio((67, 68))`. These reduce
|
|
104
|
+
the horizon-filtered channel variance around 67–68 MHz; the ratio uses `dt_V`
|
|
105
|
+
as its noise reference. See the
|
|
106
|
+
[band-variance guide](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/metrics.html#rfi-diagnostics-in-a-frequency-band)
|
|
107
|
+
for units, filtering and interpretation.
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
Start with the
|
|
112
|
+
[complete synthetic example](https://cosmic-metrics.readthedocs.io/en/latest/getting_started.html),
|
|
113
|
+
which needs no input data. The guide and generated API reference cover the current
|
|
114
|
+
implementation:
|
|
115
|
+
|
|
116
|
+
- [Dataset configuration, blocks and paths](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/datasets.html)
|
|
117
|
+
- [Metric definitions, array shapes and physical units](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/metrics.html)
|
|
118
|
+
- [Selection, persistence and pspipe weights](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/selection_weights.html)
|
|
119
|
+
- [Cache inspection, invalidation and cleanup](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/cache.html)
|
|
120
|
+
- [Cache preparation locally and on Slurm](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/cache_population.html)
|
|
121
|
+
- [Command-line analysis and report](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/command_line.html)
|
|
122
|
+
- [Custom readers and metrics](https://cosmic-metrics.readthedocs.io/en/latest/user_guide/extending.html)
|
|
123
|
+
- [Python API reference](https://cosmic-metrics.readthedocs.io/en/latest/api/index.html)
|
|
124
|
+
- [Development and documentation builds](https://cosmic-metrics.readthedocs.io/en/latest/development.html)
|
|
125
|
+
|
|
126
|
+
The earlier metrics prototype is retained on the `eric-prototype` branch.
|
|
127
|
+
|
|
128
|
+
## Validate and build docs
|
|
129
|
+
|
|
130
|
+
```console
|
|
131
|
+
python -m pip install -e '.[test,docs,report]'
|
|
132
|
+
python -m pytest
|
|
133
|
+
python -m ruff check src tests docs/conf.py
|
|
134
|
+
python -m sphinx -W --keep-going -b html docs docs/_build/html
|
|
135
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cosmic-metrics"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Per-block quality metrics of NenuFAR Cosmic Dawn data, for block selection and combination weights"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Eric Le Saux", email = "eric.ar.saos@gmail.com" },
|
|
8
|
+
{ name = "Florent Mertens", email = "florent.mertens@gmail.com" },
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy",
|
|
13
|
+
"pandas",
|
|
14
|
+
"h5py",
|
|
15
|
+
"astropy",
|
|
16
|
+
"matplotlib",
|
|
17
|
+
"threadpoolctl >= 3.1",
|
|
18
|
+
"click >= 8.0",
|
|
19
|
+
"libpipe >= 0.9",
|
|
20
|
+
"tomli; python_version < '3.11'",
|
|
21
|
+
"aoquality >= 0.6",
|
|
22
|
+
"ps_eor >= 1.0",
|
|
23
|
+
"pspipe >= 0.14",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
iono = ["spinifex >= 2.0"]
|
|
28
|
+
report = ["bokeh >= 3.8"]
|
|
29
|
+
test = ["pytest", "ruff"]
|
|
30
|
+
docs = ["sphinx >= 7.0", "myst-parser >= 3.0", "pydata-sphinx-theme >= 0.16"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics"
|
|
34
|
+
Repository = "https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics"
|
|
35
|
+
Documentation = "https://cosmic-metrics.readthedocs.io"
|
|
36
|
+
Issues = "https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics/-/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
cosmic-metrics = "cosmic_metrics.cli:main"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["poetry-core>=2.0"]
|
|
43
|
+
build-backend = "poetry.core.masonry.api"
|
|
44
|
+
|
|
45
|
+
[tool.poetry]
|
|
46
|
+
packages = [{ include = "cosmic_metrics", from = "src" }]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = [
|
|
54
|
+
"E",
|
|
55
|
+
"W",
|
|
56
|
+
"F",
|
|
57
|
+
"I",
|
|
58
|
+
"B",
|
|
59
|
+
"UP",
|
|
60
|
+
"C4",
|
|
61
|
+
"SIM",
|
|
62
|
+
"PIE",
|
|
63
|
+
"NPY",
|
|
64
|
+
"RUF",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.per-file-ignores]
|
|
68
|
+
"**/__init__.py" = ["F401"]
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Per-block quality metrics of NenuFAR Cosmic Dawn data, for block selection and weights."""
|
|
2
|
+
|
|
3
|
+
from . import metrics, sources
|
|
4
|
+
from .analysis import Analysis
|
|
5
|
+
from .blocks import Block, BlockSet
|
|
6
|
+
from .cache import Cache, CacheMissError
|
|
7
|
+
from .dataset import Dataset
|
|
8
|
+
from .report import write_report
|
|
9
|
+
from .selection import Rule, SelectionResult, Stage
|
|
10
|
+
from .table import MetricTable
|
|
11
|
+
|
|
12
|
+
__version__ = '0.1.1'
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Analysis: the metrics, detrending, selection stages and outputs of a dataset TOML."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .dataset import _resolve, tomllib
|
|
11
|
+
from .metrics import METRICS
|
|
12
|
+
from .selection import Rule, SelectionResult, Stage
|
|
13
|
+
from .table import INFO, MetricTable
|
|
14
|
+
|
|
15
|
+
TABLE, SELECTION, REPORT = 'metrics.csv', 'selection.json', 'report.html'
|
|
16
|
+
N_PANELS = 6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_args(func, where, *args, **kwargs):
|
|
20
|
+
signature = inspect.signature(func)
|
|
21
|
+
try:
|
|
22
|
+
signature.bind(*args, **kwargs)
|
|
23
|
+
except TypeError as exc:
|
|
24
|
+
names = ', '.join(list(signature.parameters)[len(args):])
|
|
25
|
+
raise ValueError(f'{where}: {exc} (arguments: {names})') from None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _unknown(name, names, what):
|
|
29
|
+
close = difflib.get_close_matches(name, names, n=1)
|
|
30
|
+
hint = f'; did you mean {close[0]}?' if close else ''
|
|
31
|
+
return ValueError(f'{what} {name!r} is not a defined metric{hint}')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _tables(cfg, key, path):
|
|
35
|
+
entries = cfg.get(key, [])
|
|
36
|
+
if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
|
|
37
|
+
raise ValueError(f'{path}: write each entry of {key} as a [[{key}]] table')
|
|
38
|
+
return entries
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _section(cfg, key, allowed, path):
|
|
42
|
+
section = cfg.get(key, {})
|
|
43
|
+
unknown = set(section) - set(allowed)
|
|
44
|
+
if unknown:
|
|
45
|
+
raise ValueError(f'{path}: unknown [{key}] options: {", ".join(sorted(unknown))}')
|
|
46
|
+
return section
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Analysis:
|
|
50
|
+
"""Metric definitions, detrending, selection stages and output paths.
|
|
51
|
+
|
|
52
|
+
Construct with Python objects or :meth:`from_toml`. The ``cosmic-metrics
|
|
53
|
+
compute``, ``select`` and ``report`` commands run the same methods.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
metrics (iterable of Metric): Definitions with unique output names.
|
|
57
|
+
detrend (iterable of (str, dict)): Metric name and :meth:`MetricTable.detrend`
|
|
58
|
+
arguments; each adds one column after the metrics.
|
|
59
|
+
stages (iterable of Stage): Selection stages applied in order.
|
|
60
|
+
output_dir (str or pathlib.Path or None): Directory of the written files.
|
|
61
|
+
panels (list of str or None): Report panels shown at load; None shows the
|
|
62
|
+
metrics used by the rules, then the first metrics, up to six.
|
|
63
|
+
title (str or None): Report title.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
names (list of str): Columns of the computed table, metrics then detrended.
|
|
67
|
+
table_path (pathlib.Path): metrics.csv in output_dir.
|
|
68
|
+
selection_path (pathlib.Path): selection.json in output_dir.
|
|
69
|
+
report_path (pathlib.Path): report.html in output_dir.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: Duplicate columns, or detrend, rules or panels naming an
|
|
73
|
+
undefined metric.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, metrics, detrend=(), stages=(), output_dir=None, panels=None, title=None):
|
|
77
|
+
self.metrics = list(metrics)
|
|
78
|
+
self.detrend = [(metric, dict(opts)) for metric, opts in detrend]
|
|
79
|
+
self.stages = list(stages)
|
|
80
|
+
self.output_dir = None if output_dir is None else Path(output_dir)
|
|
81
|
+
self.panels = None if panels is None else list(panels)
|
|
82
|
+
self.title = title or 'Block quality metrics'
|
|
83
|
+
names = self.names
|
|
84
|
+
duplicate = sorted({n for n in names if names.count(n) > 1})
|
|
85
|
+
if duplicate:
|
|
86
|
+
raise ValueError(f'Duplicate metric columns: {", ".join(duplicate)}')
|
|
87
|
+
base = [m.name for m in self.metrics]
|
|
88
|
+
for metric, opts in self.detrend:
|
|
89
|
+
if metric not in base:
|
|
90
|
+
raise _unknown(metric, base, 'Detrended metric')
|
|
91
|
+
if opts.get('mode', 'difference') not in ('difference', 'ratio'):
|
|
92
|
+
raise ValueError(f'detrend mode must be difference or ratio, not '
|
|
93
|
+
f'{opts["mode"]!r}')
|
|
94
|
+
if opts.get('against', 'lst') not in INFO + base:
|
|
95
|
+
raise ValueError(f'detrend against must be a table column, not '
|
|
96
|
+
f'{opts["against"]!r}')
|
|
97
|
+
stage_names = [s.name for s in self.stages]
|
|
98
|
+
if len(set(stage_names)) != len(stage_names):
|
|
99
|
+
raise ValueError('Duplicate selection stage names')
|
|
100
|
+
for name in [r.metric for s in self.stages for r in s.rules] + (self.panels or []):
|
|
101
|
+
if name not in names:
|
|
102
|
+
raise _unknown(name, names, 'Column')
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_toml(cls, path):
|
|
106
|
+
"""Read ``[[metrics]]``, ``[[selection]]``, ``[output]`` and ``[report]``.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
path (str or pathlib.Path): Dataset TOML. Metric entries hold ``type``
|
|
110
|
+
(a class of :mod:`cosmic_metrics.metrics`), its constructor arguments
|
|
111
|
+
and an optional ``detrend`` table. Selection entries hold ``name``,
|
|
112
|
+
``rules`` (:class:`Rule` fields), ``missing`` and ``exclude``.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Analysis: ``[output].dir`` is relative to the TOML directory. Blocks and
|
|
116
|
+
sources are read by :meth:`Dataset.from_toml`, not here.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: Unknown metric types, arguments or options.
|
|
120
|
+
"""
|
|
121
|
+
path = os.fspath(path)
|
|
122
|
+
with open(path, 'rb') as f:
|
|
123
|
+
cfg = tomllib.load(f)
|
|
124
|
+
metrics, detrend = [], []
|
|
125
|
+
for i, entry in enumerate(_tables(cfg, 'metrics', path), 1):
|
|
126
|
+
where = f'{path}: [[metrics]] {i}'
|
|
127
|
+
opts = dict(entry)
|
|
128
|
+
kind = opts.pop('type', None)
|
|
129
|
+
if kind not in METRICS:
|
|
130
|
+
raise ValueError(f'{where}: type must be one of {", ".join(METRICS)}, '
|
|
131
|
+
f'not {kind!r}')
|
|
132
|
+
trend = opts.pop('detrend', None)
|
|
133
|
+
_check_args(METRICS[kind], f'{where} ({kind})', **opts)
|
|
134
|
+
metrics.append(METRICS[kind](**opts))
|
|
135
|
+
if trend is not None:
|
|
136
|
+
if not isinstance(trend, dict):
|
|
137
|
+
raise ValueError(f'{where}: detrend must be a table of MetricTable.detrend '
|
|
138
|
+
f'arguments')
|
|
139
|
+
_check_args(MetricTable.detrend, f'{where} detrend', None, metrics[-1].name,
|
|
140
|
+
**trend)
|
|
141
|
+
detrend.append((metrics[-1].name, trend))
|
|
142
|
+
stages = []
|
|
143
|
+
for i, entry in enumerate(_tables(cfg, 'selection', path), 1):
|
|
144
|
+
where = f'{path}: [[selection]] {i}'
|
|
145
|
+
_check_args(Stage, where, **{'name': '', **entry})
|
|
146
|
+
opts = dict(entry)
|
|
147
|
+
rules = []
|
|
148
|
+
for j, rule in enumerate(opts.pop('rules', []), 1):
|
|
149
|
+
_check_args(Rule, f'{where} rule {j}', **rule)
|
|
150
|
+
rules.append(Rule(**rule))
|
|
151
|
+
stages.append(Stage(rules=rules, **{'name': f'stage{i}', **opts}))
|
|
152
|
+
root = os.path.dirname(os.path.abspath(path))
|
|
153
|
+
output = _resolve(_section(cfg, 'output', ['dir'], path), root)
|
|
154
|
+
report = _section(cfg, 'report', ['panels', 'title'], path)
|
|
155
|
+
return cls(metrics, detrend, stages, output.get('dir'), report.get('panels'),
|
|
156
|
+
report.get('title'))
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def names(self):
|
|
160
|
+
against = inspect.signature(MetricTable.detrend).parameters['against'].default
|
|
161
|
+
return ([m.name for m in self.metrics]
|
|
162
|
+
+ [opts.get('name') or f'{metric}_detrend_{opts.get("against", against)}'
|
|
163
|
+
for metric, opts in self.detrend])
|
|
164
|
+
|
|
165
|
+
def _output(self, name):
|
|
166
|
+
if self.output_dir is None:
|
|
167
|
+
raise ValueError('No output directory; set [output].dir in the dataset TOML')
|
|
168
|
+
return self.output_dir / name
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def table_path(self):
|
|
172
|
+
return self._output(TABLE)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def selection_path(self):
|
|
176
|
+
return self._output(SELECTION)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def report_path(self):
|
|
180
|
+
return self._output(REPORT)
|
|
181
|
+
|
|
182
|
+
def compute_metrics(self, dataset, n_workers=1, verbose=True):
|
|
183
|
+
"""Compute the metrics on a dataset, then add the detrended columns.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
dataset (Dataset): Blocks and sources, e.g. from the same TOML.
|
|
187
|
+
n_workers (int): Positive process count.
|
|
188
|
+
verbose (bool): Print the progress of :meth:`Dataset.compute_metrics`.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
MetricTable: Columns in the order of :attr:`names`.
|
|
192
|
+
"""
|
|
193
|
+
table = dataset.compute_metrics(self.metrics, n_workers=n_workers, verbose=verbose)
|
|
194
|
+
for metric, opts in self.detrend:
|
|
195
|
+
table = table.detrend(metric, **opts)
|
|
196
|
+
return table
|
|
197
|
+
|
|
198
|
+
def load_table(self):
|
|
199
|
+
"""Read the table written by ``cosmic-metrics compute``.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: Its metrics differ from :attr:`names`.
|
|
203
|
+
"""
|
|
204
|
+
table = MetricTable.load(self.table_path)
|
|
205
|
+
missing = [n for n in self.names if n not in table.metric_names]
|
|
206
|
+
extra = [n for n in table.metric_names if n not in self.names]
|
|
207
|
+
if missing or extra:
|
|
208
|
+
raise ValueError(f'{self.table_path} does not hold the metrics of the TOML '
|
|
209
|
+
f'(missing: {", ".join(missing) or "none"}; other: '
|
|
210
|
+
f'{", ".join(extra) or "none"}); run cosmic-metrics compute')
|
|
211
|
+
return table
|
|
212
|
+
|
|
213
|
+
def select_blocks(self, table, verbose=False):
|
|
214
|
+
"""Apply the stages in order; each sees only the blocks kept so far.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
table (MetricTable): Table with the rule columns.
|
|
218
|
+
verbose (bool): Print the rejected count of each stage.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
SelectionResult: Every block of the table. Reasons are prefixed with the
|
|
222
|
+
stage name, e.g. ``early: Std_mean_XX > 4 MAD (5.2)``. Without stages,
|
|
223
|
+
all blocks are kept.
|
|
224
|
+
"""
|
|
225
|
+
keep = pd.Series(True, index=table.df.index, name='keep')
|
|
226
|
+
reasons = {obs_id: [] for obs_id in table.df.index}
|
|
227
|
+
for stage in self.stages:
|
|
228
|
+
ids = list(keep.index[keep])
|
|
229
|
+
excluded = set(stage.exclude) & set(ids)
|
|
230
|
+
ids = [obs_id for obs_id in ids if obs_id not in excluded]
|
|
231
|
+
rejected = sorted(excluded)
|
|
232
|
+
for obs_id in rejected:
|
|
233
|
+
reasons[obs_id].append(f'{stage.name}: excluded')
|
|
234
|
+
if stage.rules and ids:
|
|
235
|
+
sub = MetricTable(table.df.loc[ids], table.metric_names, table.missing_reasons)
|
|
236
|
+
result = sub.select_blocks(stage.rules, stage.missing)
|
|
237
|
+
for obs_id in result.rejected_ids():
|
|
238
|
+
reasons[obs_id].extend(f'{stage.name}: {r}'
|
|
239
|
+
for r in result.rejection_reasons[obs_id])
|
|
240
|
+
rejected.append(obs_id)
|
|
241
|
+
keep[rejected] = False
|
|
242
|
+
if verbose:
|
|
243
|
+
print(f'{stage.name}: {len(rejected)} / {len(ids) + len(excluded)} blocks '
|
|
244
|
+
f'rejected', flush=True)
|
|
245
|
+
return SelectionResult(keep, reasons, table.df['night'])
|
|
246
|
+
|
|
247
|
+
def write_report(self, table, selection=None, path=None):
|
|
248
|
+
"""Write the interactive HTML report (needs Bokeh, the ``report`` extra).
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
table (MetricTable): Blocks and metrics to show.
|
|
252
|
+
selection (SelectionResult or None): Kept/rejected status and reasons.
|
|
253
|
+
path (str or pathlib.Path or None): Destination; default report_path.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
pathlib.Path: Written file.
|
|
257
|
+
"""
|
|
258
|
+
from .report import write_report
|
|
259
|
+
|
|
260
|
+
panels = self.panels
|
|
261
|
+
if panels is None:
|
|
262
|
+
used = [r.metric for s in self.stages for r in s.rules]
|
|
263
|
+
panels = list(dict.fromkeys(used + table.metric_names))[:N_PANELS]
|
|
264
|
+
path = Path(self.report_path if path is None else path)
|
|
265
|
+
write_report(table, path, selection, panels=panels, title=self.title)
|
|
266
|
+
return path
|