isgri 0.2.0__tar.gz → 0.3.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.
@@ -0,0 +1 @@
1
+ 3.10
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: isgri
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python package for INTEGRAL IBIS/ISGRI lightcurve analysis
5
5
  Author: Dominik Patryk Pacholski
6
6
  License: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
- Requires-Dist: astropy>=7.2.0
10
- Requires-Dist: numpy>=2.3.5
9
+ Requires-Dist: astropy
10
+ Requires-Dist: numpy
11
11
  Description-Content-Type: text/markdown
12
12
 
13
13
  # isgri
@@ -243,7 +243,7 @@
243
243
  },
244
244
  {
245
245
  "cell_type": "code",
246
- "execution_count": 35,
246
+ "execution_count": 3,
247
247
  "id": "69106f8b",
248
248
  "metadata": {},
249
249
  "outputs": [
@@ -251,11 +251,12 @@
251
251
  "name": "stdout",
252
252
  "output_type": "stream",
253
253
  "text": [
254
- "Raw chisq reduced: 8.11\n",
255
- "Clipped chisq reduced: 0.82\n",
256
- "GTI chisq reduced: 8.05\n",
257
- "Module variability: [ 1.7798905 1.9385959 9.868956 10.112125 10.713058 11.080585\n",
258
- " 9.421991 9.954155 ]\n"
254
+ "Raw chisq reduced: 10.04\n",
255
+ "Clipped chisq reduced: 1.00\n",
256
+ "GTI chisq reduced: 10.03\n",
257
+ "Module variability: (array([ 6359.549, 6926.603, 35261.777, 36130.625, 38277.758, 39590.93 ,\n",
258
+ " 33664.773, 35566.195], dtype=float32), array([2942, 3025, 3567, 3568, 3568, 3568, 3568, 3567]), array([ 6185.0806, 6649.9795, 91052.58 , 93023.62 , 97074.28 ,\n",
259
+ " 96712.97 , 91139.47 , 85202.89 ], dtype=float32))\n"
259
260
  ]
260
261
  }
261
262
  ],
@@ -389,7 +390,7 @@
389
390
  "name": "python",
390
391
  "nbconvert_exporter": "python",
391
392
  "pygments_lexer": "ipython3",
392
- "version": "3.12.3"
393
+ "version": "3.10.19"
393
394
  }
394
395
  },
395
396
  "nbformat": 4,
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "isgri"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  authors = [{name = "Dominik Patryk Pacholski"}]
9
9
  license = {text = "MIT"}
10
10
  description = "Python package for INTEGRAL IBIS/ISGRI lightcurve analysis"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
14
- "numpy>=2.3.5",
15
- "astropy>=7.2.0",
14
+ "numpy",
15
+ "astropy",
16
16
  ]
17
17
 
18
18
  [tool.setuptools.packages.find]
@@ -62,7 +62,7 @@ class QualityMetrics:
62
62
  time, counts = self.lightcurve.rebin_by_modules(
63
63
  binsize=self.binsize, emin=self.emin, emax=self.emax, local_time=self.local_time
64
64
  )
65
- module_data = {"time": time, "counts": counts}
65
+ module_data = {"time": time, "counts": np.asarray(counts)}
66
66
  self.module_data = module_data
67
67
  return module_data
68
68
 
@@ -71,20 +71,36 @@ class QualityMetrics:
71
71
  Compute reduced chi-squared for count data.
72
72
 
73
73
  Args:
74
- counts (ndarray): Count array(s) to analyze.
75
- return_all (bool, optional): If True, returns chi-squared for each array. If False, returns mean. Defaults to False.
74
+ counts (ndarray): Count array(s) to analyze. Shape: (n_modules, n_bins) or (n_bins,)
75
+ return_all (bool, optional): If True, returns detailed results. If False, returns weighted mean. Defaults to False.
76
76
 
77
77
  Returns:
78
- float or ndarray: Reduced chi-squared value(s).
78
+ If return_all=False:
79
+ float: Weighted mean chi-squared (weighted by total counts per module)
80
+ If return_all=True:
81
+ tuple: (chi_squared, dof, no_counts) where:
82
+ - chi_squared: Raw chi-squared values per module
83
+ - dof: Degrees of freedom per module (n_bins - 1 excluding NaN)
84
+ - no_counts: Total counts per module
79
85
  """
80
86
  counts = np.asarray(counts)
81
87
  counts = np.where(counts == 0, np.nan, counts)
82
88
  mean_counts = np.nanmean(counts, axis=-1, keepdims=True)
83
89
  chi_squared = np.nansum((counts - mean_counts) ** 2 / mean_counts, axis=-1)
84
- dof = counts.shape[-1] - 1
90
+
91
+ # DOF = number of non-empty bins minus 1
92
+ nan_mask = ~np.isnan(counts)
93
+ dof = np.sum(nan_mask, axis=-1) - 1
94
+ no_counts = np.nansum(counts, axis=-1)
95
+
85
96
  if return_all:
86
- return chi_squared / dof
87
- return np.nanmean(chi_squared / dof)
97
+ return chi_squared, dof, no_counts
98
+
99
+ if np.sum(no_counts) == 0 or np.all(dof <= 0):
100
+ return np.nan
101
+
102
+ # Weight by total counts per module
103
+ return np.average(chi_squared / dof, weights=no_counts)
88
104
 
89
105
  def raw_chi_squared(self, counts=None, return_all=False):
90
106
  """
@@ -219,3 +219,70 @@ def test_lightcurve_rebin_with_custom_mask(mock_events_file):
219
219
  time_full, counts_full = lc.rebin(binsize=1.0, emin=30, emax=300)
220
220
 
221
221
  assert np.sum(counts_masked) < np.sum(counts_full)
222
+
223
+
224
+ def test_lightcurve_module_assignment():
225
+ """Test that events are correctly assigned to detector modules."""
226
+ # Create synthetic events with DIFFERENT counts per module to verify assignment
227
+ module_counts_expected = [10, 25, 50, 75, 100, 125, 150, 200] # Different for each module
228
+ n_events = sum(module_counts_expected)
229
+
230
+ events = np.zeros(
231
+ n_events,
232
+ dtype=[
233
+ ("TIME", "f8"),
234
+ ("ISGRI_ENERGY", "f4"),
235
+ ("DETY", "i2"),
236
+ ("DETZ", "i2"),
237
+ ("SELECT_FLAG", "i2"),
238
+ ],
239
+ )
240
+
241
+ # Module layout: 0 1
242
+ # 2 3
243
+ # 4 5
244
+ # 6 7
245
+ module_positions = [
246
+ (16, 32), # Module 0: DETZ [0-32), DETY [0-64)
247
+ (16, 96), # Module 1: DETZ [0-32), DETY [64-130)
248
+ (48, 32), # Module 2: DETZ [32-66), DETY [0-64)
249
+ (48, 96), # Module 3: DETZ [32-66), DETY [64-130)
250
+ (80, 32), # Module 4: DETZ [66-100), DETY [0-64)
251
+ (80, 96), # Module 5: DETZ [66-100), DETY [64-130)
252
+ (116, 32), # Module 6: DETZ [100-134), DETY [0-64)
253
+ (116, 96), # Module 7: DETZ [100-134), DETY [64-130)
254
+ ]
255
+
256
+ idx = 0
257
+ for module_no, (detz, dety) in enumerate(module_positions):
258
+ n_events_module = module_counts_expected[module_no]
259
+
260
+ events["DETZ"][idx : idx + n_events_module] = detz
261
+ events["DETY"][idx : idx + n_events_module] = dety
262
+ events["TIME"][idx : idx + n_events_module] = np.linspace(0, 10 / 86400, n_events_module)
263
+ events["ISGRI_ENERGY"][idx : idx + n_events_module] = 100 # All same energy
264
+ events["SELECT_FLAG"][idx : idx + n_events_module] = 0
265
+
266
+ idx += n_events_module
267
+
268
+ # Create LightCurve
269
+ time = events["TIME"]
270
+ energies = events["ISGRI_ENERGY"]
271
+ dety = events["DETY"]
272
+ detz = events["DETZ"]
273
+ gtis = np.array([[time[0], time[-1]]])
274
+ weights = np.ones(n_events)
275
+ metadata = {}
276
+
277
+ lc = LightCurve(time, energies, gtis, dety, detz, weights, metadata)
278
+
279
+ # Rebin by modules (1 bin covering all time)
280
+ times, counts = lc.rebin_by_modules(binsize=20.0, emin=50, emax=200, local_time=True)
281
+
282
+ # Verify each module has the expected count
283
+ for module_no, expected_count in enumerate(module_counts_expected):
284
+ actual_count = counts[module_no][0]
285
+ assert actual_count == expected_count, (
286
+ f"Module {module_no} at position {module_positions[module_no]}: "
287
+ f"expected {expected_count} counts, got {actual_count}"
288
+ )
@@ -0,0 +1,268 @@
1
+ import pytest
2
+ import numpy as np
3
+ from isgri.utils.quality import QualityMetrics
4
+ from isgri.utils.lightcurve import LightCurve
5
+
6
+
7
+ @pytest.fixture
8
+ def mock_lightcurve():
9
+ """Create a simple mock LightCurve for testing."""
10
+ n_events = 1000
11
+ time = np.linspace(0, 100 / 86400, n_events)
12
+ energies = np.random.uniform(30, 300, n_events)
13
+ gtis = np.array([[time[0], time[-1]]])
14
+ dety = np.random.randint(0, 128, n_events)
15
+ detz = np.random.randint(0, 134, n_events)
16
+ weights = np.ones(n_events)
17
+ metadata = {"SWID": "test"}
18
+
19
+ return LightCurve(time, energies, gtis, dety, detz, weights, metadata)
20
+
21
+
22
+ def test_quality_metrics_init():
23
+ """Test QualityMetrics initialization."""
24
+ qm = QualityMetrics(binsize=1.0, emin=30, emax=300)
25
+
26
+ assert qm.lightcurve is None
27
+ assert qm.binsize == 1.0
28
+ assert qm.emin == 30
29
+ assert qm.emax == 300
30
+ assert qm.local_time is False
31
+ assert qm.module_data is None
32
+
33
+
34
+ def test_quality_metrics_init_with_lightcurve(mock_lightcurve):
35
+ """Test QualityMetrics initialization with LightCurve."""
36
+ qm = QualityMetrics(mock_lightcurve, binsize=1.0, emin=30, emax=300)
37
+
38
+ assert qm.lightcurve is not None
39
+ assert isinstance(qm.lightcurve, LightCurve)
40
+
41
+
42
+ def test_quality_metrics_init_invalid_type():
43
+ """Test QualityMetrics raises error for invalid lightcurve type."""
44
+ with pytest.raises(TypeError):
45
+ QualityMetrics(lightcurve="not_a_lightcurve")
46
+
47
+
48
+ def test_compute_counts_without_lightcurve():
49
+ """Test _compute_counts raises error when lightcurve is None."""
50
+ qm = QualityMetrics()
51
+
52
+ with pytest.raises(ValueError, match="Lightcurve is not set"):
53
+ qm._compute_counts()
54
+
55
+
56
+ def test_compute_counts_caching(mock_lightcurve):
57
+ """Test that _compute_counts caches results."""
58
+ qm = QualityMetrics(mock_lightcurve, binsize=1.0, emin=30, emax=300)
59
+
60
+ data1 = qm._compute_counts()
61
+ data2 = qm._compute_counts()
62
+
63
+ assert data1 is data2
64
+ assert "time" in data1
65
+ assert "counts" in data1
66
+
67
+
68
+ def test_chi_squared_constant_lightcurve():
69
+ """Test chi-squared for constant lightcurve."""
70
+ counts = np.ones(100) * 10.0
71
+ qm = QualityMetrics()
72
+
73
+ chi = qm.raw_chi_squared(counts=counts)
74
+
75
+ assert chi < 0.1
76
+
77
+
78
+ def test_chi_squared_poisson_lightcurve():
79
+ """Test chi-squared for Poisson-distributed data."""
80
+ np.random.seed(42)
81
+ counts = np.random.poisson(10, 1000)
82
+ qm = QualityMetrics()
83
+
84
+ chi = qm.raw_chi_squared(counts=counts)
85
+
86
+ assert 0.7 < chi < 1.3
87
+
88
+
89
+ def test_chi_squared_variable_lightcurve():
90
+ """Test chi-squared for highly variable lightcurve."""
91
+ counts = np.concatenate([np.ones(50) * 5, np.ones(50) * 50])
92
+ qm = QualityMetrics()
93
+
94
+ chi = qm.raw_chi_squared(counts=counts)
95
+
96
+ assert chi > 2.0
97
+
98
+
99
+ def test_chi_squared_1d_vs_2d():
100
+ """Test chi-squared handles both 1D and 2D arrays."""
101
+ np.random.seed(42)
102
+ counts_1d = np.random.poisson(10, 100)
103
+ counts_2d = np.random.poisson(10, (8, 100))
104
+
105
+ qm = QualityMetrics()
106
+ chi_1d = qm.raw_chi_squared(counts=counts_1d)
107
+ chi_2d = qm.raw_chi_squared(counts=counts_2d)
108
+
109
+ assert isinstance(chi_1d, (float, np.floating))
110
+ assert isinstance(chi_2d, (float, np.floating))
111
+
112
+
113
+ def test_chi_squared_return_all():
114
+ """Test chi-squared with return_all=True."""
115
+ np.random.seed(42)
116
+ counts = np.random.poisson(10, (8, 100))
117
+ qm = QualityMetrics()
118
+
119
+ chi, dof, no_counts = qm.raw_chi_squared(counts=counts, return_all=True)
120
+
121
+ assert chi.shape == (8,)
122
+ assert dof.shape == (8,)
123
+ assert no_counts.shape == (8,)
124
+ assert np.all(dof == 99)
125
+ assert np.all(no_counts > 0)
126
+
127
+
128
+ def test_chi_squared_with_zeros():
129
+ """Test chi-squared handles zero counts."""
130
+ counts = np.array([10, 10, 0, 10, 10])
131
+ qm = QualityMetrics()
132
+
133
+ chi = qm.raw_chi_squared(counts=counts)
134
+
135
+ assert np.isfinite(chi)
136
+ assert chi < 0.5
137
+
138
+
139
+ def test_sigma_clip_removes_outliers():
140
+ """Test sigma clipping removes outliers."""
141
+ counts = np.ones(100) * 10.0
142
+ counts[50] = 100.0
143
+
144
+ qm = QualityMetrics()
145
+ chi_raw = qm.raw_chi_squared(counts=counts)
146
+ chi_clipped = qm.sigma_clip_chi_squared(counts=counts, sigma=3.0)
147
+
148
+ assert chi_clipped < chi_raw
149
+ assert chi_clipped < 0.5
150
+
151
+
152
+ def test_sigma_clip_different_thresholds():
153
+ """Test different sigma thresholds."""
154
+ np.random.seed(42)
155
+ counts = np.random.poisson(10, 100)
156
+ counts[50] = 100
157
+
158
+ qm = QualityMetrics()
159
+ chi_1sigma = qm.sigma_clip_chi_squared(counts=counts, sigma=1.0)
160
+ chi_3sigma = qm.sigma_clip_chi_squared(counts=counts, sigma=3.0)
161
+
162
+ assert chi_1sigma <= chi_3sigma
163
+
164
+
165
+ def test_sigma_clip_2d_arrays():
166
+ """Test sigma clipping works with 2D arrays."""
167
+ np.random.seed(42)
168
+ counts = np.random.poisson(10, (8, 100))
169
+ counts[:, 50] = 100
170
+
171
+ qm = QualityMetrics()
172
+ chi_raw = qm.raw_chi_squared(counts=counts)
173
+ chi_clipped = qm.sigma_clip_chi_squared(counts=counts, sigma=3.0)
174
+
175
+ assert chi_clipped < chi_raw
176
+
177
+
178
+ def test_gti_chi_squared_filters_correctly(mock_lightcurve):
179
+ """Test GTI filtering applies correctly."""
180
+ qm = QualityMetrics(mock_lightcurve, binsize=1.0, emin=30, emax=300, local_time=False)
181
+
182
+ chi = qm.gti_chi_squared()
183
+
184
+ assert np.isfinite(chi)
185
+ assert chi > 0
186
+
187
+
188
+ def test_gti_chi_squared_no_overlap():
189
+ """Test GTI chi-squared raises error when no overlap."""
190
+ np.random.seed(42)
191
+ time = np.linspace(0, 100, 100)
192
+ counts = np.random.poisson(10, (8, 100))
193
+ gtis = np.array([[200, 300]])
194
+
195
+ qm = QualityMetrics()
196
+
197
+ with pytest.raises(ValueError, match="No overlap"):
198
+ qm.gti_chi_squared(time=time, counts=counts, gtis=gtis)
199
+
200
+
201
+ def test_gti_chi_squared_custom_gtis():
202
+ """Test GTI chi-squared with custom GTI array."""
203
+ np.random.seed(42)
204
+ time = np.linspace(0, 100, 100)
205
+ counts = np.random.poisson(10, (8, 100))
206
+ gtis = np.array([[0, 50], [75, 100]])
207
+
208
+ qm = QualityMetrics()
209
+ chi = qm.gti_chi_squared(time=time, counts=counts, gtis=gtis)
210
+
211
+ assert np.isfinite(chi)
212
+ assert chi > 0
213
+
214
+
215
+ def test_gti_chi_squared_return_all():
216
+ """Test GTI chi-squared with return_all=True."""
217
+ np.random.seed(42)
218
+ time = np.linspace(0, 100, 100)
219
+ counts = np.random.poisson(10, (8, 100))
220
+ gtis = np.array([[0, 100]])
221
+
222
+ qm = QualityMetrics()
223
+ chi, dof, no_counts = qm.gti_chi_squared(time=time, counts=counts, gtis=gtis, return_all=True)
224
+
225
+ assert chi.shape == (8,)
226
+ assert dof.shape == (8,)
227
+ assert no_counts.shape == (8,)
228
+
229
+
230
+ def test_chi_squared_weighting():
231
+ """Test chi-squared weighting by total counts."""
232
+ counts = np.array(
233
+ [
234
+ np.concatenate([np.ones(50) * 5, np.ones(50) * 50]),
235
+ np.ones(100) * 100,
236
+ ]
237
+ )
238
+
239
+ qm = QualityMetrics()
240
+ chi_weighted = qm.raw_chi_squared(counts=counts)
241
+ chi_1, chi_2 = qm.raw_chi_squared(counts=counts, return_all=True)[0]
242
+
243
+ # Weighted average closer to high-count module
244
+ assert abs(chi_weighted - chi_2) < abs(chi_weighted - chi_1)
245
+
246
+
247
+ def test_chi_squared_all_nan():
248
+ """Test chi-squared handles all-NaN arrays."""
249
+ counts = np.full(100, np.nan)
250
+ qm = QualityMetrics()
251
+
252
+ chi = qm.raw_chi_squared(counts=counts)
253
+
254
+ assert np.isnan(chi)
255
+
256
+
257
+ def test_integration_with_real_lightcurve(mock_lightcurve):
258
+ """Test full workflow with real LightCurve."""
259
+ qm = QualityMetrics(mock_lightcurve, binsize=1.0, emin=30, emax=300, local_time=False)
260
+
261
+ chi_raw = qm.raw_chi_squared()
262
+ chi_clipped = qm.sigma_clip_chi_squared(sigma=3.0)
263
+ chi_gti = qm.gti_chi_squared()
264
+
265
+ assert np.isfinite(chi_raw)
266
+ assert np.isfinite(chi_clipped)
267
+ assert np.isfinite(chi_gti)
268
+ assert qm.module_data is not None
@@ -1,39 +0,0 @@
1
- import pytest
2
- import numpy as np
3
- from isgri.utils.quality import QualityMetrics
4
-
5
-
6
- def test_quality_metrics_init():
7
- """Test QualityMetrics initialization."""
8
- qm = QualityMetrics(binsize=1.0, emin=30, emax=300)
9
- assert qm.lightcurve is None
10
- assert qm.binsize == 1.0
11
-
12
- def test_chi_squared_raw_constant():
13
- """Test chi-squared for constant lightcurve (should be ~0)."""
14
- counts = np.ones(100) * 10
15
- qm = QualityMetrics()
16
- chi = qm.raw_chi_squared(counts=counts)
17
- assert chi < 0.1 # Should be very small
18
-
19
-
20
- def test_chi_squared_raw_poisson():
21
- """Test chi-squared for Poisson-like data."""
22
- np.random.seed(42)
23
- counts = np.random.poisson(10, 100)
24
- qm = QualityMetrics()
25
- chi = qm.raw_chi_squared(counts=counts)
26
- assert 0.5 < chi < 2.0 # Should be ~1 for Poisson
27
-
28
-
29
- def test_chi_squared_clipped():
30
- """Test sigma-clipped chi-squared removes outliers."""
31
- counts = np.ones(100) * 10
32
- counts[50] = 100 # Add outlier
33
-
34
- qm = QualityMetrics()
35
- chi_raw = qm.raw_chi_squared(counts=counts)
36
- chi_clipped = qm.sigma_clip_chi_squared(counts=counts, sigma=3.0)
37
-
38
- assert chi_clipped < chi_raw # Clipped should be smaller
39
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes