Skip to content

Commit 8e3c18d

Browse files
authored
Add instrument class for computing VDEM (#103)
* minor instr refactoring * fix tests * add analytic mikic B-field model * remove SpatialPair * add vdem instrument class * add function for making VDEM cubes * fix doc build * more doc fixes
1 parent 1248ea9 commit 8e3c18d

15 files changed

+449
-255
lines changed

.rtd-environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ dependencies:
55
- python=3.12
66
- pip
77
- graphviz!=2.42.*,!=2.43.*
8+
- sqlite<3.49.0

docs/references.bib

+16
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,19 @@ @article{reep_diagnosing_2013
4949
doi = {10.1088/0004-637X/764/2/193},
5050
urldate = {2014-07-23}
5151
}
52+
53+
@article{mikic_importance_2013,
54+
title = {The {{Importance}} of {{Geometric Effects}} in {{Coronal Loop Models}}},
55+
author = {Miki{\'c}, Zoran and Lionello, Roberto and Mok, Yung and Linker, Jon A. and Winebarger, Amy R.},
56+
year = {2013},
57+
month = aug,
58+
journal = {The Astrophysical Journal},
59+
volume = {773},
60+
number = {2},
61+
pages = {94},
62+
issn = {0004-637X},
63+
doi = {10.1088/0004-637X/773/2/94},
64+
url = {http://iopscience.iop.org.ezproxy.rice.edu/0004-637X/773/2/94},
65+
urldate = {2013-09-16},
66+
langid = {english}
67+
}

examples/ebtel-nanoflares.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
###########################################################################
9595
# We can easily visualize this time-dependent emission using a
9696
# `~sunpy.map.MapSequence`.
97-
mseq = sunpy.map.MapSequence(maps['193'], sequence=True)
97+
mseq = sunpy.map.Map(maps['193'], sequence=True)
9898
fig = plt.figure()
9999
ax = fig.add_subplot(projection=mseq[0])
100100
ani = mseq.plot(axes=ax, norm=ImageNormalize(vmin=0, vmax=5, stretch=AsinhStretch()))

examples/multi-instrument.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,15 @@ def get_heating_constant(self, loop):
8787
# We'll select a field of view by specifying the center of the field of view
8888
# as well as the width and height.
8989
center = SkyCoord(Tx=0*u.arcsec, Ty=-550*u.arcsec, frame=Helioprojective(observer=sdo, obstime=sdo.obstime))
90-
aia = InstrumentSDOAIA([0, 1]*u.s, sdo, fov_center=center, fov_width=(250, 250)*u.arcsec)
90+
aia = InstrumentSDOAIA([0]*u.s, sdo, fov_center=center, fov_width=(250, 250)*u.arcsec)
9191
aia_images = aia.observe(skeleton)
9292
for k in aia_images:
9393
aia_images[k][0].peek()
9494

9595
###############################################################################
9696
# We can carry out this same procedure for *Hinode* XRT for the same field of view.
9797
# We'll look just at the Be-thin and Al-poly channels.
98-
xrt = InstrumentHinodeXRT([0, 1]*u.s, hinode, ['Be-thin', 'Al-poly'],
98+
xrt = InstrumentHinodeXRT([0]*u.s, hinode, ['Be-thin', 'Al-poly'],
9999
fov_center=center, fov_width=(250, 250)*u.arcsec)
100100
xrt_images = xrt.observe(skeleton)
101101
for k in xrt_images:
@@ -111,13 +111,13 @@ def get_heating_constant(self, loop):
111111
class InstrumentSTEREOEUVI(InstrumentSDOAIA):
112112
name = 'STEREO_EUVI'
113113

114-
@property
115-
def resolution(self) -> u.Unit('arcsec / pixel'):
116-
return u.Quantity([1.58777404, 1.58777404], 'arcsec / pixel')
117-
118-
@property
119-
def cadence(self) -> u.s:
120-
return 1 * u.h
114+
def __init__(self, *args, **kwargs):
115+
super().__init__(
116+
*args,
117+
resolution=u.Quantity([1.58777404, 1.58777404], 'arcsec / pixel'),
118+
cadence=1*u.h,
119+
**kwargs,
120+
)
121121

122122
@property
123123
def observatory(self):
@@ -138,6 +138,6 @@ def get_instrument_name(self, *args):
138138
# We can then use our custom instrument class in the exact same way as our
139139
# predefined classes to model the emission from EUVI. Note that we'll only do
140140
# this for the 171 Å channel.
141-
euvi = InstrumentSTEREOEUVI([0, 1]*u.s, stereo_a, fov_center=center, fov_width=(250, 250)*u.arcsec)
141+
euvi = InstrumentSTEREOEUVI([0]*u.s, stereo_a, fov_center=center, fov_width=(250, 250)*u.arcsec)
142142
euvi_images = euvi.observe(skeleton, channels=euvi.channels[2:3])
143143
euvi_images['171'][0].peek()

synthesizAR/conftest.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313

1414

1515
@pytest.fixture
16-
def bare_skeleton():
17-
observer = get_earth(time='2020-01-01T00:00:00')
18-
arcade = semi_circular_arcade(100*u.Mm, 20*u.deg, 10, observer)
16+
def earth_observer():
17+
return get_earth(time='2020-01-01T00:00:00')
18+
19+
20+
@pytest.fixture
21+
def bare_skeleton(earth_observer):
22+
arcade = semi_circular_arcade(100*u.Mm, 20*u.deg, 10, earth_observer)
1923
loops = [synthesizAR.Strand(f'{i}', c) for i, c in enumerate(arcade)]
2024
return synthesizAR.Skeleton(loops)
2125

@@ -27,6 +31,18 @@ def skeleton_with_model(bare_skeleton):
2731
return bare_skeleton
2832

2933

34+
@pytest.fixture
35+
def skeleton_with_time_dependent_model(bare_skeleton):
36+
from ebtelplusplus.models import HeatingModel, TriangularHeatingEvent
37+
38+
from synthesizAR.interfaces.ebtel import EbtelInterface
39+
heating_model = HeatingModel(partition=1)
40+
heating_model.events = [TriangularHeatingEvent(0*u.s, 200*u.s, 5e-3*u.Unit('erg cm-3 s-1'))]
41+
interface = EbtelInterface(5e3*u.s, heating_model=heating_model)
42+
bare_skeleton.load_loop_simulations(interface)
43+
return bare_skeleton
44+
45+
3046
@pytest.fixture
3147
def semi_circle_strand(bare_skeleton):
3248
coords = semi_circular_loop(length=100*u.Mm)

synthesizAR/instruments/base.py

+40-10
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ class InstrumentBase:
3737
Parameters
3838
----------
3939
observing_time : `~astropy.units.Quantity`
40-
Tuple of start and end observing times
41-
observer_coordinate : `~astropy.coordinates.SkyCoord`
40+
If cadence is also provided and this has a length of 2, this is interpreted as
41+
the start and end time of the observation period and an observing time is
42+
constructed based on this interval and the cadence. Otherwise, this is interpreted
43+
as the times at which the observations should be forward modeled.
44+
observer : `~astropy.coordinates.SkyCoord`
4245
Coordinate of the observing instrument
43-
cadence : `~astropy.units.Quantity`
4446
resolution : `~astropy.units.Quantity`
47+
cadence : `~astropy.units.Quantity`, optional
48+
If specified, this is used to construct the observing time
4549
pad_fov : `~astropy.units.Quantity`, optional
4650
Two-dimensional array specifying the padding to apply to the field of view of the synthetic
4751
image in both directions. If None, no padding is applied and the field of view is defined
@@ -55,13 +59,17 @@ class InstrumentBase:
5559
def __init__(self,
5660
observing_time: u.s,
5761
observer,
62+
resolution: u.Unit('arcsec/pix'),
63+
cadence: u.s = None,
5864
pad_fov: u.arcsec = None,
5965
fov_center=None,
6066
fov_width: u.arcsec = None,
6167
average_over_los=False):
6268
self.observer = observer
69+
self.cadence = cadence
6370
self.observing_time = observing_time
64-
self.pad_fov = (0, 0) * u.arcsec if pad_fov is None else pad_fov
71+
self.resolution = resolution
72+
self.pad_fov = pad_fov
6573
self.fov_center = fov_center
6674
self.fov_width = fov_width
6775
self.average_over_los = average_over_los
@@ -72,19 +80,27 @@ def observing_time(self) -> u.s:
7280

7381
@observing_time.setter
7482
def observing_time(self, value):
75-
if self.cadence is None or len(value) > 2:
76-
self._observing_time = value
77-
else:
83+
if self.cadence is not None and len(value) == 2:
7884
self._observing_time = np.arange(*value.to_value('s'),
7985
self.cadence.to_value('s')) * u.s
86+
else:
87+
self._observing_time = value
8088

8189
@property
8290
def cadence(self):
83-
return None
91+
return self._cadence
92+
93+
@cadence.setter
94+
def cadence(self, value):
95+
self._cadence = value
8496

8597
@property
8698
def resolution(self) -> u.arcsec/u.pix:
87-
return (1, 1) * u.arcsec / u.pix
99+
return self._resolution
100+
101+
@resolution.setter
102+
def resolution(self, value):
103+
self._resolution = value
88104

89105
@property
90106
def observer(self):
@@ -94,6 +110,16 @@ def observer(self):
94110
def observer(self, value):
95111
self._observer = value
96112

113+
@property
114+
def pad_fov(self) -> u.arcsec:
115+
return self._pad_fov
116+
117+
@pad_fov.setter
118+
def pad_fov(self, value):
119+
if value is None:
120+
value = [0, 0] * u.arcsec
121+
self._pad_fov = value
122+
97123
@property
98124
def telescope(self):
99125
return self.name
@@ -108,7 +134,11 @@ def observatory(self):
108134

109135
@property
110136
def _expected_unit(self):
111-
return None
137+
raise NotImplementedError
138+
139+
@property
140+
def channels(self):
141+
raise NotImplementedError
112142

113143
def get_instrument_name(self, channel):
114144
return self.name

0 commit comments

Comments
 (0)