11# This code is a part of X-ray: Generate and Analyse (XGA), a module designed for the XMM Cluster Survey (XCS).
2- # Last modified by David J Turner (turne540@msu.edu) 07/08 /2025, 16:40 . Copyright (c) The Contributors
2+ # Last modified by David J Turner (turne540@msu.edu) 09/12 /2025, 17:17 . Copyright (c) The Contributors
33
44import os
5- import warnings
65from functools import wraps
76# from multiprocessing.dummy import Pool
87from multiprocessing import Pool
1817from tqdm import tqdm
1918
2019from .. import XSPEC_VERSION
21- from ..exceptions import XSPECFitError , MultipleMatchError , NoMatchFoundError , XSPECNotFoundError , XGADeveloperError
20+ from ..exceptions import MultipleMatchError , NoMatchFoundError , XSPECNotFoundError , XGADeveloperError
2221from ..samples .base import BaseSample
2322from ..sources import BaseSource
2423
24+ XGA_XSPEC_ERRS = ["No acceptable spectra are left after the minimum channel step" ,
25+ "No acceptable spectra are left after the cleaning step" ]
2526
2627def execute_cmd (x_script : str , out_file : str , src : str , run_type : str , timeout : float ) \
2728 -> Tuple [Union [FITS , str ], str , bool , list , list , str ]:
@@ -45,7 +46,7 @@ def execute_cmd(x_script: str, out_file: str, src: str, run_type: str, timeout:
4546 usable = True
4647
4748 # We're going to make a temporary pfiles directory which is a) local to the XGA directory, and thus sure to
48- # be on the same filesystem (can be a performance issue for HPCs I think), and b) is unique to a particular
49+ # be on the same filesystem (can be a performance issue for HPCs I think); and b) is unique to a particular
4950 # fit process, so there shouldn't be any clashes. The temporary file name is randomly generated
5051 tmp_ident = str (randint (0 , int (100_000_000 )))
5152 tmp_hea_dir = os .path .join (os .path .dirname (out_file ), tmp_ident , 'pfiles/' )
@@ -65,7 +66,7 @@ def execute_cmd(x_script: str, out_file: str, src: str, run_type: str, timeout:
6566 out , err = xspec_proc .communicate ()
6667 # Need to infer the name of the source to supply it in the warning
6768 source_name = x_script .split ('/' )[- 1 ].split ("_" )[0 ]
68- warnings . warn ("An XSPEC fit for {} has timed out" .format (source_name ), stacklevel = 2 )
69+ warn ("An XSPEC fit for {} has timed out" .format (source_name ), stacklevel = 2 )
6970 usable = False
7071
7172 out = out .decode ("UTF-8" ).split ("\n " )
@@ -77,8 +78,8 @@ def execute_cmd(x_script: str, out_file: str, src: str, run_type: str, timeout:
7778 # We ignore that particular string in the errors identified from stdout because if we don't just it being
7879 # present in the if statement in the executed script is enough to make XGA think that the fit failed, even if
7980 # that error message was never printed at all
80- err_out_lines = [line .split ("***Error: " )[- 1 ] for line in out if "***Error" in line
81- if "No acceptable spectra are left after the cleaning step" not in line ]
81+ err_out_lines = [line .split ("***Error: " )[- 1 ] for line in out
82+ if "***Error" in line and all ([ xxe not in line for xxe in XGA_XSPEC_ERRS ]) ]
8283 err_out_line_init = [line for line in out if "You do not have an up-to-date version of Xspec.init" in line ]
8384 warn_out_lines = [line .split ("***Warning: " )[- 1 ] for line in out if "***Warning" in line ]
8485 err_err_lines = [line .split ("***Error: " )[- 1 ] for line in err if "***Error" in line ]
@@ -89,8 +90,8 @@ def execute_cmd(x_script: str, out_file: str, src: str, run_type: str, timeout:
8990 else :
9091 usable = False
9192
92- error = err_out_lines + err_err_lines + err_out_line_init
93- warn = warn_out_lines + warn_err_lines
93+ cur_error = err_out_lines + err_err_lines + err_out_line_init
94+ cur_warn = warn_out_lines + warn_err_lines
9495
9596 if os .path .exists (out_file + "_info.csv" ) and run_type == "fit" and usable :
9697 # The original version of the xga_output.tcl script output everything as one nice neat fits file
@@ -132,28 +133,29 @@ def execute_cmd(x_script: str, out_file: str, src: str, run_type: str, timeout:
132133 elif not os .path .exists (out_file ):
133134 usable = False
134135 res_tables = None
135- error .append ('Results table not written.' )
136+ cur_error .append ('Results table not written.' )
136137 else :
137138 res_tables = None
138139 usable = False
139140
140- # This uses outfile name (which has a structure set by XGA, so this is reliable) to figure out which telescope
141- # this script was run for - this isn't necessarily how I would have designed it from the beginning, but is a
142- # good solution to get the telescope which doesn't require adding stuff to all the user-facing XSPEC functions
141+ # This uses outfile name (has structure set by XGA, so this is reliable) to
142+ # figure out which telescope this script was run for. This isn't necessarily
143+ # how I would have designed it from the beginning, but is a good solution to get
144+ # the telescope which doesn't require adding stuff to all the user-facing
145+ # XSPEC functions
143146 if run_type == "conv_factors" :
144147 tel = out_file .split ('_' )[- 3 ]
145148 else :
146149 tel = out_file .split ('_' )[- 1 ].split ('.' )[0 ]
147150
148- return res_tables , src , usable , error , warn , tel
151+ return res_tables , src , usable , cur_error , cur_warn , tel
149152
150153
151154def xspec_call (xspec_func ):
152155 """
153156 This is used as a decorator for functions that produce XSPEC scripts. Depending on the
154157 system that XGA is running on (and whether the user requests parallel execution), the method of
155158 executing the XSPEC commands will change. This supports multi-threading.
156- :return:
157159 """
158160
159161 @wraps (xspec_func )
@@ -200,14 +202,14 @@ def wrapper(*args, **kwargs):
200202 with tqdm (total = len (script_list ), desc = desc ) as fit , Pool (cores ) as pool :
201203 def callback (results_in ):
202204 """
203- Callback function for the apply_async pool method, gets called when a task finishes
204- and something is returned.
205+ Callback function for the apply_async pool method which gets
206+ called when a task finishes and something is returned.
205207 """
206208 nonlocal fit # The progress bar will need updating
207209 nonlocal results # The dictionary the command call results are added to
208210
209- res_fits , rel_src , successful , err_list , warn_list , tel = results_in
210- results [rel_src ].append ([res_fits , successful , err_list , warn_list , tel ])
211+ res_fits , rel_src , successful , cur_err_list , cur_warn_list , cur_tel = results_in
212+ results [rel_src ].append ([res_fits , successful , cur_err_list , cur_warn_list , cur_tel ])
211213 fit .update (1 )
212214
213215 for s_ind , s in enumerate (script_list ):
@@ -238,7 +240,6 @@ def callback(results_in):
238240 for res_set in results [src_repr ]:
239241 # res_set[0] = res_table, if it is None then the xspec fit has failed
240242 if res_set [0 ] is None :
241- print (res_set )
242243 for err in res_set [2 ]:
243244 errors_all_sources .append (err + " - {s}" .format (s = s .name ))
244245 # Extract the telescope from the information passed back by the running of the fit
0 commit comments