Skip to content

Commit 0669172

Browse files
Complete creation of PDSs. Test all attributes
1 parent d86c51e commit 0669172

2 files changed

Lines changed: 74 additions & 81 deletions

File tree

hendrics/parallel.py

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import os
2-
import subprocess as sp
3-
import time
41
from functools import partial
52
from multiprocessing import Pool
63

7-
import matplotlib.pyplot as plt
84
import numpy as np
95
from mpi4py import MPI
106
from stingray import AveragedPowerspectrum, EventList
@@ -16,8 +12,8 @@
1612

1713

1814
def get_data_intervals(interval_idxs, info=None, fname=None, sample_time=None):
19-
"""
20-
Generate light curves for specified time intervals from event data.
15+
"""Generate light curves for specified time intervals from event data.
16+
2117
Parameters
2218
----------
2319
interval_idxs : array-like
@@ -28,17 +24,18 @@ def get_data_intervals(interval_idxs, info=None, fname=None, sample_time=None):
2824
Path to the FITS file containing event data.
2925
sample_time : float, optional
3026
The time resolution (bin width) for the light curve histogram.
27+
3128
Yields
3229
------
3330
lc : array-like
3431
Histogrammed light curve for each specified interval.
32+
3533
Notes
3634
-----
3735
- Uses FITSTimeseriesReader to read event data from the FITS file.
3836
- Each yielded light curve corresponds to one interval in `interval_idxs`.
3937
- The number of bins is determined by the interval duration and `sample_time`.
4038
"""
41-
4239
tsreader = FITSTimeseriesReader(fname, output_class=EventList)
4340
time_intervals = info["interval_times"][interval_idxs]
4441
if np.shape(time_intervals) == (2,):
@@ -54,8 +51,8 @@ def get_data_intervals(interval_idxs, info=None, fname=None, sample_time=None):
5451

5552

5653
def single_rank_intervals(this_ranks_intervals, sample_time=None, info=None, fname=None):
57-
"""
58-
Generate an averaged powerspectrum from light curve intervals for a single rank.
54+
"""Generate an averaged powerspectrum from light curve intervals for a single rank.
55+
5956
Parameters
6057
----------
6158
this_ranks_intervals : list or array-like
@@ -66,18 +63,19 @@ def single_rank_intervals(this_ranks_intervals, sample_time=None, info=None, fna
6663
Dictionary containing metadata about the intervals, including "interval_times".
6764
fname : str or None, optional
6865
Filename or path to the data source, if required by `get_data_intervals`.
66+
6967
Returns
7068
-------
7169
pds : AveragedPowerspectrum
7270
The averaged powerspectrum computed from the light curve intervals.
7371
nbin : int
7472
Number of bins in each interval, calculated from the interval duration and sample time.
73+
7574
Notes
7675
-----
7776
This function extracts light curve data for the specified intervals, computes the number of bins,
7877
and returns the averaged powerspectrum using the "leahy" normalization.
7978
"""
80-
8179
# print(kwargs)
8280
t_int = info["interval_times"][0]
8381
nbin = int(np.rint((t_int[1] - t_int[0]) / sample_time))
@@ -98,10 +96,7 @@ def single_rank_intervals(this_ranks_intervals, sample_time=None, info=None, fna
9896

9997

10098
def main_none(fname, sample_time, segment_size):
101-
"""
102-
Process a FITS timeseries file and compute an averaged powerspectrum.
103-
This function reads event data from a FITS file, processes it using the Stingray
104-
library, and computes an averaged powerspectrum with Leahy normalization.
99+
"""Process a FITS timeseries file and compute an averaged powerspectrum. This function reads event data from a FITS file, processes it using the Stingray library, and computes an averaged powerspectrum with Leahy normalization.
105100
106101
Parameters
107102
----------
@@ -111,6 +106,7 @@ def main_none(fname, sample_time, segment_size):
111106
The time resolution (bin size) to use when processing the data.
112107
segment_size : float
113108
The size of each segment (in seconds) for averaging the powerspectrum.
109+
114110
Returns
115111
-------
116112
freq : numpy.ndarray
@@ -123,7 +119,6 @@ def main_none(fname, sample_time, segment_size):
123119
This function uses the standard Stingray processing pipeline and does not
124120
parallelize the computation.
125121
"""
126-
127122
logger.info("Using standard Stingray processing")
128123
tsreader = FITSTimeseriesReader(fname, output_class=EventList)
129124

@@ -153,6 +148,7 @@ def main_mpi(fname, sample_time, segment_size):
153148
5. Partial results are combined using a binary tree reduction, where ranks pair up and
154149
send/receive data until only one rank holds the final result.
155150
6. The final result is normalized and frequency bins are computed.
151+
156152
Parameters
157153
----------
158154
fname : str
@@ -161,6 +157,7 @@ def main_mpi(fname, sample_time, segment_size):
161157
The sampling time for the time series analysis.
162158
segment_size : float
163159
The size of each segment (interval) to be processed.
160+
164161
Returns
165162
-------
166163
output : AveragedPowerspectrum
@@ -172,7 +169,6 @@ def main_mpi(fname, sample_time, segment_size):
172169
and dependencies are available.
173170
- Only the rank responsible for the final reduction returns the results; other ranks return None.
174171
"""
175-
176172
tsreader = FITSTimeseriesReader(fname, output_class=EventList)
177173

178174
def data_lookup():
@@ -184,6 +180,7 @@ def data_lookup():
184180
"gtis": tsreader.gti,
185181
"interval_times": interval_times,
186182
"n_intervals": len(interval_times),
183+
"nphots": tsreader.nphot / tsreader.exposure * segment_size,
187184
}
188185

189186
world_comm = MPI.COMM_WORLD
@@ -280,6 +277,12 @@ def data_lookup():
280277
output.m = total_n_intervals
281278
output.gti = info["gtis"]
282279
output.dt = sample_time
280+
output.power_err = totals / np.sqrt(total_n_intervals)
281+
output.unnorm_power = totals / 2 * info["nphots"]
282+
output.unnorm_power_err = output.unnorm_power / np.sqrt(total_n_intervals)
283+
output.norm = "leahy"
284+
output.nphots = info["nphots"]
285+
output.n = np.rint(segment_size / sample_time).astype(int)
283286

284287
return output
285288

@@ -315,7 +318,6 @@ def main_multiprocessing(fname, sample_time, segment_size, world_size=8):
315318
-------
316319
output : AveragedPowerspectrum
317320
The averaged power spectrum computed across all intervals and processes.
318-
319321
"""
320322

321323
def data_lookup():
@@ -354,6 +356,7 @@ def data_lookup():
354356
p = Pool(world_size)
355357

356358
totals = 0
359+
nphots = 0
357360
for results, data_size in p.imap_unordered(
358361
partial(
359362
single_rank_intervals,
@@ -364,8 +367,10 @@ def data_lookup():
364367
this_ranks_intervals,
365368
):
366369
totals += results.power * results.m
370+
nphots += results.nphots * results.m
367371
logger.debug("Results")
368372
totals /= total_n_intervals
373+
nphots /= total_n_intervals
369374

370375
freq = np.fft.fftfreq(data_size, d=sample_time)[positive_fft_bins(data_size)]
371376

@@ -375,6 +380,12 @@ def data_lookup():
375380
output.m = total_n_intervals
376381
output.gti = info["gtis"]
377382
output.dt = sample_time
383+
output.power_err = totals / np.sqrt(total_n_intervals)
384+
output.unnorm_power = totals / 2 * nphots
385+
output.unnorm_power_err = output.unnorm_power / np.sqrt(total_n_intervals)
386+
output.norm = "leahy"
387+
output.nphots = nphots
388+
output.n = np.rint(segment_size / sample_time).astype(int)
378389

379390
return output
380391

@@ -401,10 +412,12 @@ def main(args=None):
401412
"--sample_time",
402413
type=float,
403414
default=1 / 8129 / 2,
404-
help="Light curve bin time; if negative, interpreted"
405-
+ " as negative power of 2."
406-
+ " Default: 2^-13, or keep input lc bin time"
407-
+ " (whatever is larger)",
415+
help=(
416+
"Light curve bin time; if negative, interpreted"
417+
" as negative power of 2."
418+
" Default: 2^-13, or keep input lc bin time"
419+
" (whatever is larger)"
420+
),
408421
)
409422

410423
parser.add_argument(
@@ -455,7 +468,10 @@ def main(args=None):
455468
pds = main_multiprocessing(fname, sample_time, segment_size, world_size=args.nproc)
456469
else:
457470
pds = main_none(fname, sample_time, segment_size)
471+
458472
if pds is None:
459473
return
474+
if args.norm != "leahy":
475+
pds = pds.to_norm(args.norm)
460476

461477
pds.write(args.outfname)

hendrics/tests/test_parallel.py

Lines changed: 37 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import subprocess as sp
22
import tempfile
3+
34
import numpy as np
4-
from stingray import EventList, AveragedPowerspectrum
5+
import pytest
6+
from stingray import AveragedPowerspectrum, EventList
7+
58
from hendrics.fake import main as main_fake
69
from hendrics.parallel import main as main_parallel
710

@@ -18,66 +21,40 @@ def setup_class(cls):
1821
cls.events, dt=0.1, segment_size=10.0, use_common_mean=False, norm="leahy"
1922
)
2023

21-
def test_none_version(self):
24+
@pytest.mark.parametrize("method", ["none", "multiprocessing", "mpi"])
25+
@pytest.mark.parametrize("norm", ["leahy", "frac", "abs"])
26+
def test_parallel_versions(self, method, norm):
2227
out_file = tempfile.NamedTemporaryFile(suffix=".hdf5", delete=False)
23-
main_parallel(
24-
[
25-
self.fname,
26-
"-o",
27-
out_file.name,
28-
"-b",
29-
"0.1",
30-
"-f",
31-
"10.0",
32-
"--method",
33-
"none",
34-
]
35-
)
36-
pds = AveragedPowerspectrum.read(out_file.name)
37-
assert pds.m == self.pds.m
38-
assert np.allclose(pds.freq, self.pds.freq)
39-
assert np.allclose(pds.power, self.pds.power, rtol=1e-6)
28+
command = [
29+
self.fname,
30+
"-o",
31+
out_file.name,
32+
"-b",
33+
"0.1",
34+
"-f",
35+
"10.0",
36+
"--method",
37+
method,
38+
"--norm",
39+
norm,
40+
]
41+
if method == "mpi":
42+
command = ["mpirun", "-n", "4", "HENparfspec"] + command
43+
sp.check_call(command)
44+
else:
45+
main_parallel(command)
4046

41-
def test_multiprocessing_version(self):
42-
out_file = tempfile.NamedTemporaryFile(suffix=".hdf5", delete=False)
43-
main_parallel(
44-
[
45-
self.fname,
46-
"-o",
47-
out_file.name,
48-
"-b",
49-
"0.1",
50-
"-f",
51-
"10.0",
52-
"--method",
53-
"multiprocessing",
54-
]
55-
)
5647
pds = AveragedPowerspectrum.read(out_file.name)
57-
assert pds.m == self.pds.m
58-
assert np.allclose(pds.freq, self.pds.freq)
59-
assert np.allclose(pds.power, self.pds.power, rtol=1e-4)
48+
compare_pds = self.pds.to_norm(norm) if norm != "leahy" else self.pds
49+
assert pds.m == compare_pds.m
50+
assert np.allclose(pds.freq, compare_pds.freq)
51+
if norm == "leahy":
52+
power_rtol = 1e-6
53+
else:
54+
power_rtol = 1e-2
6055

61-
def test_mpi_version(self):
62-
out_file = tempfile.NamedTemporaryFile(suffix=".hdf5", delete=False)
63-
sp.check_call(
64-
[
65-
"mpirun",
66-
"-n",
67-
"4",
68-
"HENparfspec",
69-
self.fname,
70-
"-o",
71-
out_file.name,
72-
"-b",
73-
"0.1",
74-
"-f",
75-
"10.0",
76-
"--method",
77-
"mpi",
78-
]
79-
)
80-
pds = AveragedPowerspectrum.read(out_file.name)
81-
assert pds.m == self.pds.m
82-
assert np.allclose(pds.freq, self.pds.freq)
83-
assert np.allclose(pds.power, self.pds.power, rtol=1e-4)
56+
assert np.allclose(pds.power, compare_pds.power, rtol=power_rtol)
57+
assert np.allclose(pds.power_err, compare_pds.power_err, rtol=1e-2)
58+
assert pds.norm == compare_pds.norm
59+
assert np.allclose(pds.unnorm_power, compare_pds.unnorm_power, rtol=1e-2)
60+
assert np.isclose(pds.nphots, compare_pds.nphots, rtol=1e-2)

0 commit comments

Comments
 (0)