Skip to content

Commit 4a06eec

Browse files
authored
Add time lag analysis helpers (#109)
* add time lag analysis helpers * revert channels as static property
1 parent 95df5b5 commit 4a06eec

7 files changed

Lines changed: 472 additions & 14 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ Homepage = "https://github.com/wtbarnes/synthesizAR"
3030
Documentation = "https://synthesizar.readthedocs.io"
3131

3232
[project.optional-dependencies]
33-
all = ["synthesizAR[aia,atomic,ebtel,hydrad,parallel,xrt]"]
33+
all = ["synthesizAR[aia,analysis,atomic,ebtel,hydrad,parallel,xrt]"]
3434
aia = ["aiapy"]
35+
analysis = ["sunkit_image"]
3536
atomic = [
3637
"plasmapy",
3738
"fiasco",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
Convenience functions for computing and analyzing time lag measurements from datacubes.
3+
"""
4+
from .time_lag import *
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Map sources for diagnostic maps.
3+
"""
4+
from astropy.visualization import ImageNormalize
5+
from sunpy.map import GenericMap
6+
7+
import synthesizAR.visualize.colormaps # NOQA
8+
9+
10+
class TimeLagMap(GenericMap):
11+
"""
12+
A map that represents the time lag between two images.
13+
"""
14+
def __init__(self, data, header, **kwargs):
15+
super().__init__(data, header, **kwargs)
16+
self.plot_settings['cmap'] = 'idl_bgry_004'
17+
self.plot_settings['norm'] = ImageNormalize(vmin=-7200, vmax=7200)
18+
self.nickname = f"{header.get('chan_a')}-{header.get('chan_b')}"
19+
20+
@property
21+
def measurement(self):
22+
return ' '.join(self.meta.get('measrmnt', '').split('_')).capitalize()
23+
24+
@classmethod
25+
def is_datasource_for(cls, data, header, **kwargs):
26+
return ('chan_a' in header and
27+
'chan_b' in header and
28+
header.get('measrmnt')=='time_lag')
29+
30+
31+
class CrossCorrelationMap(GenericMap):
32+
"""
33+
A map that represents the maximum cross-correlation value between two images
34+
"""
35+
def __init__(self, data, header, **kwargs):
36+
super().__init__(data, header, **kwargs)
37+
self.plot_settings['cmap'] = 'magma'
38+
self.plot_settings['norm'] = ImageNormalize(vmin=0, vmax=1)
39+
self.nickname = f"{header.get('chan_a')}-{header.get('chan_b')}"
40+
41+
@property
42+
def measurement(self):
43+
return ' '.join(self.meta.get('measrmnt', '').split('_')).capitalize()
44+
45+
@classmethod
46+
def is_datasource_for(cls, data, header, **kwargs):
47+
return ('chan_a' in header and
48+
'chan_b' in header and
49+
header.get('measrmnt')=='max_cross_correlation')
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
Wrapper functions for producing time lag and cross-correlation maps from data cubes.
3+
"""
4+
import astropy.units as u
5+
import itertools
6+
import sunkit_image.time_lag
7+
import sunpy.map
8+
9+
from synthesizAR.instruments.sdo import _AIA_CHANNEL_WAVELENGTHS
10+
11+
import synthesizAR.analysis.time_lag.map_sources # NOQA
12+
13+
14+
__all__ = ['get_aia_channel_combinations', 'make_time_lag_map', 'make_cross_correlation_map']
15+
16+
17+
def get_aia_channel_combinations():
18+
"""
19+
Convenience function for listing all possible AIA channel pairs.
20+
This is useful for computing time lags.
21+
"""
22+
channel_list = [f"{chan.to_value('Angstrom'):.0f}" for chan in _AIA_CHANNEL_WAVELENGTHS]
23+
channel_combinations = list(itertools.combinations(channel_list, 2))
24+
channel_combinations = channel_combinations[:5] + [sorted(c, key=lambda x: float(x), reverse=True) for c in channel_combinations[5:]]
25+
return channel_combinations
26+
27+
28+
def _get_meta_and_time(cube_a, cube_b, lag_bounds):
29+
time_a = cube_a.axis_world_coords('time')[0]
30+
time_b = cube_b.axis_world_coords('time')[0]
31+
if not (time_a == time_b).all():
32+
raise ValueError('Time axes of both cubes must be the same')
33+
time = (time_a - time_a[0]).to('s')
34+
if lag_bounds is None:
35+
lag_bounds = u.Quantity([-time[-1]/2, time[-1]/2])
36+
meta = cube_a.meta.copy()
37+
stale_keys = ['bunit', 'date_sim', 'wavelnth', 'waveunit', 'instrume', 'telescop', 'obsrvtry', 'detector']
38+
for k in stale_keys:
39+
_ = meta.pop(k)
40+
meta['chan_a'] = cube_a.meta.get('wavelnth')
41+
meta['chan_b'] = cube_b.meta.get('wavelnth')
42+
return time, lag_bounds, meta
43+
44+
45+
@u.quantity_input
46+
def make_time_lag_map(cube_a, cube_b, lag_bounds: u.s=None):
47+
"""
48+
Coordinate-aware wrapper around `~sunkit_image.time_lag.time_lag`
49+
50+
Parameters
51+
----------
52+
cube_a : `~ndcube.NDCube`
53+
cube_b : `~ndcube.NDCube`
54+
lag_bounds : `~astropy.units.Quantity`
55+
56+
Return
57+
------
58+
: `~sunpy.map.GenericMap`
59+
"""
60+
time, lag_bounds, meta = _get_meta_and_time(cube_a, cube_b, lag_bounds)
61+
data = sunkit_image.time_lag.time_lag(cube_a.data, cube_b.data, time, lag_bounds=lag_bounds)
62+
meta['bunit'] = time.unit.to_string(format='FITS')
63+
meta['measrmnt'] = 'time_lag'
64+
return sunpy.map.Map(data, meta)
65+
66+
67+
@u.quantity_input
68+
def make_cross_correlation_map(cube_a, cube_b, lag_bounds: u.s=None):
69+
"""
70+
Coordinate-aware wrapper around `~sunkit_image.time_lag.max_cross_correlation`
71+
72+
Parameters
73+
----------
74+
cube_a : `~ndcube.NDCube`
75+
cube_b : `~ndcube.NDCube`
76+
lag_bounds : `~astropy.units.Quantity`
77+
78+
Return
79+
------
80+
: `~sunpy.map.GenericMap`
81+
"""
82+
time, lag_bounds, meta = _get_meta_and_time(cube_a, cube_b, lag_bounds)
83+
data = sunkit_image.time_lag.max_cross_correlation(cube_a.data, cube_b.data, time, lag_bounds=lag_bounds)
84+
meta['measrmnt'] = 'max_cross_correlation'
85+
return sunpy.map.Map(data, meta)

synthesizAR/instruments/sdo.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
"""
55
import asdf
66
import astropy.units as u
7+
import ndcube
78
import numpy as np
9+
import pathlib
10+
import sunpy.map
811

912
from aiapy.psf import filter_mesh_parameters
1013
from aiapy.response import Channel
@@ -13,6 +16,7 @@
1316
from scipy.interpolate import interpn
1417

1518
from synthesizAR.instruments import InstrumentBase
19+
from synthesizAR.instruments.util import map_list_to_time_cube
1620
from synthesizAR.util.decorators import return_quantity_as_tuple
1721

1822
__all__ = ['InstrumentSDOAIA', 'aia_kernel_quick']
@@ -22,6 +26,15 @@
2226
with asdf.open(_TEMPERATURE_RESPONSE_FILE, 'r', memmap=False) as af:
2327
_TEMPERATURE_RESPONSE = af.tree
2428

29+
_AIA_CHANNEL_WAVELENGTHS = [
30+
94*u.angstrom,
31+
131*u.angstrom,
32+
171*u.angstrom,
33+
193*u.angstrom,
34+
211*u.angstrom,
35+
335*u.angstrom,
36+
]
37+
2538

2639
class AIAChannel(Channel):
2740

@@ -72,6 +85,10 @@ def __init__(self, observing_time, observer, **kwargs):
7285
**kwargs,
7386
)
7487

88+
@cached_property
89+
def channels(self):
90+
return [AIAChannel(w) for w in _AIA_CHANNEL_WAVELENGTHS]
91+
7592
@property
7693
def observatory(self):
7794
return 'SDO'
@@ -84,17 +101,6 @@ def detector(self):
84101
def telescope(self):
85102
return 'SDO/AIA'
86103

87-
@cached_property
88-
def channels(self):
89-
return [
90-
AIAChannel(94*u.angstrom),
91-
AIAChannel(131*u.angstrom),
92-
AIAChannel(171*u.angstrom),
93-
AIAChannel(193*u.angstrom),
94-
AIAChannel(211*u.angstrom),
95-
AIAChannel(335*u.angstrom),
96-
]
97-
98104
@property
99105
def _expected_unit(self):
100106
return u.DN / (u.pix * u.s)
@@ -136,6 +142,30 @@ def calculate_intensity_kernel(loop, channel, **kwargs):
136142
kernel = aia_kernel_quick(channel.name, loop.electron_temperature, loop.density)
137143
return kernel
138144

145+
@staticmethod
146+
def build_collection_from_maps(save_directory, channels=None):
147+
"""
148+
Build an `~ndcube.NDCollection` from all maps in all channels produced by a simulation.
149+
150+
Parameters
151+
----------
152+
save_directory : path-like
153+
Directory that contains all FITS files produced by the `~synthesizAR.instruments.InstrumentBase.observe` method.
154+
channels : `list` of `str`
155+
Channel labels from which to build collection. If not specified, will default to the
156+
six EUV channels of AIA.
157+
"""
158+
save_directory = pathlib.Path(save_directory)
159+
if channels is None:
160+
channels = [f"{chan.to_value('Angstrom'):.0f}" for chan in _AIA_CHANNEL_WAVELENGTHS]
161+
key_data_pairs = []
162+
for chan in channels:
163+
filenames = sorted(save_directory.glob(f'm_{chan}_t*.fits'))
164+
cube = map_list_to_time_cube(sunpy.map.Map(filenames))
165+
key_data_pairs.append((chan, cube))
166+
col = ndcube.NDCollection(key_data_pairs, aligned_axes=(0, 1, 2))
167+
return col
168+
139169

140170
@u.quantity_input
141171
def aia_kernel_quick(channel,

synthesizAR/instruments/util.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
import numpy as np
99
import xarray
1010

11-
from ndcube.extra_coords.table_coord import QuantityTableCoordinate
11+
from ndcube.extra_coords.table_coord import QuantityTableCoordinate, TimeTableCoordinate
1212
from ndcube.wcs.wrappers import CompoundLowLevelWCS
1313

1414
__all__ = [
1515
'get_wave_keys',
1616
'add_wave_keys_to_header',
1717
'extend_celestial_wcs',
18+
'map_list_to_time_cube',
1819
'read_cube_from_dataset',
1920
'write_cube_to_netcdf',
2021
]
@@ -58,11 +59,28 @@ def extend_celestial_wcs(celestial_wcs, *extra_coords, **kwargs):
5859
for ec in extra_coords:
5960
if isinstance(ec, tuple):
6061
array, name, physical_type = ec
61-
ec = QuantityTableCoordinate(array, names=name, physical_types=physical_type)
62+
if isinstance(array, u.Quantity):
63+
ec = QuantityTableCoordinate(array, names=name, physical_types=physical_type)
64+
elif isinstance(array, astropy.time.Time):
65+
ec = TimeTableCoordinate(array, names=name, physical_types=physical_type)
66+
else:
67+
raise TypeError(f'{array} has unrecognized type {type(array)}')
6268
wcses.append(ec.wcs)
6369
return CompoundLowLevelWCS(celestial_wcs, *wcses, **kwargs)
6470

6571

72+
def map_list_to_time_cube(map_list):
73+
"""
74+
Transform a list of maps into a single `ndcube.NDCube`
75+
"""
76+
data = np.array([m.data for m in map_list])
77+
# NOTE: This is specific to datacubes produced by synthesizAR
78+
times = astropy.time.Time([m.meta['DATE_SIM'] for m in map_list])
79+
new_wcs = extend_celestial_wcs(map_list[0].wcs, (times, 'time', 'time'))
80+
new_meta = map_list[0].meta.copy()
81+
return ndcube.NDCube(data, wcs=new_wcs, meta=new_meta, unit=map_list[0].unit)
82+
83+
6684
def read_cube_from_dataset(filename, axis_name, physical_type):
6785
"""
6886
Read an `~ndcube.NDCube` from an `xarray` dataset.

0 commit comments

Comments
 (0)