Skip to content

Commit 00e4253

Browse files
Merge pull request #187 from StingraySoftware/use_memmap_in_zsearch
Use memmap in zsearch
2 parents f7f3ca6 + 38973a1 commit 00e4253

5 files changed

Lines changed: 198 additions & 74 deletions

File tree

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
r"https://.*adsabs.harvard.edu/",
172172
r"https://zenodo.org/",
173173
r"http.*://stackoverflow.com/questions/.*",
174-
r"https://*.nasa.gov/*",
174+
r"https://.*.nasa.gov/.*",
175175
]
176176

177177
# -- Options for the edit_on_github extension ---------------------------------

hendrics/efsearch.py

Lines changed: 160 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ def transient_search(
517517
t0=None,
518518
t1=None,
519519
oversample=4,
520+
force_memmap=False,
520521
):
521522
"""Search for transient pulsations.
522523
@@ -591,11 +592,8 @@ def transient_search(
591592
if allvalues == []:
592593
allvalues = [0]
593594

594-
all_results = []
595-
all_freqs = []
596-
597595
dt = (times[-1] - times[0]) / nprof
598-
596+
all_results = None
599597
for ii, i in enumerate(show_progress(allvalues)):
600598
offset = step * i
601599
fdot_offset = 0
@@ -605,13 +603,44 @@ def transient_search(
605603
nave, results = _transient_search_step(
606604
times, mean_f, mean_fdot=mean_fdot, nbin=nbin, nprof=nprof, n=n
607605
)
608-
all_results.append(results)
609-
all_freqs.append(mean_f)
606+
if all_results is None:
607+
results_shape = (len(allvalues), nave.size, results.shape[1])
608+
use_memmap = force_memmap
609+
if np.prod(results_shape) > 1e7 or force_memmap:
610+
import tempfile
611+
612+
tmp_results = tempfile.NamedTemporaryFile(delete=True).name + "_hen.npy"
613+
tmp_f = tempfile.NamedTemporaryFile(delete=True).name + "_hen.npy"
614+
log.info(
615+
"Transient search results are very large. "
616+
f"Using memmapped arrays ({tmp_results}; "
617+
f"shape {results_shape}) to reduce memory usage.",
618+
)
619+
all_results = np.lib.format.open_memmap(
620+
tmp_results, mode="w+", dtype=results.dtype, shape=results_shape
621+
)
622+
all_freqs = np.lib.format.open_memmap(
623+
tmp_f, mode="w+", dtype=mean_f.dtype, shape=(len(allvalues),)
624+
)
625+
use_memmap = True
626+
else:
627+
all_results = np.empty(results_shape, dtype=results.dtype)
628+
all_freqs = np.empty((len(allvalues),), dtype=mean_f.dtype)
610629

611-
all_results = np.array(all_results)
612-
all_freqs = np.array(all_freqs)
630+
all_results[ii] = results
631+
all_freqs[ii] = mean_f
613632

614633
times = dt * np.arange(all_results.shape[2])
634+
final_results_shape = (nave.size, all_results.shape[2], all_results.shape[0])
635+
if use_memmap:
636+
tmp_results_stats = tempfile.NamedTemporaryFile(delete=True).name + "_hen.npy"
637+
all_results_stats = np.lib.format.open_memmap(
638+
tmp_results_stats, mode="w+", dtype=results.dtype, shape=final_results_shape
639+
)
640+
else:
641+
all_results_stats = np.empty(final_results_shape, dtype=results.dtype)
642+
for i in range(nave.size):
643+
all_results_stats[i] = all_results[:, i, :].T
615644

616645
results = TransientResults()
617646
results.oversample = oversample
@@ -621,12 +650,43 @@ def transient_search(
621650
results.nave = nave
622651
results.freqs = all_freqs
623652
results.times = times
624-
results.stats = np.array([all_results[:, i, :].T for i in range(nave.size)])
653+
results.stats = all_results_stats
654+
655+
if use_memmap:
656+
os.remove(all_results.filename)
657+
os.remove(all_freqs.filename)
658+
del all_results, all_freqs
625659

626660
return results
627661

628662

629663
def plot_transient_search(results, gif_name=None):
664+
"""Analyze and plot the results of a transient search.
665+
666+
Parameters
667+
----------
668+
results : TransientResults
669+
The results of the transient search.
670+
gif_name : str or None, default None
671+
The name of the gif file to save the plots to. If None, no gif is saved.
672+
"""
673+
return _analyze_and_plot_transient_search(results, gif_name=gif_name, force_plotting=True)
674+
675+
676+
def _analyze_and_plot_transient_search(results, gif_name=None, force_plotting=False):
677+
"""Analyze and plot the results of a transient search.
678+
679+
By default, it will only plot if the results are smaller than 1e7 elements.
680+
681+
Parameters
682+
----------
683+
results : TransientResults
684+
The results of the transient search.
685+
gif_name : str or None, default None
686+
The name of the gif file to save the plots to. If None, no gif is saved.
687+
force_plotting : bool, default False
688+
Whether to force plotting even if the results are too large.
689+
"""
630690
import matplotlib as mpl
631691
import matplotlib.pyplot as plt
632692

@@ -641,7 +701,17 @@ def plot_transient_search(results, gif_name=None):
641701
result_name = gif_name.replace(".gif", ".csv")
642702
max_stats_rows = []
643703
all_images = []
644-
for i, (ima, nave) in enumerate(zip(results.stats, results.nave)):
704+
import tqdm
705+
706+
plot_results = (results.stats.size < 1e7) or force_plotting
707+
if not plot_results:
708+
log.info("Transient search results are too large to plot. Skipping plots.")
709+
else:
710+
log.info("Generating plots for transient search...")
711+
712+
for i, (ima, nave) in tqdm.tqdm(
713+
enumerate(zip(results.stats, results.nave)), total=len(results.nave)
714+
):
645715
f = results.freqs
646716
t = results.times
647717
nprof = ima.shape[0]
@@ -665,20 +735,46 @@ def plot_transient_search(results, gif_name=None):
665735
ntrial=ntrial_sum,
666736
n_summed_spectra=nprof / nave,
667737
)
668-
fig = plt.figure(figsize=(10, 10))
738+
739+
mean_line = np.mean(ima, axis=0) / sum_detl * 3
740+
maxidx = np.argmax(mean_line)
741+
maxline = mean_line[maxidx]
742+
743+
for il, line in enumerate(ima):
744+
line = line / detl * 3
745+
746+
maxidx = np.argmax(mean_line)
747+
# if line[maxidx] > maxline:
748+
best_f = f[maxidx]
749+
maxline = line[maxidx]
750+
751+
max_stats_rows.append({"step": i + 1, "nave": nave, "best_f": best_f, "max_stat": maxline})
752+
753+
if 3.5 < maxline < 5: # pragma: no cover
754+
print(f"{gif_name}: Possible candidate at step {i}: {best_f} Hz (~{maxline:.1f} sigma)")
755+
elif maxline >= 5: # pragma: no cover
756+
print(f"{gif_name}: Candidate at step {i}: {best_f} Hz (~{maxline:.1f} sigma)")
757+
758+
if not plot_results:
759+
continue
760+
761+
fig = plt.figure(figsize=(10, 10), dpi=100)
669762
plt.clf()
670763
gs = plt.GridSpec(2, 2, height_ratios=(1, 3))
671764
for i_f in [0, 1]:
672765
axf = plt.subplot(gs[0, i_f])
673766
axima = plt.subplot(gs[1, i_f], sharex=axf)
674767

675-
axima.pcolormesh(f, t, ima / detl * 3, vmax=3, vmin=0.3, shading="nearest")
768+
axima.pcolormesh(
769+
f, t, ima, vmax=detl, vmin=detl / 10, shading="nearest", rasterized=True
770+
)
676771

677772
mean_line = np.mean(ima, axis=0) / sum_detl * 3
678773
maxidx = np.argmax(mean_line)
679774
maxline = mean_line[maxidx]
680775
best_f = f[maxidx]
681-
for il, line in enumerate(ima / detl * 3):
776+
for il, line in enumerate(ima):
777+
line = line / detl * 3
682778
axf.plot(
683779
f,
684780
line,
@@ -687,17 +783,13 @@ def plot_transient_search(results, gif_name=None):
687783
c="grey",
688784
alpha=0.5,
689785
label=f"{il}",
786+
rasterized=True,
690787
)
691788
maxidx = np.argmax(mean_line)
692789
if line[maxidx] > maxline:
693790
best_f = f[maxidx]
694791
maxline = line[maxidx]
695-
if 3.5 < maxline < 5 and i_f == 0: # pragma: no cover
696-
print(
697-
f"{gif_name}: Possible candidate at step {i}: {best_f} Hz (~{maxline:.1f} sigma)"
698-
)
699-
elif maxline >= 5 and i_f == 0: # pragma: no cover
700-
print(f"{gif_name}: Candidate at step {i}: {best_f} Hz (~{maxline:.1f} sigma)")
792+
701793
axf.plot(f, mean_line, lw=1, c="k", zorder=10, label="mean", ls="-")
702794

703795
axima.set_xlabel("Frequency")
@@ -708,24 +800,26 @@ def plot_transient_search(results, gif_name=None):
708800
xmin = max(best_f - df, results.f0)
709801
xmax = min(best_f + df, results.f1)
710802
if i_f == 0:
711-
max_stats_rows.append(
712-
{"step": i + 1, "nave": nave, "best_f": best_f, "max_stat": maxline}
713-
)
714803
axf.set_xlim([results.f0, results.f1])
715804
axf.axvline(xmin, ls="--", c="b", lw=2)
716805
axf.axvline(xmax, ls="--", c="b", lw=2)
717806
else:
718807
axf.set_xlim([xmin, xmax])
719808

720-
fig.canvas.draw()
721-
image = np.frombuffer(fig.canvas.buffer_rgba().cast("B"), dtype="uint8")
722-
image = image.reshape(fig.canvas.get_width_height()[::-1] + (4,))
809+
fig.canvas.draw()
810+
image = np.frombuffer(fig.canvas.buffer_rgba().cast("B"), dtype="uint8")
811+
image = image.reshape(fig.canvas.get_width_height()[::-1] + (4,))
723812

724813
plt.close(fig)
725814
all_images.append(image)
815+
816+
if hasattr(results.stats, "filename"):
817+
os.remove(results.stats.filename)
818+
726819
vstack(max_stats_rows).write(result_name, overwrite=True)
727820

728-
imageio.v3.imwrite(gif_name, all_images, duration=1000.0)
821+
if plot_results:
822+
imageio.v3.imwrite(gif_name, all_images, duration=1000.0)
729823

730824
return all_images
731825

@@ -856,6 +950,7 @@ def search_with_qffa(
856950
t0=None,
857951
t1=None,
858952
silent=False,
953+
force_memmap=False,
859954
):
860955
"""'Quite fast folding' algorithm.
861956
@@ -930,9 +1025,7 @@ def search_with_qffa(
9301025
if allvalues == []:
9311026
allvalues = [0]
9321027

933-
all_fgrid = []
934-
all_fdotgrid = []
935-
all_stats = []
1028+
all_fgrid = None
9361029

9371030
local_show_progress = show_progress
9381031
if silent:
@@ -961,16 +1054,35 @@ def local_show_progress(x):
9611054
)
9621055

9631056
if all_fgrid is None:
964-
all_fgrid = fgrid
965-
all_fdotgrid = fdotgrid
966-
all_stats = stats
967-
else:
968-
all_fgrid.append(fgrid)
969-
all_fdotgrid.append(fdotgrid)
970-
all_stats.append(stats)
971-
all_fgrid = np.vstack(all_fgrid)
972-
all_fdotgrid = np.vstack(all_fdotgrid)
973-
all_stats = np.vstack(all_stats)
1057+
fgrid_shape = fgrid.shape
1058+
all_fgrid_shape = (fgrid_shape[0] * len(allvalues), fgrid_shape[1])
1059+
log.info(f"Initializing result arrays of shape {all_fgrid_shape}")
1060+
if all_fgrid_shape[0] * all_fgrid_shape[1] > 1e7 or force_memmap:
1061+
import tempfile
1062+
1063+
log.info(
1064+
"Large result arrays detected, using memory-mapped files to reduce "
1065+
"memory usage."
1066+
)
1067+
tmp_f = tempfile.NamedTemporaryFile("w+").name + "_hen.npy"
1068+
tmp_fdot = tempfile.NamedTemporaryFile("w+").name + "_hen.npy"
1069+
tmp_stat = tempfile.NamedTemporaryFile("w+").name + "_hen.npy"
1070+
all_fgrid = np.lib.format.open_memmap(
1071+
tmp_f, mode="w+", dtype=fgrid.dtype, shape=all_fgrid_shape
1072+
)
1073+
all_fdotgrid = np.lib.format.open_memmap(
1074+
tmp_fdot, mode="w+", dtype=fgrid.dtype, shape=all_fgrid_shape
1075+
)
1076+
all_stats = np.lib.format.open_memmap(
1077+
tmp_stat, mode="w+", dtype=fgrid.dtype, shape=all_fgrid_shape
1078+
)
1079+
else:
1080+
all_fgrid = np.zeros(all_fgrid_shape)
1081+
all_fdotgrid = np.zeros(all_fgrid_shape)
1082+
all_stats = np.zeros(all_fgrid_shape)
1083+
all_fgrid[ii * fgrid_shape[0] : (ii + 1) * fgrid_shape[0], :] = fgrid
1084+
all_fdotgrid[ii * fdotgrid.shape[0] : (ii + 1) * fdotgrid.shape[0], :] = fdotgrid
1085+
all_stats[ii * stats.shape[0] : (ii + 1) * stats.shape[0], :] = stats
9741086

9751087
step = np.median(np.diff(all_fgrid[:, 0]))
9761088
fdotstep = np.median(np.diff(all_fdotgrid[0]))
@@ -1647,6 +1759,12 @@ def _common_parser(args=None):
16471759
help="The number of harmonics to use in the search "
16481760
"(the 'N' in Z^2_N; only relevant to Z search!)",
16491761
)
1762+
parser.add_argument(
1763+
"--force-memmap",
1764+
help="Force the use of memory-mapped files",
1765+
default=False,
1766+
action="store_true",
1767+
)
16501768

16511769
args = check_negative_numbers_in_args(args)
16521770
_add_default_args(parser, ["deorbit", "loglevel", "debug"])
@@ -1730,8 +1848,9 @@ def _common_main(args, func):
17301848
n=n,
17311849
nprof=args.n_transient_intervals,
17321850
oversample=oversample,
1851+
force_memmap=args.force_memmap,
17331852
)
1734-
plot_transient_search(results, out_fname + "_transient.gif")
1853+
_analyze_and_plot_transient_search(results, out_fname + "_transient.gif")
17351854
if not args.fast and not args.ffa:
17361855
continue
17371856

@@ -1777,6 +1896,7 @@ def _common_main(args, func):
17771896
npfact=args.npfact,
17781897
oversample=oversample,
17791898
search_fdot=search_fdot,
1899+
force_memmap=args.force_memmap,
17801900
)
17811901

17821902
ref_time = (events.time[-1] + events.time[0]) / 2

0 commit comments

Comments
 (0)