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) 27 /03/2025, 11:43 . Copyright (c) The Contributors
2+ # Last modified by David J Turner (turne540@msu.edu) 29 /03/2025, 05:01 . Copyright (c) The Contributors
33
44import warnings
55from inspect import signature , Parameter
@@ -926,34 +926,26 @@ def blackbody(sources: Union[BaseSource, BaseSample], outer_radius: Union[str, Q
926926 return script_paths , outfile_paths , num_cores , run_type , src_inds , None , timeout , model , fit_confs , inv_ents
927927
928928
929- # If the spectrum checking step of the XSPEC fit is enabled (using the boolean flag spectrum_checking), then
930- # each individual spectrum available for a given source will be fitted, and if the measured temperature is less
931- # than or equal to 0.01keV, or greater than 20keV, or the temperature uncertainty is greater than 15keV, then
932- # that spectrum will be rejected and not included in the final fit. Spectrum checking also involves rejecting any
933- # spectra with fewer than 10 noticed channels.
934- #
935- # Freezing the temperature value of the fit is also possible, in cases where the data may not be sufficient to
936- # constrain it, and an external temperature constrain is used (by passing to the 'start_temp' argument).
937-
938-
939929@xspec_call
940- def n_temp_apec (sources : Union [BaseSource , BaseSample ], outer_radius : Union [str , Quantity ],
941- num_temp_comp : Union [int , List [int ]], inner_radius : Union [str , Quantity ] = Quantity (0 , 'arcsec' ),
942- start_temp : Quantity = Quantity (3.0 , "keV" ), start_met : float = 0.3 ,
930+ def double_temp_apec (sources : Union [BaseSource , BaseSample ], outer_radius : Union [str , Quantity ],
931+ inner_radius : Union [str , Quantity ] = Quantity (0 , 'arcsec' ),
932+ start_temp_one : Quantity = Quantity (3.0 , "keV" ), start_temp_two : Quantity = Quantity (5.0 , "keV" ),
933+ start_met_one : float = 0.3 , start_met_two : float = 0.3 ,
943934 lum_en : Quantity = Quantity ([[0.5 , 2.0 ], [0.01 , 100.0 ]], "keV" ), freeze_nh : bool = True ,
944- freeze_met : bool = True , freeze_temp : bool = False , lo_en : Quantity = Quantity (0.3 , "keV" ),
935+ freeze_met_one : bool = True , freeze_met_two : bool = True , freeze_temp_one : bool = False ,
936+ freeze_temp_two : bool = False , lo_en : Quantity = Quantity (0.3 , "keV" ),
945937 hi_en : Quantity = Quantity (7.9 , "keV" ), par_fit_stat : float = 1. , lum_conf : float = 68. ,
946938 abund_table : str = "angr" , fit_method : str = "leven" , group_spec : bool = True , min_counts : int = 5 ,
947939 min_sn : float = None , over_sample : float = None , one_rmf : bool = True , num_cores : int = NUM_CORES ,
948- spectrum_checking : bool = True , timeout : Quantity = Quantity (1 , 'hr' )):
940+ spectrum_checking : bool = False , timeout : Quantity = Quantity (1 , 'hr' )):
949941 """
950- This is a convenience function for fitting an absorbed multi -temperature apec model(constant*tbabs*apec) to an
951- object's spectrum - the number of temperature components (i.e. APEC models) is set using the 'num_temp_comp'
952- argument. If there are no existing spectra with the passed settings, then they will be generated automatically.
942+ This is a convenience function for fitting an absorbed double -temperature apec model (constant*tbabs*( apec+apec))
943+ to an object's spectrum. If there are no existing spectra with the passed settings, then they will be
944+ generated automatically.
953945
954946 BE AWARE that higher quality data (i.e. higher signal-to-noise spectra, with a greater number of X-ray counts) are
955- required to fit models with more free parameters, so the more temperature components you include, the less likely
956- the model fit is to be successful .
947+ required to fit models with more free parameters, so this model fit is less likely to converge than single
948+ temperature apec fits for lower quality data .
957949
958950 :param List[BaseSource] sources: A single source object, or a sample of sources.
959951 :param str/Quantity outer_radius: The name or value of the outer radius of the region that the
@@ -965,14 +957,24 @@ def n_temp_apec(sources: Union[BaseSource, BaseSample], outer_radius: Union[str,
965957 desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster,
966958 or Quantity(1000, 'kpc')). By default this is zero arcseconds, resulting in a circular spectrum. If
967959 you are fitting for multiple sources then you can also pass a Quantity with one entry per source.
968- :param Quantity start_temp: The initial temperature for the fit, the default is 3 keV. This value can also be
969- a non-scalar Quantity, with an entry for every source in a sample (this is most useful when used with the
970- 'freeze_temp' argument, to provide some external constraint on temperature for objects with poor data).
971- :param start_met: The initial metallicity for the fit (in ZSun).
960+ :param Quantity start_temp_one: The initial temperature of the first APEC model, the default is 3 keV. This value
961+ can also be a non-scalar Quantity, with an entry for every source in a sample (this is most useful when
962+ used with the 'freeze_temp' argument, to provide some external constraint on temperature for objects with
963+ poor data).
964+ :param Quantity start_temp_two: The initial temperature of the second APEC model, the default is 3 keV. This value
965+ can also be a non-scalar Quantity, with an entry for every source in a sample (this is most useful when
966+ used with the 'freeze_temp' argument, to provide some external constraint on temperature for objects with
967+ poor data).
968+ :param start_met_one: The initial metallicity of the first APEC model (in ZSun).
969+ :param start_met_two: The initial metallicity of the second APEC model (in ZSun).
972970 :param Quantity lum_en: Energy bands in which to measure luminosity.
973971 :param bool freeze_nh: Whether the hydrogen column density should be frozen. Default is True.
974- :param bool freeze_met: Whether the metallicity parameter in the fit should be frozen. Default is True.
975- :param bool freeze_temp: Whether the temperature parameter in the fit should be frozen. Default is False
972+ :param bool freeze_met_one: Whether the metallicity of the first APEC model should be frozen. Default is True.
973+ :param bool freeze_met_two: Whether the metallicity of the second APEC model should be frozen. Default is True.
974+ :param bool freeze_temp_one: Whether the temperature parameter of the first APEC model should be
975+ frozen. Default is False
976+ :param bool freeze_temp_two: Whether the temperature parameter of the second APEC model should be
977+ frozen. Default is False
976978 :param Quantity lo_en: The lower energy limit for the data to be fitted.
977979 :param Quantity hi_en: The upper energy limit for the data to be fitted.
978980 :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command, default is 1.0 which should be
@@ -985,7 +987,7 @@ def n_temp_apec(sources: Union[BaseSource, BaseSample], outer_radius: Union[str,
985987 :param float min_counts: If generating a grouped spectrum, this is the minimum number of counts per channel.
986988 To disable minimum counts set this parameter to None.
987989 :param float min_sn: If generating a grouped spectrum, this is the minimum signal to noise in each channel.
988- To disable minimum signal to noise set this parameter to None.
990+ To disable minimum signal-to- noise set this parameter to None.
989991 :param float over_sample: The minimum energy resolution for each group, set to None to disable. e.g. if
990992 over_sample=3 then the minimum width of a group is 1/3 of the resolution FWHM at that energy.
991993 :param bool one_rmf: This flag tells the method whether it should only generate one RMF for a particular
@@ -998,37 +1000,56 @@ def n_temp_apec(sources: Union[BaseSource, BaseSample], outer_radius: Union[str,
9981000 Please note that this is not a timeout for the entire fitting process, but a timeout to individual source
9991001 fits.
10001002 """
1001- raise NotImplementedError ("Barely begun" )
1003+
1004+ # This is a little cheesy as we haven't decided what exactly to do yet, but the spectrum checking behaviour
1005+ # is super inconsistent with double_temp_apec - presumably just because the greater number of parameters
1006+ # makes estimating errors much less stable for individual spectral fits
1007+ if spectrum_checking :
1008+ raise NotImplementedError ("The double_temp_apec function does not currently support "
1009+ "'spectrum_checking=True', as it exhibits very unstable behaviour." )
1010+
10021011 sources , inn_rad_vals , out_rad_vals = _pregen_spectra (sources , outer_radius , inner_radius , group_spec , min_counts ,
10031012 min_sn , over_sample , one_rmf , num_cores )
10041013 sources = _check_inputs (sources , lum_en , lo_en , hi_en , fit_method , abund_table , timeout )
10051014
1006- # Have to check that every source has a start temperature entry, if the user decided to pass a set of them
1007- if not start_temp .isscalar and len (start_temp ) != len (sources ):
1008- raise ValueError ("If a non-scalar Quantity is passed for 'start_temp', it must have one entry for each "
1009- "source. It currently has {n} for {s} sources." .format (n = len (start_temp ), s = len (sources )))
1015+ # Have to check that every source has start temperature entries, if the user decided to pass a set of them - this
1016+ # is identical to the checks we perform at the beginning of 'single_temp_apec'
1017+ if ((not start_temp_one .isscalar and len (start_temp_one ) != len (sources )) or
1018+ (not start_temp_two .isscalar and len (start_temp_two ) != len (sources ))):
1019+ raise ValueError ("If a non-scalar Quantity is passed for 'start_temp_one' or 'start_temp_two', there must be "
1020+ "one entry for each source. They currently have {n1} and {n2} respectively, for {s} "
1021+ "sources." .format (n1 = len (start_temp_one ), n2 = len (start_temp_two ), s = len (sources )))
1022+
10101023 # Want to make sure that the start_temp variable is always a non-scalar Quantity with an entry for every source
10111024 # after this point, it means we normalise how we deal with it.
1012- elif start_temp .isscalar :
1025+ if start_temp_one .isscalar :
10131026 # Doing it like this, defining a new variable and then redeclaring the start_temp in each bit of the loop
10141027 # below is important, as the fit_conf generation code accesses the current value of start_temp specifically
1015- all_start_temps = Quantity ([start_temp .value .copy ()]* len (sources ), start_temp .unit )
1028+ all_start_temp_ones = Quantity ([start_temp_one .value .copy ()]* len (sources ), start_temp_one .unit )
10161029 else :
1017- all_start_temps = start_temp .copy ()
1030+ all_start_temp_ones = start_temp_one .copy ()
10181031
1019- # This function is for a set model, absorbed apec, so I can hard code all of this stuff.
1020- # These will be inserted into the general XSPEC script template, so lists of parameters need to be in the form
1021- # of TCL lists.
1022- model = "constant*tbabs*apec"
1023- par_names = "{factor nH kT Abundanc Redshift norm}"
1032+ # And the same thing for start temperature the second
1033+ if start_temp_two .isscalar :
1034+ # Doing it like this, defining a new variable and then redeclaring the start_temp in each bit of the loop
1035+ # below is important, as the fit_conf generation code accesses the current value of start_temp specifically
1036+ all_start_temp_twos = Quantity ([start_temp_two .value .copy ()]* len (sources ), start_temp_two .unit )
1037+ else :
1038+ all_start_temp_twos = start_temp_two .copy ()
1039+
1040+ # This function is for a set model, absorbed double apec, so I can hard code all of this stuff.
1041+ model = "constant*tbabs*(apec+apec)"
1042+ # These will be inserted into the general XSPEC script template, so lists of parameters need to be
1043+ # in the form of TCL lists.
1044+ par_names = "{factor nH kT Abundanc Redshift norm kT Abundanc Redshift norm}"
10241045 lum_low_lims = "{" + " " .join (lum_en [:, 0 ].to ("keV" ).value .astype (str )) + "}"
10251046 lum_upp_lims = "{" + " " .join (lum_en [:, 1 ].to ("keV" ).value .astype (str )) + "}"
10261047
10271048 # Here we generate the fit configuration storage key from those arguments to this function that control the fit
10281049 # and how it behaves
1029- rel_args = FIT_FUNC_ARGS ['single_temp_apec ' ]
1050+ rel_args = FIT_FUNC_ARGS ['double_temp_apec ' ]
10301051
1031- sig = signature (single_temp_apec )
1052+ sig = signature (double_temp_apec )
10321053 cur_args = {k : v .default for k , v in sig .parameters .items () if v .default is not Parameter .empty }
10331054
10341055 # This is purely for developers, as a check to make sure that the FIT_FUNC_ARGS dictionary is updated if the
@@ -1063,22 +1084,27 @@ def n_temp_apec(sources: Union[BaseSource, BaseSample], outer_radius: Union[str,
10631084 if source .redshift is None :
10641085 raise ValueError ("You cannot supply a source without a redshift to this model." )
10651086
1066- # Whatever start temperature is passed gets converted to keV, this will be put in the template
1067- start_temp = all_start_temps [src_ind ].to ("keV" , equivalencies = u .temperature_energy ())
1087+ # Whatever start temperatures are passed get converted to keV - then will be put in the template
1088+ start_temp_one = all_start_temp_ones [src_ind ].to ("keV" , equivalencies = u .temperature_energy ())
1089+ start_temp_two = all_start_temp_twos [src_ind ].to ("keV" , equivalencies = u .temperature_energy ())
1090+
10681091 # Another TCL list, this time of the parameter start values for this model.
1069- par_values = "{{{0} {1} {2} {3} {4} {5}}}" .format (1. , source .nH .to ("10^22 cm^-2" ).value , start_temp .value ,
1070- start_met , source .redshift , 1. )
1092+ par_values = ("{{{0} {1} {2} {3} {4} {5} {6} {7} {8} "
1093+ "{9}}}" ).format (1. , source .nH .to ("10^22 cm^-2" ).value , start_temp_one .value , start_met_one ,
1094+ source .redshift , 1. , start_temp_two .value , start_met_two , source .redshift , 1. )
10711095
10721096 # Set up the TCL list that defines which parameters are frozen, dependent on user input - this can now
10731097 # include the temperature, if the user wants it fixed at the start value
1074- freezing = "{{F {n} {t} {a} T F}}" .format (n = "T" if freeze_nh else "F" ,
1075- t = "T" if freeze_temp else "F" ,
1076- a = "T" if freeze_met else "F" )
1098+ freezing = "{{F {n} {t1} {a2} T F {t2} {a2} T F}}" .format (n = "T" if freeze_nh else "F" ,
1099+ t1 = "T" if freeze_temp_one else "F" ,
1100+ a1 = "T" if freeze_met_one else "F" ,
1101+ t2 = "T" if freeze_temp_two else "F" ,
1102+ a2 = "T" if freeze_met_two else "F" )
10771103
10781104 # Set up the TCL list that defines which parameters are linked across different spectra, only the
10791105 # multiplicative constant that accounts for variation in normalisation over different observations is not
10801106 # linked
1081- linking = "{F T T T T T}"
1107+ linking = "{F T T T T T T T T T }"
10821108
10831109 # If the user wants the spectrum cleaning step to be run, then we have to setup some acceptable
10841110 # limits. For this function they will be hardcoded, for simplicities sake, and we're only going to
@@ -1135,5 +1161,5 @@ def n_temp_apec(sources: Union[BaseSource, BaseSample], outer_radius: Union[str,
11351161 'constant*tbabs*zpowerlw' : power_law ,
11361162 'constant*tbabs*powerlaw' : power_law ,
11371163 'constant*tbabs*zbbody' : blackbody ,
1138- 'constant*tbabs*bbody' : blackbody
1139- }
1164+ 'constant*tbabs*bbody' : blackbody ,
1165+ 'constant*tbabs*(apec+apec)' : double_temp_apec }
0 commit comments