jacscanomaly 0.1.0__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.
- jacscanomaly-0.1.0/LICENSE +21 -0
- jacscanomaly-0.1.0/PKG-INFO +180 -0
- jacscanomaly-0.1.0/README.md +162 -0
- jacscanomaly-0.1.0/pyproject.toml +31 -0
- jacscanomaly-0.1.0/setup.cfg +4 -0
- jacscanomaly-0.1.0/src/jacscanomaly/__init__.py +20 -0
- jacscanomaly-0.1.0/src/jacscanomaly/config.py +85 -0
- jacscanomaly-0.1.0/src/jacscanomaly/extract.py +134 -0
- jacscanomaly-0.1.0/src/jacscanomaly/finder.py +225 -0
- jacscanomaly-0.1.0/src/jacscanomaly/models.py +107 -0
- jacscanomaly-0.1.0/src/jacscanomaly/plot.py +500 -0
- jacscanomaly-0.1.0/src/jacscanomaly/pspl.py +158 -0
- jacscanomaly-0.1.0/src/jacscanomaly/runner.py +267 -0
- jacscanomaly-0.1.0/src/jacscanomaly/seasons.py +126 -0
- jacscanomaly-0.1.0/src/jacscanomaly/utils.py +115 -0
- jacscanomaly-0.1.0/src/jacscanomaly.egg-info/PKG-INFO +180 -0
- jacscanomaly-0.1.0/src/jacscanomaly.egg-info/SOURCES.txt +18 -0
- jacscanomaly-0.1.0/src/jacscanomaly.egg-info/dependency_links.txt +1 -0
- jacscanomaly-0.1.0/src/jacscanomaly.egg-info/requires.txt +4 -0
- jacscanomaly-0.1.0/src/jacscanomaly.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KansukeNunota
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jacscanomaly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: JAX-based scan anomaly detection for time-series residuals
|
|
5
|
+
Author: Kansuke Nunota
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NunotaKansuke/jacscanomaly
|
|
8
|
+
Project-URL: Repository, https://github.com/NunotaKansuke/jacscanomaly
|
|
9
|
+
Project-URL: Issues, https://github.com/NunotaKansuke/jacscanomaly/issues
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: jax
|
|
15
|
+
Requires-Dist: jaxopt
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# jacscanomaly
|
|
20
|
+
|
|
21
|
+
**jacscanomaly** is a JAX-based framework for scan-based anomaly detection
|
|
22
|
+
in time-series data.
|
|
23
|
+
|
|
24
|
+
The package is designed to detect **localized, transient anomalies** by
|
|
25
|
+
scanning residuals after fitting a baseline model (e.g. PSPL in microlensing),
|
|
26
|
+
while remaining fast and differentiable thanks to JAX.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
* **JAX-powered**: fast, vectorized grid scans with JIT compilation
|
|
33
|
+
* **Scan-based anomaly detection** on residuals
|
|
34
|
+
* **Model-agnostic design**: anomaly scan works on residuals of any baseline model
|
|
35
|
+
* **Built-in visualization**: PSPL fit, residuals, and anomaly scan summary
|
|
36
|
+
* Designed for **research-grade workflows** (clear statistics & reproducibility)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install jacscanomaly
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> For local development (without pip), see the example notebook.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick Example
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import numpy as np
|
|
54
|
+
from jacscanomaly import Finder, FinderConfig
|
|
55
|
+
|
|
56
|
+
# load data (time, flux, flux_err)
|
|
57
|
+
data = np.load("example_data.npy")
|
|
58
|
+
time, flux, ferr = data[:, 0], data[:, 1], data[:, 2]
|
|
59
|
+
|
|
60
|
+
# initial guess for PSPL parameters
|
|
61
|
+
p0 = np.array([9826.56, 8.61, 0.353])
|
|
62
|
+
|
|
63
|
+
# run anomaly finder
|
|
64
|
+
config = FinderConfig()
|
|
65
|
+
finder = Finder(config)
|
|
66
|
+
result = finder.run(time, flux, ferr, p0)
|
|
67
|
+
|
|
68
|
+
print("=== PSPL fit ===")
|
|
69
|
+
|
|
70
|
+
t0_pspl, tE_pspl, u0_pspl = result.fit.params
|
|
71
|
+
print(f" t0 = {float(t0_pspl):.3f}")
|
|
72
|
+
print(f" tE = {float(tE_pspl):.3f}")
|
|
73
|
+
print(f" u0 = {float(u0_pspl):.3f}")
|
|
74
|
+
print(f" chi2 / dof = {result.chi2_dof:.3f}\n")
|
|
75
|
+
|
|
76
|
+
b = result.best
|
|
77
|
+
print("=== Anomaly candidate ===")
|
|
78
|
+
print(f" t0 = {b.t0:.3f}")
|
|
79
|
+
print(f" teff = {b.teff:.3f}")
|
|
80
|
+
print(f" dchi2 = {b.dchi2:.3e}")
|
|
81
|
+
print(f" score = {b.score:.2f}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Visualization
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
finder.plot_result()
|
|
90
|
+
plt.show()
|
|
91
|
+
|
|
92
|
+
finder.plot_anomaly_window()
|
|
93
|
+
plt.show()
|
|
94
|
+
```
|
|
95
|
+
These commands produce two complementary visualizations:
|
|
96
|
+
|
|
97
|
+
1. **Three-panel summary plot (`finder.plot_result`)**
|
|
98
|
+
- **Top:** Observed light curve with the best-fit baseline model (PSPL)
|
|
99
|
+
- **Middle:** Residuals after baseline fitting
|
|
100
|
+
- **Bottom:** Anomaly scan result (Δχ² vs. time), showing where localized
|
|
101
|
+
deviations from the baseline model are detected
|
|
102
|
+
|
|
103
|
+
2. **Focused anomaly window plot (`finder.plot_anomaly_window`)**
|
|
104
|
+
- A zoomed-in view around the best anomaly candidate
|
|
105
|
+
- Residuals are shown together with the anomaly template and the flat model
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Method Overview
|
|
110
|
+
|
|
111
|
+
The workflow of `jacscanomaly` is:
|
|
112
|
+
|
|
113
|
+
1. **Baseline fitting**
|
|
114
|
+
Fit a global model (e.g. PSPL) to the full light curve.
|
|
115
|
+
|
|
116
|
+
2. **Residual analysis**
|
|
117
|
+
Compute residuals:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
residual = data − baseline_model
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
3. **Local anomaly scan**
|
|
124
|
+
For each grid point `(t0, teff)`, compare:
|
|
125
|
+
|
|
126
|
+
* a null (flat) model
|
|
127
|
+
* an anomaly template model
|
|
128
|
+
|
|
129
|
+
within a local time window.
|
|
130
|
+
|
|
131
|
+
4. **Detection statistic**
|
|
132
|
+
The improvement is measured by:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Δχ² = χ²_flat − χ²_anomaly
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Anomaly Score
|
|
141
|
+
|
|
142
|
+
To quantify how significant the best anomaly candidate is relative to others,
|
|
143
|
+
we define a **score**:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
score = (Δχ²_best − median(Δχ²_others)) / std(Δχ²_others)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This measures how strongly the best candidate stands out from the rest of the grid.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
Key parameters are controlled via `FinderConfig`:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from jacscanomaly import FinderConfig
|
|
159
|
+
|
|
160
|
+
config = FinderConfig(
|
|
161
|
+
gap=100.0, # season gap
|
|
162
|
+
teff_init=0.3, # initial anomaly timescale
|
|
163
|
+
teff_grid_n=5, # number of teff grid points
|
|
164
|
+
sigma=3.0, # threshold for outlier counting
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See `FinderConfig` for the full list of options.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
* Python ≥ 3.9
|
|
175
|
+
* numpy
|
|
176
|
+
* jax
|
|
177
|
+
* jaxopt
|
|
178
|
+
* matplotlib
|
|
179
|
+
|
|
180
|
+
---
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# jacscanomaly
|
|
2
|
+
|
|
3
|
+
**jacscanomaly** is a JAX-based framework for scan-based anomaly detection
|
|
4
|
+
in time-series data.
|
|
5
|
+
|
|
6
|
+
The package is designed to detect **localized, transient anomalies** by
|
|
7
|
+
scanning residuals after fitting a baseline model (e.g. PSPL in microlensing),
|
|
8
|
+
while remaining fast and differentiable thanks to JAX.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
* **JAX-powered**: fast, vectorized grid scans with JIT compilation
|
|
15
|
+
* **Scan-based anomaly detection** on residuals
|
|
16
|
+
* **Model-agnostic design**: anomaly scan works on residuals of any baseline model
|
|
17
|
+
* **Built-in visualization**: PSPL fit, residuals, and anomaly scan summary
|
|
18
|
+
* Designed for **research-grade workflows** (clear statistics & reproducibility)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install jacscanomaly
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> For local development (without pip), see the example notebook.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick Example
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import numpy as np
|
|
36
|
+
from jacscanomaly import Finder, FinderConfig
|
|
37
|
+
|
|
38
|
+
# load data (time, flux, flux_err)
|
|
39
|
+
data = np.load("example_data.npy")
|
|
40
|
+
time, flux, ferr = data[:, 0], data[:, 1], data[:, 2]
|
|
41
|
+
|
|
42
|
+
# initial guess for PSPL parameters
|
|
43
|
+
p0 = np.array([9826.56, 8.61, 0.353])
|
|
44
|
+
|
|
45
|
+
# run anomaly finder
|
|
46
|
+
config = FinderConfig()
|
|
47
|
+
finder = Finder(config)
|
|
48
|
+
result = finder.run(time, flux, ferr, p0)
|
|
49
|
+
|
|
50
|
+
print("=== PSPL fit ===")
|
|
51
|
+
|
|
52
|
+
t0_pspl, tE_pspl, u0_pspl = result.fit.params
|
|
53
|
+
print(f" t0 = {float(t0_pspl):.3f}")
|
|
54
|
+
print(f" tE = {float(tE_pspl):.3f}")
|
|
55
|
+
print(f" u0 = {float(u0_pspl):.3f}")
|
|
56
|
+
print(f" chi2 / dof = {result.chi2_dof:.3f}\n")
|
|
57
|
+
|
|
58
|
+
b = result.best
|
|
59
|
+
print("=== Anomaly candidate ===")
|
|
60
|
+
print(f" t0 = {b.t0:.3f}")
|
|
61
|
+
print(f" teff = {b.teff:.3f}")
|
|
62
|
+
print(f" dchi2 = {b.dchi2:.3e}")
|
|
63
|
+
print(f" score = {b.score:.2f}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Visualization
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
finder.plot_result()
|
|
72
|
+
plt.show()
|
|
73
|
+
|
|
74
|
+
finder.plot_anomaly_window()
|
|
75
|
+
plt.show()
|
|
76
|
+
```
|
|
77
|
+
These commands produce two complementary visualizations:
|
|
78
|
+
|
|
79
|
+
1. **Three-panel summary plot (`finder.plot_result`)**
|
|
80
|
+
- **Top:** Observed light curve with the best-fit baseline model (PSPL)
|
|
81
|
+
- **Middle:** Residuals after baseline fitting
|
|
82
|
+
- **Bottom:** Anomaly scan result (Δχ² vs. time), showing where localized
|
|
83
|
+
deviations from the baseline model are detected
|
|
84
|
+
|
|
85
|
+
2. **Focused anomaly window plot (`finder.plot_anomaly_window`)**
|
|
86
|
+
- A zoomed-in view around the best anomaly candidate
|
|
87
|
+
- Residuals are shown together with the anomaly template and the flat model
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Method Overview
|
|
92
|
+
|
|
93
|
+
The workflow of `jacscanomaly` is:
|
|
94
|
+
|
|
95
|
+
1. **Baseline fitting**
|
|
96
|
+
Fit a global model (e.g. PSPL) to the full light curve.
|
|
97
|
+
|
|
98
|
+
2. **Residual analysis**
|
|
99
|
+
Compute residuals:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
residual = data − baseline_model
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
3. **Local anomaly scan**
|
|
106
|
+
For each grid point `(t0, teff)`, compare:
|
|
107
|
+
|
|
108
|
+
* a null (flat) model
|
|
109
|
+
* an anomaly template model
|
|
110
|
+
|
|
111
|
+
within a local time window.
|
|
112
|
+
|
|
113
|
+
4. **Detection statistic**
|
|
114
|
+
The improvement is measured by:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Δχ² = χ²_flat − χ²_anomaly
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Anomaly Score
|
|
123
|
+
|
|
124
|
+
To quantify how significant the best anomaly candidate is relative to others,
|
|
125
|
+
we define a **score**:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
score = (Δχ²_best − median(Δχ²_others)) / std(Δχ²_others)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This measures how strongly the best candidate stands out from the rest of the grid.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
Key parameters are controlled via `FinderConfig`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from jacscanomaly import FinderConfig
|
|
141
|
+
|
|
142
|
+
config = FinderConfig(
|
|
143
|
+
gap=100.0, # season gap
|
|
144
|
+
teff_init=0.3, # initial anomaly timescale
|
|
145
|
+
teff_grid_n=5, # number of teff grid points
|
|
146
|
+
sigma=3.0, # threshold for outlier counting
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
See `FinderConfig` for the full list of options.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Requirements
|
|
155
|
+
|
|
156
|
+
* Python ≥ 3.9
|
|
157
|
+
* numpy
|
|
158
|
+
* jax
|
|
159
|
+
* jaxopt
|
|
160
|
+
* matplotlib
|
|
161
|
+
|
|
162
|
+
---
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jacscanomaly"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "JAX-based scan anomaly detection for time-series residuals"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Kansuke Nunota" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy",
|
|
15
|
+
"jax",
|
|
16
|
+
"jaxopt",
|
|
17
|
+
"matplotlib",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/NunotaKansuke/jacscanomaly"
|
|
22
|
+
Repository = "https://github.com/NunotaKansuke/jacscanomaly"
|
|
23
|
+
Issues = "https://github.com/NunotaKansuke/jacscanomaly/issues"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = {"" = "src"}
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
exclude = ["jacscanomaly.test_tool*"]
|
|
31
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# scanomaly/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from jax import config as jax_config
|
|
5
|
+
jax_config.update("jax_enable_x64", True)
|
|
6
|
+
|
|
7
|
+
from .config import FinderConfig
|
|
8
|
+
from .finder import Finder
|
|
9
|
+
from .plot import AnomalyPlotter
|
|
10
|
+
from .pspl import PSPLFitter, PSPLFitResult
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FinderConfig",
|
|
14
|
+
"Finder",
|
|
15
|
+
"AnomalyPlotter",
|
|
16
|
+
"PSPLFitter",
|
|
17
|
+
"PSPLFitResult",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class FinderConfig:
|
|
8
|
+
"""
|
|
9
|
+
Configuration for :class:`scanomaly.finder.Finder`.
|
|
10
|
+
|
|
11
|
+
This dataclass contains *only* hyperparameters that control the behavior of the
|
|
12
|
+
anomaly search. It is intentionally dependency-free (no NumPy/JAX imports) and
|
|
13
|
+
frozen for reproducibility.
|
|
14
|
+
|
|
15
|
+
Parameter groups
|
|
16
|
+
----------------
|
|
17
|
+
1) Season splitting:
|
|
18
|
+
Split the time series into seasons based on large time gaps.
|
|
19
|
+
|
|
20
|
+
2) Grid construction:
|
|
21
|
+
Build a (t0, teff) grid per season.
|
|
22
|
+
|
|
23
|
+
3) Grid scan:
|
|
24
|
+
Evaluate delta-chi2 on the grid within a local window.
|
|
25
|
+
|
|
26
|
+
4) Cluster extraction:
|
|
27
|
+
Group overlapping candidates and pick the best per cluster.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# ==================================================
|
|
31
|
+
# 1) Season splitting
|
|
32
|
+
# ==================================================
|
|
33
|
+
gap: float = 100.0
|
|
34
|
+
"""Time gap threshold for splitting seasons. A new season starts when dt > gap."""
|
|
35
|
+
|
|
36
|
+
# ==================================================
|
|
37
|
+
# 2) Grid construction (t0, teff)
|
|
38
|
+
# ==================================================
|
|
39
|
+
teff_init: float = 0.03
|
|
40
|
+
"""Initial teff value for the grid (first element of the geometric series)."""
|
|
41
|
+
|
|
42
|
+
common_ratio: float = 4.0 / 3.0
|
|
43
|
+
"""Common ratio for the geometric series of teff values."""
|
|
44
|
+
|
|
45
|
+
teff_grid_n: int = 5
|
|
46
|
+
"""Number of teff values in the grid."""
|
|
47
|
+
|
|
48
|
+
dt0_coeff: float = 0.17
|
|
49
|
+
"""
|
|
50
|
+
Grid spacing coefficient for t0:
|
|
51
|
+
dt0 = dt0_coeff * teff
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# ==================================================
|
|
55
|
+
# 3) Grid scan (local evaluation window)
|
|
56
|
+
# ==================================================
|
|
57
|
+
sigma: float = 3.0
|
|
58
|
+
"""
|
|
59
|
+
Threshold parameter used in counting per-point chi2 improvement.
|
|
60
|
+
(Kept for compatibility with your original `n_out` logic.)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
teff_coeff: float = 3.0
|
|
64
|
+
"""
|
|
65
|
+
Window half-width multiplier in units of teff:
|
|
66
|
+
window = [t0 - teff_coeff*teff, t0 + teff_coeff*teff]
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
min_pts_in_window: int = 4
|
|
70
|
+
"""Minimum number of data points required inside the window to evaluate a grid point."""
|
|
71
|
+
|
|
72
|
+
# ==================================================
|
|
73
|
+
# 4) Cluster extraction
|
|
74
|
+
# ==================================================
|
|
75
|
+
overlap_sigma: float = 3.0
|
|
76
|
+
"""
|
|
77
|
+
Overlap threshold multiplier used to group nearby grid points into clusters:
|
|
78
|
+
|t0_i - t0_j| < overlap_sigma * (teff_i + teff_j)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
min_cluster_points: int = 3
|
|
82
|
+
"""
|
|
83
|
+
Stop extracting clusters when the number of remaining grid points becomes
|
|
84
|
+
smaller than this value.
|
|
85
|
+
"""
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ResultExtractor:
|
|
11
|
+
"""
|
|
12
|
+
Cluster extractor for grid-scan candidates.
|
|
13
|
+
|
|
14
|
+
Given arrays of (t0, teff, delta_chi2) evaluated on a grid,
|
|
15
|
+
this class groups overlapping candidates and returns one representative
|
|
16
|
+
(the maximum delta_chi2 point) per cluster.
|
|
17
|
+
|
|
18
|
+
Overlap definition
|
|
19
|
+
------------------
|
|
20
|
+
Two candidates i and j are considered overlapping if:
|
|
21
|
+
|
|
22
|
+
|t0_i - t0_j| < sigma_overlap * (teff_i + teff_j)
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
- This operates on CPU / NumPy arrays (no JAX).
|
|
27
|
+
- Returned `clusters` has shape (K, 3) with rows [t0_best, teff_best, dchi2_best].
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
sigma_overlap: float = 3.0
|
|
31
|
+
min_points: int = 3
|
|
32
|
+
|
|
33
|
+
def _overlap_with_max(
|
|
34
|
+
self,
|
|
35
|
+
t0: np.ndarray,
|
|
36
|
+
teff: np.ndarray,
|
|
37
|
+
dchi2: np.ndarray,
|
|
38
|
+
) -> Tuple[np.ndarray, int]:
|
|
39
|
+
"""
|
|
40
|
+
Compute the overlap mask around the current maximum dchi2 point.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
overlap_mask : np.ndarray of bool, shape (N,)
|
|
45
|
+
Mask selecting points overlapping with the maximum point.
|
|
46
|
+
i_max : int
|
|
47
|
+
Index of the maximum point within the provided arrays.
|
|
48
|
+
"""
|
|
49
|
+
i_max = int(np.nanargmax(dchi2))
|
|
50
|
+
t0_max = t0[i_max]
|
|
51
|
+
teff_max = teff[i_max]
|
|
52
|
+
overlap_mask = np.abs(t0 - t0_max) < self.sigma_overlap * (teff + teff_max)
|
|
53
|
+
return overlap_mask, i_max
|
|
54
|
+
|
|
55
|
+
def iterative_anomaly_extraction(
|
|
56
|
+
self,
|
|
57
|
+
t0_list,
|
|
58
|
+
teff_list,
|
|
59
|
+
dchi2_list,
|
|
60
|
+
) -> np.ndarray:
|
|
61
|
+
"""
|
|
62
|
+
Iteratively extract non-overlapping clusters from grid results.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
t0_list, teff_list, dchi2_list
|
|
67
|
+
1D arrays (or array-like) of equal length.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
clusters : np.ndarray, shape (K, 3)
|
|
72
|
+
Each row is [t0, teff, dchi2] for the best (max dchi2) point
|
|
73
|
+
in each extracted cluster.
|
|
74
|
+
Returns an empty array with shape (0, 3) if nothing is extractable.
|
|
75
|
+
|
|
76
|
+
Stopping conditions
|
|
77
|
+
-------------------
|
|
78
|
+
- No remaining candidates.
|
|
79
|
+
- The best remaining candidate is non-finite.
|
|
80
|
+
- Remaining candidate count drops below `min_points`.
|
|
81
|
+
"""
|
|
82
|
+
t0 = np.asarray(t0_list, dtype=float)
|
|
83
|
+
teff = np.asarray(teff_list, dtype=float)
|
|
84
|
+
dchi2 = np.asarray(dchi2_list, dtype=float)
|
|
85
|
+
|
|
86
|
+
if t0.size == 0:
|
|
87
|
+
return np.zeros((0, 3), dtype=float)
|
|
88
|
+
|
|
89
|
+
if not (t0.shape == teff.shape == dchi2.shape):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Input arrays must have the same shape, got "
|
|
92
|
+
f"t0={t0.shape}, teff={teff.shape}, dchi2={dchi2.shape}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
clusters: List[List[float]] = []
|
|
96
|
+
remaining = np.ones_like(dchi2, dtype=bool)
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
if not np.any(remaining):
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
# pick the best remaining point
|
|
103
|
+
dchi2_rem = np.where(remaining, dchi2, -np.inf)
|
|
104
|
+
i_max_global = int(np.argmax(dchi2_rem))
|
|
105
|
+
|
|
106
|
+
if not np.isfinite(dchi2[i_max_global]):
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
# overlap mask in the "compressed" remaining arrays
|
|
110
|
+
overlap_mask, _ = self._overlap_with_max(
|
|
111
|
+
t0[remaining], teff[remaining], dchi2[remaining]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# expand to full mask
|
|
115
|
+
full_mask = np.zeros_like(remaining)
|
|
116
|
+
full_mask[np.where(remaining)[0][overlap_mask]] = True
|
|
117
|
+
|
|
118
|
+
# choose the best representative in this cluster
|
|
119
|
+
cluster_dchi2 = dchi2[full_mask]
|
|
120
|
+
cluster_t0 = t0[full_mask]
|
|
121
|
+
cluster_teff = teff[full_mask]
|
|
122
|
+
|
|
123
|
+
i_local_max = int(np.argmax(cluster_dchi2))
|
|
124
|
+
clusters.append(
|
|
125
|
+
[float(cluster_t0[i_local_max]), float(cluster_teff[i_local_max]), float(cluster_dchi2[i_local_max])]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# remove this cluster from remaining
|
|
129
|
+
remaining &= ~full_mask
|
|
130
|
+
|
|
131
|
+
if int(np.sum(remaining)) < self.min_points:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
return np.asarray(clusters, dtype=float)
|