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.
@@ -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
+ ![Cosmic Metrics object and data flow](https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics/-/raw/main/docs/_static/workflow.svg)
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
+ ![Cosmic Metrics object and data flow](https://gitlab.com/nenufar-cosmic-dawn/cosmic-metrics/-/raw/main/docs/_static/workflow.svg)
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