-
Notifications
You must be signed in to change notification settings - Fork 20
/
Photosphere.py
2011 lines (1693 loc) · 93.1 KB
/
Photosphere.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import division, print_function
from .global_imports import *
from . import global_imports
from . import _warning, make_verbose, verbose
from os.path import join as _join
from .Spacetime import Spacetime
from .HotRegion import HotRegion
from .Elsewhere import Elsewhere
from .Everywhere import Everywhere
from .Parameter import Parameter
from .ParameterSubspace import ParameterSubspace
from .pixelmesh.integrator import integrate as _integrate
from .tools.energy_integrator import energy_integrator
from .tools.phase_integrator import phase_integrator
try:
_mpl
except NameError:
pass
else:
import matplotlib
from matplotlib import pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib import rcParams
from matplotlib.ticker import MultipleLocator, AutoLocator, AutoMinorLocator
from matplotlib import gridspec
from matplotlib import cm
from matplotlib import animation
import matplotlib.image as mgimg
class Photosphere(ParameterSubspace):
""" A photosphere embedded in an ambient Schwarzschild spacetime.
:param obj hot:
An instance of :class:`~.HotRegion.HotRegion` (or a derived class).
This objects represents the hot regions of the surface that in most
use-cases will be assumed to contain radiating material that is hotter
than that *elsewhere*.
:param obj elsewhere:
An instance of :class:`~.Elsewhere.Elsewhere` (or a derived class).
:param obj everywhere:
An instance of :class:`~.Everywhere.Everywhere` (or a derived class).
Note that if you use construct the surface radiation field in this
way, you should use the :attr:`~.Photosphere.Photosphere.hot_atmosphere`
property to pass a buffer of numerical data to the integrator
routines. You then need to ensure that the extension modules
``xpsi/surface_radiation_field/hot_radiation_field.pyx`` and
``xpsi/surface_radiation_field/elsewhere_radiation_field.pyx`` match.
.. note::
You cannot specify the surface radiation field *everywhere* if you
use hot regions (the latter usage may also include specification of
the radiation field *elsewhere*).
:param dict bounds:
Bounds are supplied for instantiation of a frequency parameter.
The parameter name ``'mode_frequency'`` must be a key in the
dictionary unless the parameter is *fixed* or *derived*. If a bound
is ``None`` that bound is set equal to a strict hard-coded bound.
If ``None``, lock the coordinate rotation frequency of a mode of
asymmetry in the photosphere to a fixed frequency, e.g., the stellar
rotation frequency. If bounds are passed, the frequency is interpreted
as a free parameter.
:param dict values:
Either the fixed value of the mode frequency, a callable if the
frequency is *derived*, or a value upon initialisation if the
frequency is free. The dictionary must have a key with name
``'mode_frequency'`` if it is *fixed* or *derived*.
If the asymmetry is locked to the stellar spin, then you need to pass
the spin frequency. If fixed but different to the spin frequency, this
value needs to be passed instead. In the hot region base class this
mode frequency is applied to normalise the ray lags instead of the
stellar rotation frequency.
:param iterable custom:
A :class:`~.Parameter.Parameter` instance or iterable over such
instances. Might be useful for calling image plane extensions and
passing global variables, without having to instantiate
surface-discretisation classes and without having to handle global
variable values at compile time or from disk for runtime access.
.. note::
In basic modelling patterns the frequency is the spin frequency,
and thus you only need to explicitly pass the spin as ``value`` whilst
leaving ``bounds`` to default. If the spin frequency happens to be a
free parameter (perhaps with informative prior information), then
pass a callable instead that can be used to get the spin frequency
dynamically when the derived mode frequency variable is called for.
"""
required_names = ['mode_frequency']
def __init__(self,
hot = None, elsewhere = None,
everywhere = None,
bounds = None, values = None,
custom = None,
**kwargs):
if everywhere is not None:
if hot or elsewhere is not None:
raise ValueError('Cannot use hot region nor elsewhere '
'functionality if constructing the '
'radiation field everywhere.')
if not isinstance(everywhere, Everywhere):
raise TypeError('Invalid type for everywhere object.')
elif hot is None and elsewhere is None:
pass # can call image-plane extensions
else:
if elsewhere is not None:
if not isinstance(elsewhere, Elsewhere):
raise TypeError('Invalid type for an elsewhere object.')
if hot is None:
raise ValueError('Hot region object(s) must be used in '
'conjuction with an elsewhere object.')
self._elsewhere_atmosphere = ()
# including derived classes
if hot is not None and hot is not isinstance(hot, HotRegion):
if hasattr(hot, 'objects'):
for obj in getattr(hot, 'objects'):
if not isinstance(obj, HotRegion):
raise TypeError('Invalid object for the hot '
'region(s).')
else:
raise TypeError('Invalid object for the hot region(s).')
self._hot = hot
self._hot_atmosphere = ()
self._elsewhere = elsewhere
self._everywhere = everywhere
if bounds is None: bounds = {}
if values is None: values = {}
doc = """
Coordinate frequency of the mode of radiative asymmetry in the
photosphere that is assumed to generate the pulsed signal [Hz].
"""
mode_frequency = Parameter('mode_frequency',
strict_bounds = (0.0, 2000.0),
bounds = bounds.get('mode_frequency', None),
doc = doc,
symbol = r'$f_{\rm mode}$',
value = values.get('mode_frequency', None))
super(Photosphere, self).__init__(mode_frequency,
hot, elsewhere, everywhere,
custom,
**kwargs)
@property
def hot_atmosphere(self):
""" Get the numerical atmosphere buffers for hot regions if used.
To preload a numerical atmosphere into a buffer, subclass and
overwrite the setter. The underscore attribute set by the setter
must be an :math:`n`-tuple whose :math:`n^{th}` element is an
:math:`(n-1)`-dimensional array flattened into a one-dimensional
:class:`numpy.ndarray`. The first :math:`n-1`
elements of the :math:`n`-tuple must each be an ordered one-dimensional
:class:`numpy.ndarray` of parameter values for the purpose of
multi-dimensional interpolation in the :math:`n^{th}` buffer. The
first :math:`n-1` elements must be ordered to match the index
arithmetic applied to the :math:`n^{th}` buffer. An example would be
``self._hot_atmosphere = (logT, logg, mu, logE, buf)``, where:
``logT`` is a logarithm of local comoving effective temperature;
``logg`` is a logarithm of effective surface gravity;
``mu`` is the cosine of the angle from the local surface normal;
``logE`` is a logarithm of the photon energy; and
``buf`` is a one-dimensional buffer of intensities of size given by
the product of sizes of the first :math:`n-1` tuple elements.
It is highly recommended that buffer preloading is used, instead
of loading from disk in the customisable radiation field extension
module, to avoid reading from disk for every signal
(likelihood) evaluation. This can be a non-negligible waste of compute
resources. By preloading in Python, the memory is allocated and
references to that memory are not in general deleted until a sampling
script exits and the kernel stops. The likelihood callback accesses
the same memory upon each call without I/O.
"""
return self._hot_atmosphere
@hot_atmosphere.setter
def hot_atmosphere(self, path):
""" Implement if required. """
raise NotImplementedError('Implement setter if required.')
@property
def elsewhere_atmosphere(self):
""" Get the numerical atmosphere buffers for elsewhere if used.
To preload a numerical atmosphere into a buffer, subclass and
overwrite the setter. The underscore attribute set by the setter
must be an :math:`n`-tuple whose :math:`n^{th}` element is an
:math:`(n-1)`-dimensional array flattened into a one-dimensional
:class:`numpy.ndarray`. The first :math:`n-1`
elements of the :math:`n`-tuple must each be an ordered one-dimensional
:class:`numpy.ndarray` of parameter values for the purpose of
multi-dimensional interpolation in the :math:`n^{th}` buffer. The
first :math:`n-1` elements must be ordered to match the index
arithmetic applied to the :math:`n^{th}` buffer. An example would be
``self._hot_atmosphere = (logT, logg, mu, logE, buf)``, where:
``logT`` is a logarithm of local comoving effective temperature;
``logg`` is a logarithm of effective surface gravity;
``mu`` is the cosine of the angle from the local surface normal;
``logE`` is a logarithm of the photon energy; and
``buf`` is a one-dimensional buffer of intensities of size given by
the product of sizes of the first :math:`n-1` tuple elements.
It is highly recommended that buffer preloading is used, instead
of loading from disk in the customisable radiation field extension
module, to avoid reading from disk for every signal
(likelihood) evaluation. This can be a non-negligible waste of compute
resources. By preloading in Python, the memory is allocated and
references to that memory are not in general deleted until a sampling
script exits and the kernel stops. The likelihood callback accesses
the same memory upon each call without I/O.
"""
return self._elsewhere_atmosphere
@elsewhere_atmosphere.setter
def elsewhere_atmosphere(self, path):
""" Implement if required. """
raise NotImplementedError('Implement setter if required.')
@property
def hot(self):
""" Get the instance of :class:`~.HotRegion.HotRegion`. """
return self._hot
@property
def elsewhere(self):
""" Get the instance of :class:`~.Elsewhere.Elsewhere`. """
return self._elsewhere
@property
def everywhere(self):
""" Get the instance of :class:`~.Everywhere.Everywhere`. """
return self._everywhere
@property
def spacetime(self):
""" Return instance of :class:`~.Spacetime.Spacetime`. """
return self._spacetime
@spacetime.setter
def spacetime(self, obj):
if not isinstance(obj, Spacetime):
raise TypeError('Invalid type for spacetime object.')
# otherwise store a reference to the spacetime object
self._spacetime = obj
def embed(self, fast_total_counts, threads):
""" Embed the photosphere in an ambient Schwarzschild spacetime.
In other words, generate a discrete representation of the photospheric
radiation field and the null mapping from the photosphere to infinity,
for use in flux integrators called by distant observers.
"""
if self._everywhere is not None:
self._everywhere.embed(self._spacetime,
self,
threads)
else:
if self._elsewhere is not None:
self._elsewhere.embed(self._spacetime, threads)
if self._hot is not None:
self._hot.embed(self._spacetime,
self,
fast_total_counts,
threads,
self._elsewhere._compute_cellParamVecs)
elif self._hot is not None:
self._hot.embed(self._spacetime,
self,
fast_total_counts,
threads)
def integrate(self, energies, threads):
""" Integrate over the photospheric radiation field.
:param energies:
A one-dimensional :class:`numpy.ndarray` of energies in keV.
:param int threads:
Number of ``OpenMP`` threads to spawn for signal integration.
"""
if self._everywhere is not None:
spectrum = self._everywhere.integrate(self._spacetime,
energies,
threads,
self._hot_atmosphere)
if spectrum.ndim == 1:
self._signal = ((spectrum.reshape(-1,1),),)
else:
self._signal = ((spectrum,),)
else:
if self._elsewhere is not None:
spectrum = self._elsewhere.integrate(self._spacetime,
energies,
threads,
*self._elsewhere_atmosphere)
if self._hot is not None:
self._signal = self._hot.integrate(self._spacetime,
energies,
threads,
self._hot_atmosphere,
self._elsewhere_atmosphere)
if not isinstance(self._signal[0], tuple):
self._signal = (self._signal,)
# add time-invariant component to first time-dependent component
if self._elsewhere is not None:
for i in range(self._signal[0][0].shape[1]):
self._signal[0][0][:,i] += spectrum
@property
def signal(self):
""" Get the stored signal.
:returns:
A tuple of tuples of *ndarray[m,n]*.
Here :math:`m` is the number of energies, and
:math:`n` is the number of phases. Units are photon/s/keV; the
distance is a fast parameter so the areal units are not yet
factored in. If the signal is a spectrum because the signal is
time-invariant, then :math:`n=1`.
"""
return self._signal
@property
def global_variables(self):
""" Get a vector of global surface radiation field variables.
:returns: An *ndarray[n]* of scalars required to evaluate variables
that control the radiation field w.r.t local comoving frames
across the stellar surface.
The following code block is how one would pass the properties of a
single-temperature circular ``HotRegion`` to the extension modules. If
you have more than one ``HotRegion`` object merged into the subspace
associated with the ``Photosphere`` object, they may each be prefixed,
meaning that the set of parameter names below would need to be prefixed
at the least, and unless you only want to image one ``HotRegion``, the
parameters of the ``HotRegions`` object are required.
.. highlight:: python
.. code-block:: python
return _np.array([self['super_colatitude'],
self['phase_shift'] * _2pi,
self['super_radius'],
self['super_temperature']])
The phase shift controls the initial rotational phase of the
``HotRegion`` when imaging commences.
"""
try:
return _np.array([self['temperature']])
except KeyError:
raise NotImplementedError('Subclass and provide an implementation.')
@property
def global_to_local_file(self):
try:
return self._global_to_local_file
except AttributeError:
return None
@global_to_local_file.setter
def global_to_local_file(self, filepath):
if not isinstance(filepath, _six.string_types):
raise TypeError('File path must be a string.')
elif not _os.path.isfile(filepath):
raise IOError('File does not exist.')
self._global_to_local_file = filepath
@property
def images(self):
""" Get the precomputed image information. """
return self._images
@images.setter
def images(self, images):
""" Store an *ndarray[i,j,k]* of images and associated information. """
try:
for i, obj in enumerate(images):
if not isinstance(obj, _np.ndarray):
if i < len(images) - 3:
raise TypeError('Image information was expected to be '
'contained in an ndarray.')
elif obj is not None and not isinstance(obj, float):
raise TypeError('Unexpected type for image information.')
except TypeError:
raise TypeError('An iterable of objects containing image '
'information must be supplied.')
if len(images) != 13:
raise ValueError('There must be six ndarray objects specifing '
'image information.')
msg = 'Image information element %i must have %i dimensions.'
# tuple elements:
# energy-phase resolved signal (2D array)
# x coordinate on image plane (1D array)
# y coordinate on image plane (1D array)
# colatitude mapped to point (x,y) on image plane (1D array)
# azimuth mapped to point (x,y) on image plane (1D array)
# radial coord mapped to point (x,y) on image plane (1D array)
# phase lag
# redshift
# aberrated ray angle to local surface normal
# elliptical image-plane radial array
# elliptical image-plane semi-major axis
# elliptical image-plane semi-minor axis
# energy-phase resolved specific intensity sky maps (3D array)
# the last element is None if intensities not cached
assert images[0].ndim == 2, msg % (0, 2)
assert images[1].ndim == 1, msg % (1, 1)
assert images[2].ndim == 1, msg % (2, 1)
assert images[3].ndim == 1, msg % (3, 1)
assert images[4].ndim == 1, msg % (4, 1)
assert images[5].ndim == 1, msg % (5, 1)
assert images[6].ndim == 1, msg % (6, 1)
assert images[7].ndim == 1, msg % (7, 1)
assert images[8].ndim == 1, msg % (8, 1)
assert images[9].ndim == 1, msg % (9, 1)
if images[12] is not None:
assert images[12].ndim == 3, msg % (12, 3)
_num_rays = len(images[1])
for i in range(2,9):
assert len(images[i] == _num_rays),\
('Ray map: array length mismatch (array at tuple index %i is '
'not equal in length to array at tuple index 1).' % i)
assert int( _m.sqrt( _num_rays - 1 ) ) == len(images[9]),\
('Ray map: array length mismatch for image-plane radial '
'coordinate array (array at tuple index 9).')
if images[12] is not None:
assert images[12].shape[0] == images[0].shape[1],\
('Intensity cache dimension 0 does not match the length of '
'dimension 1 of the specific flux array '
'(at tuple index 1), meaning the number of phases '
'is mismatched.')
assert images[12].shape[2] == _num_rays,\
('Intensity cache dimension 2 does not match the length of '
'ray map arrays (e.g., array at tuple index 1), meaning '
'the number of rays is mismatched.')
self._images = images
@images.deleter
def images(self):
del self._images
def load_image_data(self, directory):
""" Load imaging data from disk.
:param str directory:
Path to directory to load files from. Should contain files written
to disk by :meth:`write_image_data`.
"""
_d = directory
photon_specific_flux = _np.load(_join(_d, 'photon_specific_flux.npy'))
x_coordinate = _np.load(_join(_d, 'x_coordinate.npy'))
y_coordinate = _np.load(_join(_d, 'y_coordinate.npy'))
colatitude = _np.load(_join(_d, 'colatitude.npy'))
azimuth = _np.load(_join(_d, 'azimuth.npy'))
radial_coord = _np.load(_join(_d, 'radial_coord.npy'))
phase_lag = _np.load(_join(_d, 'phase_lag.npy'))
redshift = _np.load(_join(_d, 'redshift.npy'))
abberated_angle = _np.load(_join(_d, 'abberated_angle.npy'))
IP_radial_array = _np.load(_join(_d, 'IP_radial_array.npy'))
IP_ellipse_axes = _np.load(_join(_d, 'IP_ellipse_axes.npy'))
intensity = _np.load(_join(_d, 'intensity.npy'))
self.images = [photon_specific_flux,
x_coordinate,
y_coordinate,
colatitude,
azimuth,
radial_coord,
phase_lag,
redshift,
abberated_angle,
IP_radial_array,
IP_ellipse_axes[0],
IP_ellipse_axes[1],
intensity]
def write_image_data(self, directory):
""" Write imaging data to disk.
:param str directory:
Path to directory to write to. Must exist.
"""
_d = directory
_np.save(_join(_d, 'photon_specific_flux.npy'), self.images[0])
_np.save(_join(_d, 'x_coordinate.npy'), self.images[1])
_np.save(_join(_d, 'y_coordinate.npy'), self.images[2])
_np.save(_join(_d, 'colatitude.npy'), self.images[3])
_np.save(_join(_d, 'azimuth.npy'), self.images[4])
_np.save(_join(_d, 'radial_coord.npy'), self.images[5])
_np.save(_join(_d, 'phase_lag.npy'), self.images[6])
_np.save(_join(_d, 'redshift.npy'), self.images[7])
_np.save(_join(_d, 'abberated_angle.npy'), self.images[8])
_np.save(_join(_d, 'IP_radial_array.npy'), self.images[9])
_np.save(_join(_d, 'IP_ellipse_axes.npy'),
_np.array(self.images[10:12], dtype=_np.double))
_np.save(_join(_d, 'intensity.npy'), self.images[12])
@property
def photon_specific_flux(self):
""" Get the photon specific flux as a function of phase and energy.
:return: A two-dimensional :class:`numpy.ndarray`, where photon energy
varies with row number, and phase varies with column number.
"""
return self._images[0]
@property
def photon_specific_intensity(self):
""" Get the photon specific intensity.
Function of phase, energy and sky direction.
:return: A three-dimensional :class:`numpy.ndarray`, where the first
dimension is phase, the second dimension is photon energy,
and the third dimension is sky direction (flattened from
two-dimensional sky coordinates to one dimension).
"""
return self._images[12]
@make_verbose('Imaging the star', 'Star imaged')
def image(self,
reimage = False,
reuse_ray_map = True,
energies = None,
num_phases = None,
phases = None,
phases_in_cycles = False,
sqrt_num_rays = 100,
epsabs_ray = 1.0e-12,
epsrel_ray = 1.0e-12,
max_steps = 100000,
init_step = 0.1,
image_plane_radial_increment_power = 1.0 / 2.0,
threads = 1,
cache_intensities = False,
cache_energy_indices = None,
cache_phase_indices = None,
single_precision_intensities = True,
plot_sky_maps = False,
sky_map_kwargs = None,
animate_sky_maps = False,
free_memory = True,
animate_kwargs = None,
**kwargs):
""" Image the star as a function of phase and energy.
:param bool reimage:
(Re)image the star. If ``False``, but the spacetime configuration
has been updated or the photosphere parameters have been updated,
a warning will be generated. In principle, one might want to plot
sky maps using cached imaging information, or animate sky maps
using images on disk, so reimaging is not forced if (non-fixed)
parameters have been changed.
:param bool reuse_ray_map:
Reuse a precomputed ray map from the stellar surface to the image
plane. If the spacetime configuration has changed (non-fixed
parameters have changed), a cached ray map will *not* be reused. If
the spacetime configuration is unchanged, but resolution settings
have changed for ray tracing, pass ``False`` to adhere to the new
resolution settings.
:param ndarray[n] energies:
Energies in keV to evaluate incident specific intensities at.
:param int num_phases:
The number of phases spanning the unit interval (zero and unity
inclusive) to image at.
:param ndarray[m] phases:
Phases in *radians* or *cycles* at which to evaluate incident
specific intensities at. If not ``None``, takes precedence over
:obj:`num_phases`. The units need to be specified with the
:obj:`phases_in_cycles` keyword argument: if ``False``, give
the phase array in *radians*.
:param bool phases_in_cycles:
Is the phase array, if not ``None``, in units of rotational cycles?
:param int sqrt_num_rays:
Square-root of the number of rays. This is the level of
discretisation in both a radial coordinate and a polar coordinate
on an elliptical image plane.
.. note::
When the spacetime is static or extremely close to being static in
a numerical context, at the resolutions we are interested in, we
need to mitigate problems with rays that graze the pole
infinitesimally close to the polar axis. In the vicinity of the
polar coordinate singularity the ODE system is stiff and the
solution is unstable. The most straightforward way to mitigate this
is to perform a fallback forward Euler step for a ray that passes
exactly through the pole, and use that ray as an approximation for
the grazing ray that started very nearby on the image plane.
Internally, if a ray intersects the image plane at
:math:`x`-coordinate that is numerically very close to, but not
exactly, zero (which would mean alignment to the rotational axis),
it is approximated by a ray that intersects :math:`x=0`.
Image-plane interpolation of quantities (such as intensity) for
the purpose of visualisation will then smooth out any such
artefacts.
Moreover, as an additional measure against artefacts in the sky
maps in the vicinity of the rotational pole, rays are distributed
accordingingly. For example, if we request :math:`n=400` rays per
dimension, a maximal spacing of the rays from the rotational axis
is achieved by rotating the *spokes* of rays (by up to
:math:`\pm\pi/n`) so that no spoke is aligned (or anti-aligned)
with the :math:`y`-direction.
:param float epsabs_ray:
Absolute error tolerance per ray to adhere to during numerical
integration.
:param float epsrel_ray:
Relative error tolerance per ray to adhere to during numerical
integration.
:param int max_steps:
Maximum number of steps to permit per ray before forced termination
of integration.
:param float init_step:
The initial *suggested* step size at the image plane for the
affine parameter for each ray.
:param float image_plane_radial_increment_power:
Controls the behaviour of the radial discretisation.
Higher values towards unity result in linear spacing of rays with
the radial coordinate. Lower values towards zero squeeze the rays
towards the visible stellar limb, which is necessary for resolving
images of extended radiating elements. Values above unity are not
recommended, and would squeeze rays towards the image-plane origin,
compromising resolution at the stellar limb.
:param int threads:
Number of OpenMP threads to spawn for parallel blocks of code.
Parallel blocks include ray integration to generate a global ray
map from image plane to surface; and image calculation at a
sequence of rotational phases.
:param float cache_intensities:
Cache the photon specific intensity sky maps in memory, as a
function of phase and energy? The type must be a float (greater than
or equal to zero) or ``False``. The value represents the limiting
size in GB that can be allocated for the intensity cache. Defaults
to zero because this dominates memory consumption. You need to
activate this option if you want to plot the sky maps (see below).
To activate, supply a limit. A hard limit of 2 GB is imposed for
safety. To override, use the secret :obj:`_OVERRIDE_MEM_LIM`
keyword argument to supply a positive limit in GB.
:param ndarray[m] cache_phase_indices:
A one-dimensional :class:`numpy.ndarray` of ``dtype=numpy.int32``,
specifying the phase-array indices to cache intensities at. This
is useful to save memory when you want to plot specific intensity
skymaps but also compute the specific flux at many more phases. If
``None``, intensities will be cached at all phases subject to
memory constraints. Note that the order of the list matters for
plotting order, so the indices should generally increase, as should
the phases themselves. If plotting the pulse-profile and spectrum,
then this is a case where many more phases are useful for the
resolving specific flux pulse-profile than are needed to plot
specific intensity skymaps and specific flux spectra at three
representative phases.
:param ndarray[m] cache_energy_indices:
A one-dimensional :class:`numpy.ndarray` of ``dtype=numpy.int32``,
specifying the energy-array indices to cache intensities at. This
is useful to save memory when you want to plot specific intensity
skymaps but also compute the specific flux at many more energies. If
``None``, intensities will be cached at all energies subject to
memory constraints. Note that the order of the list matters for
plotting order, so the indices should generally increase, as should
the energies themselves. If plotting the pulse-profile and spectrum,
then this is a case where many more energies are useful for the
resolving specific flux spectrum than are needed to plot specific
intensity skymaps and specific flux pulse-profiles at three
representative energies.
:param bool single_precision_intensities:
Cache the intensities in single precision? In most use cases,
double precision is simply unnecessary, and because memory
consumption can be high, choosing single precision can reduce
memory requirements by a factor of two. Note that this only applies
to the caching of intensities, not the calculation of intensities,
which is done safely in double precision; only the final caching
operation is a demotion cast to single precision. The default
is single precision caching. Option ignored if intensities are not
cached.
:param bool plot_sky_maps:
Plot (specific) intenity sky maps at a sequence of phases, or
by averaging over phase. Maps can be made at one more energies
or energy intervals. The images will be written to disk and
can be used as frames in an animated sequence.
:param dict sky_map_kwargs:
Dictionary of keyword arguments passed to
:meth:`~Photosphere._plot_sky_maps`. Refer to the associated
method docstring for available options.
:param bool animate_sky_maps:
Compile images from disk into an animated sequence.
:param bool free_memory:
Try to free the imaging information before animating a sequence of
sky maps written to disk, to try to avoid high memory usage. For
safety the default is to free the memory, so deactivate this at your
own risk. If there are other non-weak references created to the
underlying objects, the memory may fail to be freed. In the
methods below, the aim is that the native garbage collection cleans
up the references because they only exist in the method local scope
(no closures or globals).
.. note::
Memory used for plotting the sky maps and loading the images from
disk to animate a phase sequence might not be straightforwardly
freed despite efforts to do so, because of non-weak references
covertly held by the matplotlib module.
:param dict animate_kwargs:
Dictionary of keyword arguments passed to
:meth:`~Photosphere._animate`. Refer to the associated method
docstring for available options.
:param bool deactivate_all_verbosity:
Deactivate the verbose output? Note that despite this keyword
argument not appearing in the method signature, it is a valid
switch.
"""
ref = self._spacetime # geometry shortcut saves characters
try:
_DV = deactivate_verbosity
except NameError:
_DV = False
_exc = ValueError('You need to cache intensity sky maps if you '
'want to plot them.')
try:
self.images
except AttributeError:
if not reimage:
if plot_sky_maps:
raise _exc
else:
yield ('Warning: star will not be reimaged... assuming '
'images exist on disk.')
else:
if not reimage and plot_sky_maps and self.images[-1] is None:
raise _exc
if phases is not None and not isinstance(phases, _np.ndarray):
raise TypeError('Imaging phases must be in a 1D ndarray.')
elif isinstance(phases, _np.ndarray):
if phases_in_cycles:
if phases[0] != 0.0 or phases[-1] != 1.0:
_warning('Phase array does not span the unit interval.')
phases *= _2pi
elif phases is None:
if num_phases is None or not isinstance(num_phases, int):
raise TypeError('Integer number of phases required.')
phases = _np.linspace(0.0, 1.0, num_phases) * _2pi
if not isinstance(energies, _np.ndarray):
raise TypeError('Imaging energies must be in a 1D ndarray.')
time_is_space = sky_map_kwargs.get('time_is_space', False)
if reimage:
if plot_sky_maps and not cache_intensities:
raise _exc
if cache_intensities:
_override_mem_lim = kwargs.get('_OVERRIDE_MEM_LIM', 1.0)
if not isinstance(_override_mem_lim, float):
raise TypeError('Intensity cache limit override must be a '
'float.')
elif _override_mem_lim < 0.0:
raise ValueError('Intensity cache limit override must be '
'positive or zero.')
if not isinstance(cache_intensities, float):
raise TypeError('Intensity cache limit must be a float.')
elif not 0.0 <= cache_intensities <= _override_mem_lim:
raise ValueError('Intensity cache limit must be positive '
'and less than the safety limit, which '
'in turn can be overridden as described '
'in the method docstring.')
if cache_energy_indices is None:
cache_energy_indices = _np.arange(len(energies),
dtype=_np.int32)
elif not isinstance(cache_energy_indices, _np.ndarray):
raise TypeError('Energy indices for intensity caching '
'must be supplied in a 1D numpy.ndarray.')
elif cache_energy_indices.dtype != _np.int32:
raise TypeError('Energy indices for intensity caching '
'must be integers.')
elif time_is_space and len(cache_energy_indices) != len(energies):
raise TypeError('Sky maps must be cached at all energies.')
if cache_phase_indices is None:
cache_phase_indices = _np.arange(len(phases),
dtype=_np.int32)
elif not isinstance(cache_phases_indices, _np.ndarray):
raise TypeError('Phase indices for intensity caching '
'must be supplied in a 1D numpy.ndarray.')
elif cache_phase_indices.dtype != _np.int32:
raise TypeError('Phase indices for intensity caching '
'must be integers.')
elif not time_is_space and len(cache_phases_indices) != len(phases):
raise TypeError('Sky maps must be cached at all phases.')
_req_size = 4.0 if single_precision_intensities else 8.0
_req_size *= len(cache_phase_indices) * len(cache_energy_indices) # bytes
_req_size *= sqrt_num_rays**2.0 # + 1.0 # origin ray negligible
if _req_size/1.0e9 >= cache_intensities:
raise MemoryError('Too much memory would be required to '
'cache the intensities at this '
'resolution. Try decreasing the number '
'of rays, energies, and/or phases, or '
'override the cache size limit if '
'safe.')
cache_intensities = True
else:
cache_intensities = False
try:
self.images
except AttributeError:
if reuse_ray_map:
yield ('Warning: a ray map has not been cached... '
'tracing new ray set')
else:
# if spacetime configuration was updated
if ref.needs_update or not reuse_ray_map:
# try to free up memory; CPython reference counting means
# this should have immediate effect
del self.images
else:
# del self.images[0] # doesn't require much memory
del self.images[-1] # requires far more memory
try:
_ray_map = tuple(self.images[1:])
yield 'Cached ray set to be reused... commencing imaging'
except AttributeError:
_ray_map = None
yield 'Commencing ray tracing and imaging'
images = _integrate(threads,
ref.r_s,
ref.R,
ref.Omega,
self['mode_frequency'],
ref.zeta,
ref.epsilon,
ref.a, # dimensionless spin
ref.q, # mass quadrupole
ref.d,
ref.i,
sqrt_num_rays,
epsabs_ray,
epsrel_ray,
max_steps,
init_step,
image_plane_radial_increment_power,
self.global_variables,
energies,
phases,
cache_intensities,
cache_energy_indices,
cache_phase_indices,
single_precision_intensities,
_ray_map,
self.global_to_local_file,
self._hot_atmosphere)
if images[0] == 1:
raise Exception('A numerical error arose during imaging '
'computation... terminating simulation.')
elif _ray_map is not None: # only recalculated info is returned
# tuple elements:
# energy-phase resolved signal (2D array)
# energy-phase resolved specific intensity sky maps (3D array)
# the last element is None if intensities not cached
# transpose so signal phase increments along columns
self.images[0] = images[1].T
self.images.append(images[2])
else: # the ray map is also returned
# tuple elements:
# energy-phase resolved signal (2D array)
# x coordinate on image plane (1D array)
# y coordinate on image plane (1D array)
# colatitude mapped to point (x,y) on image plane (1D array)
# azimuth mapped to point (x,y) on image plane (1D array)
# radial coord mapped to point (x,y) on image plane (1D array)
# phase lag
# redshift
# aberrated ray angle to local surface normal
# elliptical image-plane radial array
# elliptical image-plane semi-major axis
# elliptical image-plane semi-minor axis
# energy-phase resolved specific intensity sky maps (3D array)
# the last element is None if intensities not cached
# transpose so signal phase increments along columns
self.images = [images[1].T] + list(images[2:])
yield 'Ray tracing complete.'
yield 'Ray set cached.'
if cache_intensities:
yield 'Intensity caching complete.'
else:
if len(phases) > 1:
yield 'Phase-resolved specific flux integration complete.'
else:
yield 'Specific flux integration complete.'
# memoization
self._spacetime([param.value for param in self._spacetime])
if sky_map_kwargs is None: sky_map_kwargs = {}
if animate_kwargs is None: animate_kwargs = {}
if plot_sky_maps or animate_sky_maps:
root_dir = sky_map_kwargs.pop('root_dir', './images')
file_root = sky_map_kwargs.pop('file_root', 'skymap')
file_root = _os.path.join(root_dir, file_root)