-
Notifications
You must be signed in to change notification settings - Fork 105
/
gsobject.py
2610 lines (2179 loc) · 129 KB
/
gsobject.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
# Copyright (c) 2012-2023 by the GalSim developers team on GitHub
# https://github.com/GalSim-developers
#
# This file is part of GalSim: The modular galaxy image simulation toolkit.
# https://github.com/GalSim-developers/GalSim
#
# GalSim is free software: redistribution and use in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions, and the disclaimer given in the accompanying LICENSE
# file.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the disclaimer given in the documentation
# and/or other materials provided with the distribution.
#
__all__ = [ 'GSObject' ]
import numpy as np
import math
import copy
from . import _galsim
from .gsparams import GSParams
from .position import _PositionD, _PositionI, Position, parse_pos_args
from ._utilities import lazy_property
from .errors import GalSimError, GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError
from .errors import GalSimFFTSizeError, GalSimNotImplementedError, convert_cpp_errors, galsim_warn
from .image import Image, ImageD, ImageF, ImageCD, ImageCF
from .shear import Shear, _Shear
from .angle import Angle
from .bounds import BoundsI, _BoundsI
from .random import BaseDeviate
from .sensor import Sensor
from .random import PoissonDeviate
from .wcs import BaseWCS, PixelScale
class GSObject:
"""Base class for all GalSim classes that represent some kind of surface brightness profile.
A GSObject is not intended to be constructed directly. Normally, you would use whatever
derived class is appropriate for the surface brightness profile you want::
>>> gal = galsim.Sersic(n=4, half_light_radius=4.3)
>>> psf = galsim.Moffat(beta=3, fwhm=2.85)
>>> conv = galsim.Convolve([gal,psf])
All of these classes are subclasses of GSObject, so you should see those docstrings for
more details about how to construct the various profiles. Here we discuss attributes and
methods that are common to all GSObjects.
GSObjects are always defined in sky coordinates. So all sizes and other linear dimensions
should be in terms of some kind of units on the sky, arcsec for instance. Only later (when
they are drawn) is the connection to pixel coordinates established via a pixel scale or WCS.
(See the documentation for galsim.BaseWCS for more details about how to specify various kinds
of world coordinate systems more complicated than a simple pixel scale.)
For instance, if you eventually draw onto an image that has a pixel scale of 0.2 arcsec/pixel,
then the normal thing to do would be to define your surface brightness profiles in terms of
arcsec and then draw with ``pixel_scale=0.2``. However, while arcsec are the usual choice of
units for the sky coordinates, if you wanted, you could instead define the sizes of all your
galaxies and PSFs in terms of radians and then use ``pixel_scale=0.2/206265`` when you draw
them.
**Transforming methods**:
The GSObject class uses an "immutable" design[1], so all methods that would potentially modify
the object actually return a new object instead. This uses pointers and such behind the
scenes, so it all happens efficiently, but it makes using the objects a bit simpler, since
you don't need to worry about some function changing your object behind your back.
In all cases below, we just give an example usage. See the docstrings for the methods for
more details about how to use them.::
>>> obj = obj.shear(shear) # Apply a shear to the object.
>>> obj = obj.dilate(scale) # Apply a flux-preserving dilation.
>>> obj = obj.magnify(mu) # Apply a surface-brightness-preserving magnification.
>>> obj = obj.rotate(theta) # Apply a rotation.
>>> obj = obj.shift(dx,dy) # Shft the object in real space.
>>> obj = obj.transform(dudx,dudy,dvdx,dvdy) # Apply a general jacobian transformation.
>>> obj = obj.lens(g1,g2,mu) # Apply both a lensing shear and magnification.
>>> obj = obj.withFlux(flux) # Set a new flux value.
>>> obj = obj * ratio # Scale the surface brightness profile by some factor.
**Access Methods**:
There are some access methods and properties that are available for all GSObjects.
Again, see the docstrings for each method for more details.::
>>> obj.flux
>>> obj.centroid
>>> obj.nyquist_scale
>>> obj.stepk
>>> obj.maxk
>>> obj.has_hard_edges
>>> obj.is_axisymmetric
>>> obj.is_analytic_x
>>> obj.is_analytic_k
>>> obj.xValue(x,y) or obj.xValue(pos)
>>> obj.kValue(kx,ky) os obj.kValue(kpos)
Most subclasses have additional methods that are available for values that are particular to
that specific surface brightness profile. e.g. ``sigma = gauss.sigma``. However, note
that class-specific methods are not available after performing one of the above transforming
operations.::
>>> gal = galsim.Gaussian(sigma=5)
>>> gal = gal.shear(g1=0.2, g2=0.05)
>>> sigma = gal.sigma # This will raise an exception.
It is however possible to access the original object that was transformed via the
``original`` attribute.::
>>> sigma = gal.original.sigma # This works.
No matter how many transformations are performed, the ``original`` attribute will contain the
_original_ object (not necessarily the most recent ancestor).
**Drawing Methods**:
The main thing to do with a GSObject once you have built it is to draw it onto an image.
There are two methods that do this. In both cases, there are lots of optional parameters.
See the docstrings for these methods for more details.::
>>> image = obj.drawImage(...)
>>> kimage = obj.drawKImage(...)
There two attributes that may be available for a GSObject.
Attributes:
original: This was mentioned above as a way to access the original object that has
been transformed by one of the transforming methods.
noise: Some types, like `RealGalaxy`, set this attribute to be the intrinsic noise that
is already inherent in the profile and will thus be present when you draw the
object. The noise is propagated correctly through the various transforming
methods, as well as convolutions and flux rescalings. Note that the ``noise``
attribute can be set directly by users even for GSObjects that do not naturally
have one. The typical use for this attribute is to use it to whiten the noise in
the image after drawing. See `BaseCorrelatedNoise` for more details.
**GSParams**:
All GSObject classes take an optional ``gsparams`` argument, so we document that feature here.
For all documentation about the specific derived classes, please see the docstring for each
one individually.
The ``gsparams`` argument can be used to specify various numbers that govern the tradeoff
between accuracy and speed for the calculations made in drawing a GSObject. The numbers are
encapsulated in a class called `GSParams`, and the user should make careful choices whenever
they opt to deviate from the defaults. For more details about the parameters and their default
values, please see the docstring of the `GSParams` class.
For example, let's say you want to do something that requires an FFT larger than 4096 x 4096
(and you have enough memory to handle it!). Then you can create a new `GSParams` object with a
larger ``maximum_fft_size`` and pass that to your GSObject on construction::
>>> gal = galsim.Sersic(n=4, half_light_radius=4.3)
>>> psf = galsim.Moffat(beta=3, fwhm=2.85)
>>> conv = galsim.Convolve([gal,psf])
>>> im = galsim.Image(1000,1000, scale=0.02) # Note the very small pixel scale!
>>> im = conv.drawImage(image=im) # This uses the default GSParams.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "galsim/gsobject.py", line 1666, in drawImage
added_photons = prof.drawFFT(draw_image, add)
File "galsim/gsobject.py", line 1877, in drawFFT
kimage, wrap_size = self.drawFFT_makeKImage(image)
File "galsim/gsobject.py", line 1802, in drawFFT_makeKImage
raise GalSimFFTSizeError("drawFFT requires an FFT that is too large.", Nk)
galsim.errors.GalSimFFTSizeError: drawFFT requires an FFT that is too large.
The required FFT size would be 12288 x 12288, which requires 3.38 GB of memory.
If you can handle the large FFT, you may update gsparams.maximum_fft_size.
>>> big_fft_params = galsim.GSParams(maximum_fft_size=12300)
>>> conv = galsim.Convolve([gal,psf],gsparams=big_fft_params)
>>> im = conv.drawImage(image=im) # Now it works (but is slow!)
>>> im.write('high_res_sersic.fits')
Note that for compound objects such as `Convolution` or `Sum`, not all `GSParams` can be
changed when the compound object is created. In the example given here, it is possible to
change parameters related to the drawing, but not the Fourier space parameters for the
components that go into the `Convolution`. To get better sampling in Fourier space,
for example, the ``gal`` and/or ``psf`` should be created with ``gsparams`` that have a
non-default value of ``folding_threshold``. This statement applies to the threshold and
accuracy parameters.
"""
_gsparams_opt = { 'minimum_fft_size' : int,
'maximum_fft_size' : int,
'folding_threshold' : float,
'stepk_minimum_hlr' : float,
'maxk_threshold' : float,
'kvalue_accuracy' : float,
'xvalue_accuracy' : float,
'realspace_relerr' : float,
'realspace_abserr' : float,
'integration_relerr' : float,
'integration_abserr' : float,
'shoot_accuracy' : float,
'allowed_flux_variation' : float,
'range_division_for_extrema' : int,
'small_fraction_of_flux' : float
}
redshift = 0 # For backwards compatibility with old atRedshift usage. Can be overwritten.
def __init__(self):
raise NotImplementedError("The GSObject base class should not be instantiated directly.")
# Note: subclasses are expected to define the following attributes or properties:
#
# Required for all profiles:
#
# _flux (the object's flux, natch)
# _gsparams (use GSParams.check(None) if you just want the default)
# _stepk (the sampling in k space necessary to avoid folding of image in x space)
# _maxk (the value of k beyond which aliasing can be neglected)
# _has_hard_edges (true if should use real-space convolution with another hard edge profile)
# _is_axisymmetric (true if f(x,y) = f(r))
# _is_analytic_x (true if _xValue and _drawReal are implemented)
# _is_analytic_k (true if _kValue and _drawKImage are implemented)
#
# Required and typically class attributes, but there are defaults in each case:
#
# _req_params (dict of required config parameters: name : type, default: {})
# _opt_params (dict of optional config parameters: name : type, default: {})
# _single_params (list of dicts for parameters where exactly one of several is required,
# default: [])
# _takes_rng (bool specifying whether rng is an input parameter, default: False)
#
# Optional
#
# _centroid (default = PositionD(0,0), which is often the right value)
# _positive_flux (default = _flux + _negative_flux)
# _negative_flux (default = 0; note: this should be absolute value of the negative flux)
# _max_sb (default 1.e500, which in this context is equivalent to "unknown")
# _noise (default None)
#
# In addition, subclasses should typically define most of the following methods.
# The default in each case is to raise a NotImplementedError, so if you cannot implement one,
# you may simply not define it.
#
# _xValue(self, pos)
# _kValue(self, kpos)
# _drawReal(self, image)
# _shoot(self, photons, rng):
# _drawKImage(self, image)
#
# Required for real-space convolution
#
# _sbp which must be an attribute or property providing a C++-layer SBProfile instance.
#
# Note that most objects don't need to implement real-space convolution, so use of a C++-layer
# SBProfile sub-class is usually only an implementation detail to improve efficiency.
#
# TODO: For now, _sbp is also required for transformations, but this is expected to be
# addressed in a future PR.
@property
def flux(self):
"""The flux of the profile.
"""
return self._flux
@property
def gsparams(self):
"""A `GSParams` object that sets various parameters relevant for speed/accuracy trade-offs.
"""
return self._gsparams
@property
def maxk(self):
"""The value of k beyond which aliasing can be neglected.
"""
return self._maxk
@property
def stepk(self):
"""The sampling in k space necessary to avoid folding of image in x space.
"""
return self._stepk
@property
def nyquist_scale(self):
"""The pixel spacing that does not alias maxk.
"""
return math.pi / self.maxk
@property
def has_hard_edges(self):
"""Whether there are any hard edges in the profile, which would require very small k
spacing when working in the Fourier domain.
"""
return self._has_hard_edges
@property
def is_axisymmetric(self):
"""Whether the profile is axially symmetric; affects efficiency of evaluation.
"""
return self._is_axisymmetric
@property
def is_analytic_x(self):
"""Whether the real-space values can be determined immediately at any position without
requiring a Discrete Fourier Transform.
"""
return self._is_analytic_x
@property
def is_analytic_k(self):
"""Whether the k-space values can be determined immediately at any position without
requiring a Discrete Fourier Transform.
"""
return self._is_analytic_k
@property
def centroid(self):
"""The (x, y) centroid of an object as a `PositionD`.
"""
return self._centroid
@lazy_property
def _centroid(self):
# Most profiles are centered at 0,0, so make this the default.
return _PositionD(0,0)
@property
def positive_flux(self):
"""The expectation value of flux in positive photons.
Some profiles, when rendered with photon shooting, need to shoot both positive- and
negative-flux photons. For such profiles, this method returns the total flux
of the positive-valued photons.
For profiles that don't have this complication, this is equivalent to getFlux().
It should be generally true that ``obj.positive_flux - obj.negative_flux`` returns the same
thing as ``obj.flux``. Small difference may accrue from finite numerical accuracy in
cases involving lookup tables, etc.
"""
return self._positive_flux
@property
def negative_flux(self):
"""Returns the expectation value of flux in negative photons.
Some profiles, when rendered with photon shooting, need to shoot both positive- and
negative-flux photons. For such profiles, this method returns the total absolute flux
of the negative-valued photons (i.e. as a positive value).
For profiles that don't have this complication, this returns 0.
It should be generally true that ``obj.positive_flux - obj.negative_flux`` returns the same
thing as ``obj.flux``. Small difference may accrue from finite numerical accuracy in
cases involving lookup tables, etc.
"""
return self._negative_flux
@lazy_property
def _positive_flux(self):
# The usual case.
return self.flux + self._negative_flux
@lazy_property
def _negative_flux(self):
# The usual case.
return 0.
@lazy_property
def _flux_per_photon(self):
# The usual case.
return 1.
def _calculate_flux_per_photon(self):
# If negative_flux is overriden, then _flux_per_photon should be overridden as well
# to return this calculation.
posflux = self.positive_flux
negflux = self.negative_flux
eta = negflux / (posflux + negflux)
return 1.-2.*eta
@property
def max_sb(self):
"""An estimate of the maximum surface brightness of the object.
Some profiles will return the exact peak SB, typically equal to the value of
obj.xValue(obj.centroid). However, not all profiles (e.g. Convolution) know how to
calculate this value without just drawing the image and checking what the maximum value is.
Clearly, this would be inefficient, so in these cases, some kind of estimate is returned,
which will generally be conservative on the high side.
This routine is mainly used by the photon shooting process, where an overestimate of
the maximum surface brightness is acceptable.
Note, for negative-flux profiles, this will return the absolute value of the most negative
surface brightness. Technically, it is an estimate of the maximum deviation from zero,
rather than the maximum value. For most profiles, these are the same thing.
"""
return self._max_sb
@lazy_property
def _max_sb(self):
# The way this is used, overestimates are conservative.
# So the default value of 1.e500 will skip the optimization involving the maximum sb.
return 1.e500
@property
def noise(self):
"""An estimate of the noise already in the profile.
Some profiles have some noise already in their definition. E.g. those that come from
observations of galaxies in real data. In GalSim, `RealGalaxy` objects are an example of
this. In these cases, the noise attribute gives an estimate of the Noise object that
would generate noise consistent with that already in the profile.
It is permissible to attach a noise estimate to an existing object with::
>>> obj.noise = noise # Some BaseNoise instance
"""
return self._noise
@noise.setter
def noise(self, n):
# We allow the user to set the noise with obj.noise = n
self._noise = n
@lazy_property
def _noise(self):
# Most profiles don't have any noise.
return None
# a few definitions for using GSObjects as duck-typed ChromaticObjects
@property
def separable(self): return True
@property
def interpolated(self): return False
@property
def deinterpolated(self): return self
@property
def sed(self):
return sed.SED(self.flux, 'nm', '1')
@property
def SED(self):
from .deprecated import depr
depr('obj.SED', 2.5, 'obj.sed')
return self.sed
@property
def spectral(self): return False
@property
def dimensionless(self): return True
@property
def wave_list(self): return np.array([], dtype=float)
def _fiducial_profile(self, bandpass): return bandpass.effective_wavelength, self
# Also need these methods to duck-type as a ChromaticObject
def evaluateAtWavelength(self, wave):
return self
def _approxWavelength(self, wave):
return wave, self
# Make op+ of two GSObjects work to return an Add object
# Note: we don't define __iadd__ and similar. Let python handle this automatically
# to make obj += obj2 be equivalent to obj = obj + obj2.
def __add__(self, other):
"""Add two GSObjects.
Equivalent to Add(self, other)
"""
return _sum.Add([self, other])
# op- is unusual, but allowed. It subtracts off one profile from another.
def __sub__(self, other):
"""Subtract two GSObjects.
Equivalent to Add(self, -1 * other)
"""
return _sum.Add([self, (-1. * other)])
# Make op* work to adjust the flux of an object
def __mul__(self, other):
"""Scale the flux of the object by the given factor.
obj * flux_ratio is equivalent to obj.withScaledFlux(flux_ratio)
It creates a new object that has the same profile as the original, but with the
surface brightness at every location scaled by the given amount.
You can also multiply by an `SED`, which will create a `ChromaticObject` where the `SED`
acts like a wavelength-dependent ``flux_ratio``.
"""
return self.withScaledFlux(other)
def __rmul__(self, other):
"""Equivalent to obj * other. See `__mul__` for details."""
return self.__mul__(other)
# Likewise for op/
def __div__(self, other):
"""Equivalent to obj * (1/other). See `__mul__` for details."""
return self * (1. / other)
__truediv__ = __div__
def __neg__(self):
return -1. * self
# Some calculations that can be done for all GSObjects.
def calculateHLR(self, size=None, scale=None, centroid=None, flux_frac=0.5):
"""Returns the half-light radius of the object.
If the profile has a half_light_radius attribute, it will just return that, but in the
general case, we draw the profile and estimate the half-light radius directly.
This function (by default at least) is only accurate to a few percent, typically.
Possibly worse depending on the profile being measured. If you care about a high
precision estimate of the half-light radius, the accuracy can be improved using the
optional parameter scale to change the pixel scale used to draw the profile.
The default scale is half the Nyquist scale, which were found to produce results accurate
to a few percent on our internal tests. Using a smaller scale will be more accurate at
the expense of speed.
In addition, you can optionally specify the size of the image to draw. The default size is
None, which means `drawImage` will choose a size designed to contain around 99.5% of the
flux. This is overkill for this calculation, so choosing a smaller size than this may
speed up this calculation somewhat.
Also, while the name of this function refers to the half-light radius, in fact it can also
calculate radii that enclose other fractions of the light, according to the parameter
``flux_frac``. E.g. for r90, you would set flux_frac=0.90.
The default scale should usually be acceptable for things like testing that a galaxy
has a reasonable resolution, but they should not be trusted for very fine grain
discriminations.
Parameters:
size: If given, the stamp size to use for the drawn image. [default: None,
which will let `drawImage` choose the size automatically]
scale: If given, the pixel scale to use for the drawn image. [default:
0.5 * self.nyquist_scale]
centroid: The position to use for the centroid. [default: self.centroid]
flux_frac: The fraction of light to be enclosed by the returned radius.
[default: 0.5]
Returns:
an estimate of the half-light radius in physical units
"""
try:
# It there is a half_light_radius attribute, use that.
return self.half_light_radius
except (AttributeError, GalSimError):
# Otherwise, or (e.g. with Airy where it is only implemented for obscuration=0)
# if there is an error trying to use it, then keep going with this calculation.
pass
if scale is None:
scale = self.nyquist_scale * 0.5
if centroid is None:
centroid = self.centroid
# Draw the image. Note: need a method that integrates over pixels to get flux right.
# The offset is to make all the rsq values different to help the precision a bit.
offset = _PositionD(0.2, 0.33)
im = self.drawImage(nx=size, ny=size, scale=scale, offset=offset, dtype=float)
center = im.true_center + offset + centroid/scale
return im.calculateHLR(center=center, flux=self.flux, flux_frac=flux_frac)
def calculateMomentRadius(self, size=None, scale=None, centroid=None, rtype='det'):
"""Returns an estimate of the radius based on unweighted second moments.
The second moments are defined as:
Q_ij = int( I(x,y) i j dx dy ) / int( I(x,y) dx dy )
where i,j may be either x or y.
If I(x,y) is a Gaussian, then T = Tr(Q) = Qxx + Qyy = 2 sigma^2. Thus, one reasonable
choice for a "radius" for an arbitrary profile is sqrt(T/2).
In addition, det(Q) = sigma^4. So another choice for an arbitrary profile is det(Q)^1/4.
This routine can return either of these measures according to the value of the ``rtype``
parameter. ``rtype='trace'`` will cause it to return sqrt(T/2). ``rtype='det'`` will cause
it to return det(Q)^1/4. And ``rtype='both'`` will return a tuple with both values.
Note that for the special case of a Gaussian profile, no calculation is necessary, and
the ``sigma`` attribute will be used in both cases. In the limit as scale->0, this
function will return the same value, but because finite pixels are drawn, the results
will not be precisely equal for real use cases. The approximation being made is that
the integral of I(x,y) i j dx dy over each pixel can be approximated as
int(I(x,y) dx dy) * i_center * j_center.
This function (by default at least) is only accurate to a few percent, typically.
Possibly worse depending on the profile being measured. If you care about a high
precision estimate of the radius, the accuracy can be improved using the optional
parameters size and scale to change the size and pixel scale used to draw the profile.
The default is to use the the Nyquist scale for the pixel scale and let `drawImage`
choose a size for the stamp that will enclose at least 99.5% of the flux. These
were found to produce results accurate to a few percent on our internal tests.
Using a smaller scale and larger size will be more accurate at the expense of speed.
The default parameters should usually be acceptable for things like testing that a galaxy
has a reasonable resolution, but they should not be trusted for very fine grain
discriminations. For a more accurate estimate, see galsim.hsm.FindAdaptiveMom.
Parameters:
size: If given, the stamp size to use for the drawn image. [default: None,
which will let `drawImage` choose the size automatically]
scale: If given, the pixel scale to use for the drawn image. [default:
self.nyquist_scale]
centroid: The position to use for the centroid. [default: self.centroid]
rtype: There are three options for this parameter:
- 'trace' means return sqrt(T/2)
- 'det' means return det(Q)^1/4
- 'both' means return both: (sqrt(T/2), det(Q)^1/4)
[default: 'det']
Returns:
an estimate of the radius in physical units (or both estimates if rtype == 'both')
"""
if rtype not in ('trace', 'det', 'both'):
raise GalSimValueError("Invalid rtype.", rtype, ('trace', 'det', 'both'))
if hasattr(self, 'sigma'):
if rtype == 'both':
return self.sigma, self.sigma
else:
return self.sigma
if scale is None:
scale = self.nyquist_scale
if centroid is None:
centroid = self.centroid
# Draw the image. Note: need a method that integrates over pixels to get flux right.
im = self.drawImage(nx=size, ny=size, scale=scale, dtype=float)
center = im.true_center + centroid/scale
return im.calculateMomentRadius(center=center, flux=self.flux, rtype=rtype)
def calculateFWHM(self, size=None, scale=None, centroid=None):
"""Returns the full-width half-maximum (FWHM) of the object.
If the profile has a fwhm attribute, it will just return that, but in the general case,
we draw the profile and estimate the FWHM directly.
As with `calculateHLR` and `calculateMomentRadius`, this function optionally takes size and
scale values to use for the image drawing. The default is to use the the Nyquist scale
for the pixel scale and let `drawImage` choose a size for the stamp that will enclose at
least 99.5% of the flux. These were found to produce results accurate to well below
one percent on our internal tests, so it is unlikely that you will want to adjust
them for accuracy. However, using a smaller size than default could help speed up
the calculation, since the default is usually much larger than is needed.
Parameters:
size: If given, the stamp size to use for the drawn image. [default: None,
which will let `drawImage` choose the size automatically]
scale: If given, the pixel scale to use for the drawn image. [default:
self.nyquist_scale]
centroid: The position to use for the centroid. [default: self.centroid]
Returns:
an estimate of the full-width half-maximum in physical units
"""
if hasattr(self, 'fwhm'):
return self.fwhm
if scale is None:
scale = self.nyquist_scale
if centroid is None:
centroid = self.centroid
# Draw the image. Note: draw with method='sb' here, since the fwhm is a property of the
# raw surface brightness profile, not integrated over pixels.
# The offset is to make all the rsq values different to help the precision a bit.
offset = _PositionD(0.2, 0.33)
im = self.drawImage(nx=size, ny=size, scale=scale, offset=offset, method='sb', dtype=float)
# Get the maximum value, assuming the maximum is at the centroid.
if self.is_analytic_x:
Imax = self.xValue(centroid)
else:
im1 = self.drawImage(nx=1, ny=1, scale=scale, method='sb', offset=-centroid/scale)
Imax = im1(1,1)
center = im.true_center + offset + centroid/scale
return im.calculateFWHM(center=center, Imax=Imax)
def xValue(self, *args, **kwargs):
"""Returns the value of the object at a chosen 2D position in real space.
This function returns the surface brightness of the object at a particular position
in real space. The position argument may be provided as a `PositionD` or `PositionI`
argument, or it may be given as x,y (either as a tuple or as two arguments).
The object surface brightness profiles are typically defined in world coordinates, so
the position here should be in world coordinates as well.
Not all `GSObject` classes can use this method. Classes like Convolution that require a
Discrete Fourier Transform to determine the real space values will not do so for a single
position. Instead a GalSimError will be raised. The xValue() method is available if and
only if ``obj.is_analytic_x == True``.
Users who wish to use the xValue() method for an object that is the convolution of other
profiles can do so by drawing the convolved profile into an image, using the image to
initialize a new `InterpolatedImage`, and then using the xValue() method for that new
object.
Parameters:
position: The position at which you want the surface brightness of the object.
Returns:
the surface brightness at that position.
"""
pos = parse_pos_args(args,kwargs,'x','y')
return self._xValue(pos)
def _xValue(self, pos):
"""Equivalent to `xValue`, but ``pos`` must be a `galsim.PositionD` instance
Parameters:
pos: The position at which you want the surface brightness of the object.
Returns:
the surface brightness at that position.
"""
raise NotImplementedError("%s does not implement xValue"%self.__class__.__name__)
def kValue(self, *args, **kwargs):
"""Returns the value of the object at a chosen 2D position in k space.
This function returns the amplitude of the fourier transform of the surface brightness
profile at a given position in k space. The position argument may be provided as a
`Position` argument, or it may be given as kx,ky (either as a tuple or as two arguments).
Technically, kValue() is available if and only if the given obj has ``obj.is_analytic_k
== True``, but this is the case for all `GSObject` classes currently, so that should never
be an issue (unlike for `xValue`).
Parameters:
position: The position in k space at which you want the fourier amplitude.
Returns:
the amplitude of the fourier transform at that position.
"""
kpos = parse_pos_args(args,kwargs,'kx','ky')
return self._kValue(kpos)
def _kValue(self, kpos): # pragma: no cover (all our classes override this)
"""Equivalent to `kValue`, but ``kpos`` must be a `galsim.PositionD` instance.
"""
raise NotImplementedError("%s does not implement kValue"%self.__class__.__name__)
def withGSParams(self, gsparams=None, **kwargs):
"""Create a version of the current object with the given `GSParams`.
You may either provide a `GSParams` instance::
>>> gsparams = galsim.GSParams(folding_threshold=1.e-4, maxk_threshold=1.e-4)
>>> obj = obj.withGSParams(gsparams)
or one or more named parameters as keyword arguments::
>>> obj = obj.withGSParams(folding_threshold=1.e-4, maxk_threshold=1.e-4)
.. note::
The latter style will leave all non-named parameters at their current
values. It only updates the named parameters to the given values.
"""
# Note to developers: objects that wrap other objects should override this in order
# to apply the new gsparams to the components.
# This implementation relies on getstate/setstate clearing out any _sbp or similar
# attribute that depends on the details of gsparams. If there are stored calculations
# aside from these, you should also clear them as well, or update them.
if gsparams == self.gsparams: return self
ret = copy.copy(self)
ret._gsparams = GSParams.check(gsparams, self.gsparams, **kwargs)
return ret
def withFlux(self, flux):
"""Create a version of the current object with a different flux.
This function is equivalent to ``obj.withScaledFlux(flux / obj.flux)``.
It creates a new object that has the same profile as the original, but with the
surface brightness at every location rescaled such that the total flux will be
the given value. Note that if ``flux`` is an `SED`, the return value will be a
`ChromaticObject` with specified `SED`.
Parameters:
flux: The new flux for the object.
Returns:
the object with the new flux
"""
return self.withScaledFlux(flux / self.flux)
def withScaledFlux(self, flux_ratio):
"""Create a version of the current object with the flux scaled by the given ``flux_ratio``.
This function is equivalent to ``obj.withFlux(flux_ratio * obj.flux)``. Indeed, withFlux()
is implemented in terms of this one.
It creates a new object that has the same profile as the original, but with the
surface brightness at every location scaled by the given amount. If ``flux_ratio`` is an
`SED`, then the returned object is a `ChromaticObject` with the `SED` multiplied by
its current ``flux``.
Note that in this case the ``flux`` attribute of the `GSObject` being scaled gets
interpreted as being dimensionless, instead of having its normal units of [photons/s/cm^2].
The photons/s/cm^2 units are (optionally) carried by the `SED` instead, or even left out
entirely if the `SED` is dimensionless itself (see discussion in the `ChromaticObject`
docstring). The `GSObject` ``flux`` attribute *does* still contribute to the
`ChromaticObject` normalization, though. For example, the following are equivalent::
>>> chrom_obj = gsobj.withScaledFlux(sed * 3.0)
>>> chrom_obj2 = (gsobj * 3.0).withScaledFlux(sed)
An equivalent, and usually simpler, way to effect this scaling is::
>>> obj = obj * flux_ratio
Parameters:
flux_ratio: The ratio by which to rescale the flux of the object when creating a new
one.
Returns:
the object with the new flux.
"""
# Prohibit non-SED callable flux_ratio here as most likely an error.
if hasattr(flux_ratio, '__call__') and not isinstance(flux_ratio, sed.SED):
raise TypeError('callable flux_ratio must be an SED.')
if flux_ratio == 1:
return self
else:
return transform.Transform(self, flux_ratio=flux_ratio)
def expand(self, scale):
"""Expand the linear size of the profile by the given ``scale`` factor, while preserving
surface brightness.
e.g. ``half_light_radius`` <-- ``half_light_radius * scale``
This doesn't correspond to either of the normal operations one would typically want to do to
a galaxy. The functions dilate() and magnify() are the more typical usage. But this
function is conceptually simple. It rescales the linear dimension of the profile, while
preserving surface brightness. As a result, the flux will necessarily change as well.
See dilate() for a version that applies a linear scale factor while preserving flux.
See magnify() for a version that applies a scale factor to the area while preserving surface
brightness.
Parameters:
scale: The factor by which to scale the linear dimension of the object.
Returns:
the expanded object.
"""
return transform.Transform(self, jac=[scale, 0., 0., scale])
def dilate(self, scale):
"""Dilate the linear size of the profile by the given ``scale`` factor, while preserving
flux.
e.g. ``half_light_radius`` <-- ``half_light_radius * scale``
See expand() and magnify() for versions that preserve surface brightness, and thus
changes the flux.
Parameters:
scale: The linear rescaling factor to apply.
Returns:
the dilated object.
"""
# equivalent to self.expand(scale) * (1./scale**2)
return transform.Transform(self, jac=[scale, 0., 0., scale], flux_ratio=scale**-2)
def magnify(self, mu):
"""Create a version of the current object with a lensing magnification applied to it,
scaling the area and flux by ``mu`` at fixed surface brightness.
This process applies a lensing magnification mu, which scales the linear dimensions of the
image by the factor sqrt(mu), i.e., ``half_light_radius`` <--
``half_light_radius * sqrt(mu)`` while increasing the flux by a factor of mu. Thus,
magnify() preserves surface brightness.
See dilate() for a version that applies a linear scale factor while preserving flux.
See expand() for a version that applies a linear scale factor while preserving surface
brightness.
Parameters:
mu: The lensing magnification to apply.
Returns:
the magnified object.
"""
return self.expand(math.sqrt(mu))
def shear(self, *args, **kwargs):
"""Create a version of the current object with an area-preserving shear applied to it.
The arguments may be either a `Shear` instance or arguments to be used to initialize one.
For more details about the allowed keyword arguments, see the `Shear` docstring.
The shear() method precisely preserves the area. To include a lensing distortion with
the appropriate change in area, either use shear() with magnify(), or use lens(), which
combines both operations.
Parameters:
shear: The `Shear` to be applied. Or, as described above, you may instead supply
parameters do construct a shear directly. eg. ``obj.shear(g1=g1,g2=g2)``.
Returns:
the sheared object.
"""
if len(args) == 1:
if kwargs:
raise TypeError("Error, gave both unnamed and named arguments to GSObject.shear!")
if not isinstance(args[0], Shear):
raise TypeError("Error, unnamed argument to GSObject.shear is not a Shear!")
shear = args[0]
elif len(args) > 1:
raise TypeError("Error, too many unnamed arguments to GSObject.shear!")
elif len(kwargs) == 0:
raise TypeError("Error, shear argument is required")
else:
shear = Shear(**kwargs)
return transform.Transform(self, shear.getMatrix())
def _shear(self, shear):
"""Equivalent to `GSObject.shear`, but without the overhead of sanity checks or other
ways to input the ``shear`` value.
Also, it won't propagate any noise attribute.
Parameters:
shear: The `Shear` to be applied.
Returns:
the sheared object.
"""
return transform._Transform(self, shear.getMatrix())
def lens(self, g1, g2, mu):
"""Create a version of the current object with both a lensing shear and magnification
applied to it.
This `GSObject` method applies a lensing (reduced) shear and magnification. The shear must
be specified using the g1, g2 definition of shear (see `Shear` for more details).
This is the same definition as the outputs of the PowerSpectrum and NFWHalo classes, which
compute shears according to some lensing power spectrum or lensing by an NFW dark matter
halo. The magnification determines the rescaling factor for the object area and flux,
preserving surface brightness.
Parameters:
g1: First component of lensing (reduced) shear to apply to the object.
g2: Second component of lensing (reduced) shear to apply to the object.
mu: Lensing magnification to apply to the object. This is the factor by which
the solid angle subtended by the object is magnified, preserving surface
brightness.
Returns:
the lensed object.
"""
shear = Shear(g1=g1, g2=g2)
return transform.Transform(self, shear.getMatrix() * math.sqrt(mu))
def _lens(self, g1, g2, mu):
"""Equivalent to `GSObject.lens`, but without the overhead of some of the sanity checks.
Also, it won't propagate any noise attribute.
Parameters:
g1: First component of lensing (reduced) shear to apply to the object.
g2: Second component of lensing (reduced) shear to apply to the object.
mu: Lensing magnification to apply to the object. This is the factor by which
the solid angle subtended by the object is magnified, preserving surface
brightness.
Returns:
the lensed object.
"""
shear = _Shear(g1 + 1j*g2)
return transform._Transform(self, shear.getMatrix() * math.sqrt(mu))
def rotate(self, theta):
"""Rotate this object by an `Angle` ``theta``.
Parameters: