Skip to content

Commit 83999b6

Browse files
natsat0919settwi
andauthored
Legacy albedo correction (#206)
* STIX spectrogram loader * updated changes from pre-commit * added changelog * Moving STIX loader to extern * Added the actual StixLoader * code fix from review * pre-commit fixes * change STIXLoader srm placeholder name * attenuator srm loading * function description fix * code for albedo correction * fix automatic attenuation selector - att. state at end of file * albedo speed-up * fix mcmc residuals * pre-commit changes * fix .plot() when no models are defined (get_models to ouput 2 vals instead 1) * add changelog * fix albedo docstring * add energy and angle checks * remove duplicate functions * remove duplicated albedo code * fix mcmc plotting with albedo component * return albedo for joint fit of different non-combinable spectra --------- Co-authored-by: William Setterberg <[email protected]>
1 parent 1317913 commit 83999b6

File tree

3 files changed

+142
-21
lines changed

3 files changed

+142
-21
lines changed

changelog/206.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add the possibility to perform albedo correction within the legacy code. The alebdo matrix is generated with :func:`sunkit_spex.legacy.fitting.albedo.get_albedo_matrix` and the albedo correction is performed in :class:`sunkit_spex.legacy.fitting.fitter.Fitter`.

sunkit_spex/legacy/fitting/albedo.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
The following code is used to generate matrices for albedo correction
3+
in the legacy portion of the package.
4+
"""
5+
6+
import numpy as np
7+
8+
import astropy.units as u
9+
10+
from sunkit_spex.models.physical.albedo import get_albedo_matrix as _get_albedo_matrix
11+
12+
13+
@u.quantity_input
14+
def get_albedo_matrix(energy_edges: u.keV, theta: u.deg, anisotropy=1):
15+
r"""
16+
Get an albedo correction matrix.
17+
18+
Wraps `~sunkit_spex.models.physical.albedo.get_albedo_matrix`,
19+
adding support for 2D energy edges.
20+
21+
Parameters
22+
----------
23+
energy_edges :
24+
Energy edges associated with the spectrum (2D array)
25+
theta :
26+
Angle between Sun-observer line and X-ray source
27+
anisotropy :
28+
Ratio of the flux in observer direction to the flux downwards, 1 for an isotropic source
29+
"""
30+
# The physical model Albedo matrix function expects 1D
31+
# energy edges, so flatten the 2D edges from the legacy side,
32+
# and just call the other function.
33+
energy_edges = energy_edges.to_value(u.keV)
34+
flat_edges = np.concatenate((energy_edges[:, 0], [energy_edges[-1, -1]])) << u.keV
35+
return _get_albedo_matrix(flat_edges, theta, anisotropy)

sunkit_spex/legacy/fitting/fitter.py

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@
2929
import numpy as np
3030
from mpl_toolkits.axes_grid1 import make_axes_locatable
3131
from scipy.interpolate import interp1d
32+
from scipy.io import readsav
3233
from scipy.linalg import LinAlgError
3334
# to fit the model
3435
from scipy.optimize import minimize
3536

37+
import astropy.units as u
3638
from astropy.table import Table
3739

40+
from sunkit_spex.legacy.fitting.albedo import get_albedo_matrix
3841
from sunkit_spex.legacy.fitting.data_loader import LoadSpec
3942
from sunkit_spex.legacy.fitting.instruments import rebin_any_array
4043
from sunkit_spex.legacy.fitting.likelihoods import LogLikelihoods
@@ -439,6 +442,13 @@ def __init__(
439442
# attribute to determine how the class is pickled
440443
self._pickle_reason = "normal"
441444

445+
# for alebdo correction
446+
self.albedo_corr = False
447+
448+
self.albedo_angle = 0
449+
450+
self.albedo_anisotropy = 1
451+
442452
@property
443453
def model(self):
444454
"""***Property*** Allows a model to be set and a parameter table to be generated straight away.
@@ -1453,6 +1463,7 @@ def _counts_model(self, **kwargs):
14531463
# return counts s^-1 (keV^-1 has been multiplied out) photon_channel_widths=ph_e_binning
14541464

14551465
cts_models = []
1466+
albedo_excess_models = []
14561467
# loop through the parameter groups (params for spectrum1, then for spectrum2, etc)
14571468
for s, pgs in enumerate(self._param_groups):
14581469
# take the spectrum parameters (e.g., [p2_spectrum1,p1_spectrum1]) and order to be the same as the model parameters (e.g., [p1,p2])
@@ -1469,8 +1480,8 @@ def _counts_model(self, **kwargs):
14691480
) # np.diff(kwargs["photon_channels"][s]).flatten() # remove energy bin dependence
14701481

14711482
# fold the photon model through the SRM to create the count rate model, [photon s^-1 cm^-2] * [count photon^-1 cm^2] = [count s^-1]
1472-
cts_model = make_model(
1473-
energies=kwargs["photon_channels"][s], photon_model=m, parameters=None, srm=kwargs["total_responses"][s]
1483+
cts_model, cts_albedo_exess = make_model(
1484+
energies=kwargs["photon_channels"][s], photon_model=m, parameters=None, srm=kwargs["total_responses"][s], albedo_corr=self.albedo_corr, albedo_angle =self.albedo_angle, albedo_anisotropy=self.albedo_anisotropy
14741485
)
14751486

14761487
if "scaled_background_spectrum" + str(s + 1) in self._scaled_backgrounds:
@@ -1496,7 +1507,20 @@ def _counts_model(self, **kwargs):
14961507
cts_model[None, :]
14971508
) # need [None, :] these lines get rid of a dimension meaning later concatenation fails
14981509

1499-
return cts_models
1510+
if cts_albedo_exess.size > 0:
1511+
cts_albedo_exess = (
1512+
cts_albedo_exess
1513+
if len(LL_CLASS.remove_non_numbers(cts_albedo_exess[cts_albedo_exess != 0])) != 0
1514+
else np.zeros(cts_albedo_exess.shape)
1515+
)
1516+
1517+
albedo_excess_models.append(
1518+
cts_albedo_exess[None, :]
1519+
) # need [None, :] these lines get rid of a dimension meaning later concatenation fails
1520+
else:
1521+
albedo_excess_models.append(np.array([[]]))
1522+
1523+
return cts_models, albedo_excess_models
15001524

15011525
def _pseudo_model(self, free_params_list, tied_or_frozen_params_list, param_name_list_order, **other_inputs):
15021526
"""Bridging method between the input args (free,other) and different ordered args for the model calculation.
@@ -1776,7 +1800,7 @@ def _fit_stat(
17761800
"""
17771801

17781802
# make sure only the free parameters are getting varied so put them first
1779-
mu = self._pseudo_model(
1803+
mu, _ = self._pseudo_model(
17801804
free_params_list,
17811805
tied_or_frozen_params_list,
17821806
param_name_list_order,
@@ -2492,7 +2516,7 @@ def _calculate_model(self, **kwargs):
24922516
tied_or_frozen_params_list.extend(list(update_fixed_params.values()))
24932517

24942518
# make sure only the free parameters are getting varied
2495-
mu = self._pseudo_model(
2519+
mu, alebdo_excess_count = self._pseudo_model(
24962520
free_params_list,
24972521
tied_or_frozen_params_list,
24982522
param_name_list_order,
@@ -2502,18 +2526,22 @@ def _calculate_model(self, **kwargs):
25022526
total_responses=srm,
25032527
**kwargs,
25042528
)
2529+
25052530
# turn counts s^-1 into counts s^-1 keV^-1
25062531
for m, e in enumerate(e_binning):
25072532
mu[m][0] /= e
2533+
if np.array(alebdo_excess_count[m][0]).size > 0:
2534+
alebdo_excess_count[m][0] /= e
25082535

25092536
self._energy_fitting_indices = _energy_fitting_indices_orig
25102537
# numpy is better to store but all models need to be the same length, this might not be the case
25112538
try:
2512-
return np.concatenate(tuple(mu))
2539+
return np.concatenate(tuple(mu)), np.concatenate(tuple(alebdo_excess_count))
2540+
25132541
except ValueError:
2514-
return [mu[i][0] for i in range(len(mu))]
2542+
return [mu[i][0] for i in range(len(mu))], alebdo_excess_count
25152543

2516-
def _calc_counts_model(self, photon_model, parameters=None, spectrum="spectrum1", include_bg=False, **kwargs):
2544+
def _calc_counts_model(self, photon_model, parameters=None, spectrum="spectrum1", include_bg=False, for_plotting=False, **kwargs):
25172545
"""Easily calculate a spectrum's count model from the parameter table and user photon model.
25182546
25192547
Given a model (a calculated model array or a photon model function) then calcutes the counts
@@ -2572,12 +2600,27 @@ def _calc_counts_model(self, photon_model, parameters=None, spectrum="spectrum1"
25722600
)
25732601
return
25742602

2575-
cts_model = make_model(
2576-
energies=photon_channel_bins,
2577-
photon_model=m * np.diff(photon_channel_bins).flatten(),
2578-
parameters=None,
2579-
srm=srm,
2580-
)
2603+
if for_plotting:
2604+
cts_model, _ = make_model(
2605+
energies=photon_channel_bins,
2606+
photon_model=m * np.diff(photon_channel_bins).flatten(),
2607+
parameters=None,
2608+
srm=srm,
2609+
albedo_corr=False,
2610+
albedo_angle=self.albedo_angle,
2611+
albedo_anisotropy=self.albedo_anisotropy
2612+
)
2613+
2614+
else:
2615+
cts_model, _ = make_model(
2616+
energies=photon_channel_bins,
2617+
photon_model=m * np.diff(photon_channel_bins).flatten(),
2618+
parameters=None,
2619+
srm=srm,
2620+
albedo_corr=self.albedo_corr,
2621+
albedo_angle=self.albedo_angle,
2622+
albedo_anisotropy=self.albedo_anisotropy
2623+
)
25812624

25822625
if include_bg and ("scaled_background_" + spectrum in self._scaled_backgrounds):
25832626
cts_model += self._scaled_backgrounds["scaled_background_" + spectrum]
@@ -2693,6 +2736,7 @@ def _calculate_submodels(self, spectrum=None):
26932736
photon_model=self._submod_functions[p],
26942737
parameters=self._submod_value_inputs[s - 1][p],
26952738
spectrum="spectrum" + str(s),
2739+
for_plotting=True
26962740
)
26972741
spec_submods.append(cts_mod)
26982742
all_spec_submods.append(spec_submods)
@@ -3231,14 +3275,20 @@ def _plot_mcmc_mods(
32313275
name: (_params[mcmc_freepar_labels.index(val)] if type(val) == str else val)
32323276
for name, val in _spec_rpars.items()
32333277
}
3278+
_rpars['for_plotting'] = True
32343279
e_mids, ctr = self._calc_counts_model(
32353280
photon_model=self._model, parameters=_pars, spectrum="spectrum" + str(s + 1), include_bg=True, **_rpars
32363281
)
32373282
_randcts.append(ctr)
3283+
32383284
if _rebin_info is not None:
32393285
ctr = self._bin_model(ctr, *_rebin_info)
32403286
e_mids = np.mean(_rebin_info[2], axis=1)
32413287

3288+
if self.albedo_corr:
3289+
for i in range(len(res_info[1])):
3290+
ctr[i] = ctr[i] + res_info[-1][i]
3291+
32423292
residuals = [
32433293
(res_info[0][i] - ctr[i]) / res_info[1][i] if res_info[1][i] > 0 else 0 for i in range(len(res_info[1]))
32443294
]
@@ -3559,7 +3609,7 @@ def _setup_rebin_plotting(self, rebin_and_spec, data_arrays, _return_cts_rate_mo
35593609
count_rate_model), and a 1d array of energies for residual plotting (energy_channels_res), respectively.
35603610
"""
35613611
rebin_val, rebin_spec = rebin_and_spec[0], rebin_and_spec[1]
3562-
energy_channels, energy_channel_error, count_rates, count_rate_errors, count_rate_model = data_arrays
3612+
energy_channels, energy_channel_error, count_rates, count_rate_errors, albedo_excess_count, count_rate_model = data_arrays
35633613
if type(rebin_val) != type(None):
35643614
if rebin_spec in list(self.data.loaded_spec_data.keys()):
35653615
(
@@ -3714,7 +3764,7 @@ def _plot_1spec(
37143764
-------
37153765
Spectrum axes and residuals axes.
37163766
"""
3717-
energy_channels, energy_channel_error, count_rates, count_rate_errors, count_rate_model = data_arrays
3767+
energy_channels, energy_channel_error, count_rates, count_rate_errors, albedo_excess_count, count_rate_model = data_arrays
37183768

37193769
axs = axes if type(axes) != type(None) else plt.gca()
37203770
fitting_range = fitting_range if type(fitting_range) != type(None) else self.energy_fitting_range
@@ -3795,14 +3845,17 @@ def _plot_1spec(
37953845
axs.plot(energy_channels, count_rate_model, linewidth=2, color="k")
37963846
res.plot(energy_channels_res, residuals, color="k", alpha=0.8) # , drawstyle='steps-mid'
37973847

3848+
if self.albedo_corr and albedo_excess_count.size > 0:
3849+
axs.plot(energy_channels, albedo_excess_count, color="grey")
3850+
37983851
if self._latest_fit_run == "mcmc":
37993852
_rebin_info = (
38003853
[old_bin_width, old_bins, new_bins, new_bin_width] if type(rebin_and_spec[0]) != type(None) else None
38013854
)
38023855
self._plot_mcmc_mods(
38033856
axs,
38043857
res,
3805-
[count_rates, count_rate_errors, energy_channels_res],
3858+
[count_rates, count_rate_errors, energy_channels_res, albedo_excess_count],
38063859
spectrum=submod_spec,
38073860
num_of_samples=num_of_samples,
38083861
hex_grid=hex_grid,
@@ -4194,7 +4247,7 @@ def _get_models(self, number_of_models):
41944247
return self._calculate_model()
41954248
else:
41964249
self._param_groups = [None] * int(number_of_models)
4197-
return [np.array([1])] * int(number_of_models)
4250+
return [np.array([1])] * int(number_of_models), [np.array([1])] * int(number_of_models) #empty models, empty array for albedo
41984251

41994252
def plot(self, subplot_axes_grid=None, rebin=None, num_of_samples=100, hex_grid=False, plot_final_result=True):
42004253
"""Plots the latest fit or sampling result.
@@ -4268,7 +4321,7 @@ def plot(self, subplot_axes_grid=None, rebin=None, num_of_samples=100, hex_grid=
42684321
self._scaled_backgrounds = self._scaled_background_rates_full
42694322

42704323
# only need enough axes for the number of spectra to plot so doesn't matter if more axes are given
4271-
models = self._get_models(number_of_models=number_of_plots)
4324+
models, albedo_excess_count = self._get_models(number_of_models=number_of_plots)
42724325

42734326
axes, res_axes = [], []
42744327
_count_rates, _count_rate_errors = [], []
@@ -4282,6 +4335,7 @@ def plot(self, subplot_axes_grid=None, rebin=None, num_of_samples=100, hex_grid=
42824335
self.data.loaded_spec_data["spectrum" + str(s + 1)]["count_channel_binning"] / 2,
42834336
self.data.loaded_spec_data["spectrum" + str(s + 1)]["count_rate"],
42844337
self.data.loaded_spec_data["spectrum" + str(s + 1)]["count_rate_error"],
4338+
albedo_excess_count[s],
42854339
models[s],
42864340
),
42874341
axes=ax,
@@ -4308,6 +4362,7 @@ def plot(self, subplot_axes_grid=None, rebin=None, num_of_samples=100, hex_grid=
43084362
self.data.loaded_spec_data["spectrum" + str(s)]["count_channel_binning"] / 2,
43094363
np.mean(np.array(_count_rates), axis=0),
43104364
np.sqrt(np.sum(np.array(_count_rate_errors) ** 2, axis=0)) / len(_count_rate_errors),
4365+
np.array([]),
43114366
np.mean(models, axis=0),
43124367
),
43134368
axes=ax,
@@ -5600,7 +5655,7 @@ def imports():
56005655
return _imps
56015656

56025657

5603-
def make_model(energies=None, photon_model=None, parameters=None, srm=None):
5658+
def make_model(energies=None, photon_model=None, parameters=None, srm=None, albedo_corr=False, albedo_angle=0, albedo_anisotropy=1):
56045659
"""Takes a photon model array ( or function if you provide the pinputs with parameters), the spectral response matrix and returns a model count spectrum.
56055660
56065661
Parameters
@@ -5632,6 +5687,36 @@ def make_model(energies=None, photon_model=None, parameters=None, srm=None):
56325687
else:
56335688
photon_spec = photon_model(energies, *parameters)
56345689

5690+
if albedo_corr:
5691+
photon_spec, albedo_excess_phot = albedo(photon_spec, energies, albedo_angle, anisotropy=albedo_anisotropy)
5692+
albedo_excess_count = np.matmul(albedo_excess_phot, srm)
5693+
else:
5694+
albedo_excess_count = np.array([])
5695+
56355696
model_cts_spectrum = np.matmul(photon_spec, srm)
56365697

5637-
return model_cts_spectrum
5698+
return model_cts_spectrum, albedo_excess_count
5699+
5700+
#####
5701+
# Function to calculate the albedo correction, adapted from Shane's PR request
5702+
#####
5703+
5704+
def albedo(spec, energy, theta, anisotropy=1):
5705+
r"""
5706+
Gets the albedo matrix for given angle and anisotropy and returns the albedo corrected spectrum as well as the albedo component by itself.
5707+
5708+
Parameters
5709+
----------
5710+
spec :
5711+
Spectrum object
5712+
energy :
5713+
Energy edges associated with the spectrum
5714+
theta :
5715+
Angle between Sun-observer line and X-ray source
5716+
anisotropy :
5717+
Ratio of the flux in observer direction to the flux downwards, 1 for an isotropic source
5718+
"""
5719+
5720+
albedo_matrix = get_albedo_matrix(energy*u.keV, theta, anisotropy)
5721+
5722+
return spec + spec @ albedo_matrix, spec @ albedo_matrix

0 commit comments

Comments
 (0)