-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Chromatic seeing when photon shooting #1225
Changes from 4 commits
851e979
fce671c
2ddb484
d69b572
73c2733
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,6 +221,11 @@ def work(i, atm): | |
significantly faster than computing PSFs with non-frozen-flow | ||
atmospheres. If ``alpha`` != 1.0, then it is required that a | ||
``time_step`` is also specified. [default: 1.0] | ||
seeing_exp: Power law index for wavelength dependent seeing when using geometric | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had talked about deriving this value from L0. Is that possible? If so, it would make it easier for users who may be using a VonKarman profile and want to get this to match up correctly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, my memory for how these are related was faulty. There's a whole slide deck here showing some attempts to infer a relation, but I'm not confident this can or should be used directly. Safer to just let the user specify a value I think. |
||
photon shooting. For pure Kolmogorov turbulence, the correct value is | ||
-0.2, but values in the range [-0.2, -0.4] are expected for Von Karman | ||
turbulence with finite outer scales. This value is ignored if drawing | ||
in FFT mode. [default: 0.0]. | ||
time_step: Time interval between phase boiling updates. Note that this is distinct | ||
from the time interval used to integrate the PSF over time, which is set | ||
by the ``time_step`` keyword argument to `PhaseScreenPSF` or | ||
|
@@ -246,8 +251,8 @@ def work(i, atm): | |
September 2014 | ||
""" | ||
def __init__(self, screen_size, screen_scale=None, altitude=0.0, r0_500=0.2, L0=25.0, | ||
vx=0.0, vy=0.0, alpha=1.0, time_step=None, rng=None, suppress_warning=False, | ||
mp_context=None): | ||
vx=0.0, vy=0.0, alpha=1.0, seeing_exp=0.0, time_step=None, rng=None, | ||
suppress_warning=False, mp_context=None): | ||
if (alpha != 1.0 and time_step is None): | ||
raise GalSimIncompatibleValuesError( | ||
"No time_step provided when alpha != 1.0", alpha=alpha, time_step=time_step) | ||
|
@@ -274,6 +279,7 @@ def __init__(self, screen_size, screen_scale=None, altitude=0.0, r0_500=0.2, L0= | |
self.vx = vx | ||
self.vy = vy | ||
self.alpha = alpha | ||
self.seeing_exp = seeing_exp | ||
|
||
if rng is None: | ||
rng = BaseDeviate() | ||
|
@@ -689,7 +695,7 @@ def _wavefront(self, u, v, t, theta): | |
v += self._altitude*theta[1].tan() | ||
return self._tab2d._call_wrap(u.ravel(), v.ravel()).reshape(u.shape) | ||
|
||
def wavefront_gradient(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): | ||
def wavefront_gradient(self, u, v, t=None, w=None, theta=(0.0*radians, 0.0*radians)): | ||
""" Compute gradient of wavefront due to atmospheric phase screen. | ||
|
||
Parameters: | ||
|
@@ -702,6 +708,10 @@ def wavefront_gradient(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): | |
will be used for all u, v. If scalar, then the size will be broadcast up to | ||
match that of u and v. If iterable, then the shape must match the shapes of | ||
u and v. [default: None] | ||
w: Wavelength in nanometers to use to compute gradient. This is a bit of a | ||
hack to implement chromatic seeing in photon-shooting mode. Returned gradients | ||
are multiplied by (wavelength/500)^(self.seeing_exp). If not present, then | ||
no chromatic seeing correction is applied. [default: None] | ||
theta: Field angle at which to evaluate wavefront, as a 2-tuple of `galsim.Angle` | ||
instances. [default: (0.0*galsim.arcmin, 0.0*galsim.arcmin)] Only a single | ||
theta is permitted. | ||
|
@@ -728,7 +738,7 @@ def wavefront_gradient(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): | |
self.instantiate() # noop if already instantiated | ||
|
||
if self.reversible: | ||
return self._wavefront_gradient(u, v, t, theta) | ||
return self._wavefront_gradient(u, v, t, w, theta) | ||
else: | ||
dwdu = np.empty_like(u, dtype=np.float64) | ||
dwdv = np.empty_like(u, dtype=np.float64) | ||
|
@@ -738,19 +748,25 @@ def wavefront_gradient(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): | |
while tt <= tmax: | ||
self._seek(tt) | ||
here = ((tt <= t) & (t < tt+self.time_step)) | ||
dwdu[here], dwdv[here] = self._wavefront_gradient(u[here], v[here], t[here], theta) | ||
w_here = None if w is None else w[here] | ||
dwdu[here], dwdv[here] = self._wavefront_gradient( | ||
u[here], v[here], t[here], w_here, theta | ||
) | ||
tt += self.time_step | ||
return dwdu, dwdv | ||
|
||
def _wavefront_gradient(self, u, v, t, theta): | ||
# Same as wavefront(), but no argument checking and no boiling updates. | ||
def _wavefront_gradient(self, u, v, t, w, theta): | ||
# Same as wavefront_gradient(), but no argument checking and no boiling updates. | ||
u = u - t*self.vx | ||
if theta[0].rad != 0: | ||
u += self._altitude*theta[0].tan() | ||
v = v - t*self.vy | ||
if theta[1].rad != 0: | ||
v += self._altitude*theta[1].tan() | ||
dfdx, dfdy = self._tab2d._gradient_wrap(u.ravel(), v.ravel()) | ||
if w is not None and self.seeing_exp is not None: | ||
dfdx *= (w/500.)**self.seeing_exp | ||
dfdy *= (w/500.)**self.seeing_exp | ||
return dfdx.reshape(u.shape), dfdy.reshape(u.shape) | ||
|
||
|
||
|
@@ -1087,7 +1103,7 @@ def _wavefront(self, u, v, t, theta): | |
# Note, this phase screen is actually independent of time and theta. | ||
return self._zernike.evalCartesian(u, v) * self.lam_0 | ||
|
||
def wavefront_gradient(self, u, v, t=None, theta=None): | ||
def wavefront_gradient(self, u, v, t=None, w=None, theta=None): | ||
""" Compute gradient of wavefront due to optical phase screen. | ||
|
||
Parameters: | ||
|
@@ -1096,6 +1112,7 @@ def wavefront_gradient(self, u, v, t=None, theta=None): | |
v: Vertical pupil coordinate (in meters) at which to evaluate wavefront. Can | ||
be a scalar or an iterable. The shapes of u and v must match. | ||
t: Ignored for `OpticalScreen`. | ||
w: Ignored for `OpticalScreen`. | ||
theta: Ignored for `OpticalScreen`. | ||
|
||
Returns: | ||
|
@@ -1105,10 +1122,10 @@ def wavefront_gradient(self, u, v, t=None, theta=None): | |
v = np.array(v, dtype=float, copy=False) | ||
if u.shape != v.shape: | ||
raise GalSimIncompatibleValuesError("u.shape not equal to v.shape", u=u, v=v) | ||
return self._wavefront_gradient(u, v, t, theta) | ||
return self._wavefront_gradient(u, v, t, w, theta) | ||
|
||
|
||
def _wavefront_gradient(self, u, v, t, theta): | ||
def _wavefront_gradient(self, u, v, t, w, theta): | ||
# Same as wavefront(), but no argument checking. | ||
# Note, this phase screen is actually independent of time and theta. | ||
gradx, grady = self._zernike.evalCartesianGrad(u, v) | ||
|
@@ -1213,7 +1230,7 @@ def wavefront(self, u, v, t=None, theta=None): | |
def _wavefront(self, u, v, t, theta): | ||
return self.table(u, v) | ||
|
||
def wavefront_gradient(self, u, v, t=None, theta=None): | ||
def wavefront_gradient(self, u, v, t=None, w=None, theta=None): | ||
""" Evaluate gradient of wavefront from lookup table. | ||
|
||
Parameters: | ||
|
@@ -1222,12 +1239,13 @@ def wavefront_gradient(self, u, v, t=None, theta=None): | |
v: Vertical pupil coordinate (in meters) at which to evaluate wavefront. Can | ||
be a scalar or an iterable. The shapes of u and v must match. | ||
t: Ignored for `UserScreen`. | ||
w: Ignored for `UserScreen`. | ||
theta: Ignored for `UserScreen`. | ||
|
||
Returns: | ||
Arrays dWdu and dWdv of wavefront lag or lead gradient in nm/m. | ||
""" | ||
return self.table.gradient(u, v) | ||
|
||
def _wavefront_gradient(self, u, v, t, theta): | ||
def _wavefront_gradient(self, u, v, t, w, theta): | ||
return self.table.gradient(u, v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need a test that uses this feature. (Which is the feature request that triggered this PR...) Maybe one that compares this implementation to one using ChromaticAtmosphere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I had naïvely thought that adding a
WavelengthSampler
photon op todrawImage
would trigger this, but I guess those ops only get executed after the base object is drawn. Looking into how to actually trigger this now...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little bit of a design conundrum even:
PhaseScreenPSF
is currently aGSObject
(and not aChromaticObject
). That's perfectly fine for the original usage, and would continue to be fine ifseeing_exp = 0
. But the profile is chromatic, of course, ifseeing_exp != 0
. Duck-typing might work here, but may require changing some of the code around https://github.com/GalSim-developers/GalSim/blob/main/galsim/gsobject.py#L419. For instance, the chromatic version of this profile is not separable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just be using ChromaticAtmosphere then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I figured out how to trigger this branch without a big rewrite. I had to use:
rather than
I think it's a bit of a problem that the latter doesn't work though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... Maybe? Let me try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that does appear to be close. The main difference is that ChromaticAtmosphere applies its shift to the entire base object at once. So we couldn't specify an exponent per screen. This is probably good enough for the atmospheric screens, but won't be correct for any OpticalScreens and I don't think will be correct for the second_kick component of the atmosphere (which has a limiting case of an Airy, e.g.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do something a little custom in ChromaticAtmosphere to make it work right when the base is a PhaseScreenPSF?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. That or tweak https://github.com/LSSTDESC/imSim/blob/main/imsim/atmPSF.py#L83 to build 3 separate things (atm+opt+2kick) instead of embedding them all inside a single PhaseScreenPSF. Then we can use ChromaticAtmosphere to only add chromatic seeing to the right part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latter is probably simpler...