@@ -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
629663def 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