Coverage for starry/_core/ops/minimize.py : 95%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2import numpy as np
3from scipy.optimize import minimize
4import theano
5from theano import gof
6import theano.tensor as tt
7from theano.configparser import change_flags
9try:
10 import healpy as hp
11except ImportError:
12 hp = None
15__all__ = ["minimizeOp", "LDPhysicalOp"]
18class minimizeOp(tt.Op):
19 """Find the global minimum of the map intensity.
21 Returns the tuple `(lat, lon, I)`.
23 .. note::
24 This op is not very optimized. The idea here is to
25 do a coarse grid search, find the minimum, then run
26 a quick gradient descent to refine the result.
27 """
29 def __init__(self, intensity, P, ydeg, udeg, fdeg):
30 self.intensity = intensity
31 self.P = P
32 self.ydeg = ydeg
33 self.udeg = udeg
34 self.fdeg = fdeg
35 self._do_setup = True
36 self.oversample = 1
37 self.ntries = 1
38 self.result = None
40 def setup(self, oversample=1, ntries=1):
41 self.ntries = ntries
43 # Don't setup unless the user actually calls this function,
44 # since there's quite a bit of overhead
45 if self._do_setup or (oversample != self.oversample):
47 self.oversample = oversample
49 # Create the grid using healpy if available
50 # Require at least `oversample * l ** 2 points`
51 s = np.random.RandomState(0)
52 if hp is None:
53 npts = oversample * self.ydeg ** 2
54 self.lat_grid = (
55 np.arccos(2 * s.rand(npts) - 1) * 180.0 / np.pi - 90.0
56 )
57 self.lon_grid = (s.rand(npts) - 0.5) * 360
58 else:
59 nside = 1
60 while hp.nside2npix(nside) < oversample * self.ydeg ** 2:
61 nside += 1
62 theta, phi = hp.pix2ang(
63 nside=nside, ipix=range(hp.nside2npix(nside))
64 )
65 self.lat_grid = 0.5 * np.pi - theta
66 self.lon_grid = phi - np.pi
67 # Add a little noise for stability
68 self.lat_grid += 1e-4 * s.randn(len(self.lat_grid))
69 self.lon_grid += 1e-4 * s.randn(len(self.lon_grid))
70 self.P_grid = self.P(
71 tt.as_tensor_variable(self.lat_grid),
72 tt.as_tensor_variable(self.lon_grid),
73 ).eval()
75 # Set up the cost & grad function for the nonlinear solver
76 u0 = np.zeros(self.udeg + 1)
77 u0[0] = -1.0
78 u0 = tt.as_tensor_variable(u0)
79 f0 = np.zeros((self.fdeg + 1) ** 2)
80 f0[0] = np.pi
81 f0 = tt.as_tensor_variable(f0)
82 latlon = tt.dvector()
83 y = tt.dvector()
84 with change_flags(compute_test_value="off"):
85 self.I = theano.function(
86 [latlon, y],
87 [
88 self.intensity(
89 latlon[0], latlon[1], y, u0, f0, 0.0, 0.0
90 )[0],
91 *theano.grad(
92 self.intensity(
93 latlon[0], latlon[1], y, u0, f0, 0.0, 0.0
94 )[0],
95 [latlon],
96 ),
97 ],
98 )
99 self._do_setup = False
101 def make_node(self, *inputs):
102 inputs = [tt.as_tensor_variable(i) for i in inputs]
103 outputs = [
104 tt.TensorType(inputs[0].dtype, ())(),
105 tt.TensorType(inputs[0].dtype, ())(),
106 tt.TensorType(inputs[0].dtype, ())(),
107 ]
108 return gof.Apply(self, inputs, outputs)
110 def infer_shape(self, node, shapes):
111 return [(), (), ()]
113 def perform(self, node, inputs, outputs):
115 assert self._do_setup is False, "Must run `setup()` first."
117 y = inputs[0]
119 # Initial guess
120 I_grid = np.dot(self.P_grid, y)
121 outputs[2][0] = np.inf
123 for n in range(self.ntries):
125 ind = np.argmin(I_grid)
126 x0 = np.array([self.lat_grid[ind], self.lon_grid[ind]])
128 # Minimize
129 result = minimize(self.I, x0, args=(y,), jac=True)
131 if result.fun < outputs[2][0]:
132 outputs[0][0] = result.x[0]
133 outputs[1][0] = result.x[1]
134 outputs[2][0] = result.fun
136 # Prepare for next iteration
137 I_grid[ind] = np.inf
139 # Save
140 self.result = result
143class LDPhysicalOp(tt.Op):
144 """
145 Check whether a limb darkening profile is physical using Sturm's theorem.
147 """
149 def __init__(self, nroots):
150 self.nroots = nroots
152 def make_node(self, *inputs):
153 inputs = [tt.as_tensor_variable(i) for i in inputs]
154 outputs = [tt.bscalar()]
155 return gof.Apply(self, inputs, outputs)
157 def infer_shape(self, node, shapes):
158 return [()]
160 def perform(self, node, inputs, outputs):
161 u = inputs[0]
162 outputs[0][0] = 1
164 # Ensure the function is *decreasing* toward the limb
165 if u.sum() < -1:
166 outputs[0][0] = 0
167 return
169 # Sturm's theorem on the intensity to ensure positivity
170 p = u[::-1]
171 if self.nroots(p, 0, 1) > 0:
172 outputs[0][0] = 0
173 return
175 # Sturm's theorem on the derivative to ensure monotonicity
176 p = (u[1:] * np.arange(1, len(u)))[::-1]
177 if self.nroots(p, 0, 1) > 0:
178 outputs[0][0] = 0
179 return