Package park :: Package fitting :: Module rangemap

Source Code for Module park.fitting.rangemap

  1  """ 
  2  Defines transformations between the a fit space and the parameter space. 
  3   
  4  The parameter space maps a set of possibly bounded dimensions into a 
  5  real-valued fitness:: 
  6   
  7      f: [a,b]**n |-> R 
  8   
  9  The fit space is constrained to map values in the unit box:: 
 10   
 11      f': [0,1]**n |-> R 
 12   
 13  Here we can define f' using the zero-one mapper:: 
 14   
 15      f' = ZeroOneMapper(f,a,b) 
 16      f'(v) = f(x) 
 17   
 18  An asymptote function maps x to [0,1], preserving at least 12 digits of 
 19  precision. 
 20   
 21  Using this mapping, the optimizer can operate in a well known space 
 22  independent of parameter precision.  This allows the use of reasonable 
 23  constants for items such as a step size and initial value. 
 24   
 25  Note: we do not yet support fitness functions with analytic derivatives. 
 26  Given df/dp and mapping function f'(v) = f(M(v)) then df'/dv = df/dM dM/dv. 
 27  So the derivatives need to be multiplied by the derivative of the 
 28  parameter mapper.  Not difficult computationally, but the definition of 
 29  the fitness function does not currently support this organization. 
 30  """ 
 31  import numpy 
 32   
33 -class ArctanAsymptote(object):
34 """ 35 Arctan asymptote function for mapping (-inf,inf) to [0,1]. 36 37 With all attributes constant, either the class or an instance can be used. 38 39 An asymptote function is monotonically increasing and finite everywhere. 40 Assuming the function is a at -inf and b at +inf, scale is 1/(b-a) and 41 offset is a. Together with an inverse function, this allows us to 42 transform between an infinite range and the range [0,1]. Semidefinite 43 map to (-inf,0] and [0,inf). 44 45 ArctanAsymptote is approximately linear in [-0.5,0.5], and is useful 46 out to the range [-1e15,1e15], though with much reduced precision 47 for larger values. 48 """ 49 scale = 1/numpy.pi 50 offset = -numpy.pi/2 51 forward = numpy.arctan 52 inverse = numpy.tan
53
54 -def fp_forward(x):
55 """ 56 Linearize floating point values using an exponential scale. 57 58 Convert sign*m*2^e to sign*(e+1023+m), yielding a value in [-2047,2047] 59 """ 60 (m,e) = numpy.frexp(x) 61 s = numpy.sign(x) 62 v = (e+1023+m*s)*s 63 return v
64 -def fp_inverse(v):
65 """ 66 Restore floating point value from linear form on an exponential scale. 67 68 Convert sign*(e+1023+m) to sign*m*2^e, yielding a value in [-1e308,1e308] 69 """ 70 s = sign(v) 71 v *= s 72 m = floor(v) 73 e = v-m 74 x = ldexp(s*m,e) 75 return x
76 fp_min, fp_max = -(1024+1023), (1024+1023)
77 -class Asymptote:
78 """ 79 Logarithmic asymptote function. 80 81 This is an asymptote function which follows the floating point 82 representation, using the following mapping:: 83 84 [-1e308,-1e-308] -> [0,0.5) 85 0 -> 0.5 86 [1e-308,1e308] -> (0.5+eps,1] 87 88 With 11 bits of the 53 bits of available precision for the exponent, 89 this leaves a tolerance of 1e-12 on the fitting parameters, which is 90 easily good enough for any real unbounded fitting problem. The user 91 can increased the precision on the bounded parameters by using tighter 92 bounds. 93 """ 94 scale = 1/(fp_max-fp_min) 95 offset = fp_min 96 forward = fp_forward 97 inverse = fp_inverse
98
99 -class ZeroOneMapper(object):
100 """ 101 Map function range into [0,1]**n. 102 103 Given f: [a,b] -> R 104 Defines f': [0,1] -> R 105 106 The method encode(x) returns a value in [0,1] 107 The method decode(v) returns a value in [a,b] 108 The method __call__(v) returns f(decode(v)) 109 110 For indefinite and semidefinte ranges, an asymptote transformation 111 is needed. Normally this is `park.rangemap.Asymptote` which supports 112 the full floating point range of values, but is limited to about 113 12 digits of precision. The arctan function can also be used 114 (see `park.rangemap.ArctanAsymptote`). 115 116 Range is determined by the bounds low and high. Each dimension may be 117 unbounded, semi-definite or bounded. Bounded functions use a linear 118 mapping between low and high. Unbounded functions use an asymptote 119 function, which should be -1 at -inf, 0 at 0 and 1 at +inf, and 120 approximate the identity function between [-0.5,0.5]. 121 122 This can be used to turn any unconstrained optimizer into a [0,1] 123 bounded optimizer by sending infeasible points to infinity. 124 125 Note: Newton-style optimizers will not work in this regime, but 126 instead require a hint about the direction of the unconstrained 127 region. 128 """
129 - def __init__(self, base_function, low, high, asymptote=Asymptote, 130 range_check=False):
131 """ 132 Function range mapper. See `park.rangemap.ZeroOneMapper` for details. 133 134 base_function is the function being wrapped. 135 136 low[k],high[k] is the range of values for each fitting parameter k. 137 138 range_check is True if calls to the mapper may include out of range 139 values. In this case, f(v) returns inf for out of range values and 140 encode/decode raise exceptions. 141 142 asymptote is the asymptote function to use when linearizing 143 indefinite ranges. 144 """ 145 146 unbounded_low = numpy.isinf(low) 147 unbounded_high = numpy.isinf(high) 148 unbounded = unbounded_low|unbounded_high 149 150 # Determine linear scale and offset for v = (x-offset)/scale 151 # For semi-infinite ranges, use the known bound as the offset. This 152 # transforms [a,inf) to [0.5,1] and (-inf,b] to [0,0.5]. 153 # Infinite ranges should use offset 0. 154 scale = (high-low) 155 offset = -low 156 scale[unbounded_low | unbounded_high] = 0. 157 offset[unbounded_low] = high[unbounded_low] 158 offset[unbounded_high] = low[unbounded_high] 159 offset[unbounded_low & unbounded_high] = 0. # low&high must be last 160 161 # Determine transformed scale and offset 162 # If the value is unbounded, then use asymptote directly. 163 # If bounded below, then transform [0.5,1] to [0,1] using (v-0.5)/0.5 164 # If bounded above, then transform [0,0.5] to [0,1] using v/0.5 165 a_scale = numpy.ones(scale.shape)*asymptote.scale 166 a_scale[unbounded_low ^ unbounded_high] *= 0.5 167 a_offset = numpy.zeros(offset.shape)+asymptote.offset 168 a_offset[unbounded_high & ~unbounded_low] += 0.5*asymptote.scale 169 a_forward = asymptote.forward 170 a_inverse = asymptote.inverse 171 172 # Preselect the relevant elements 173 a_scale = a_scale[unbounded] 174 a_offset = a_offset[unbounded] 175 176 # Remember the transformation parameters 177 self.low,self.high = low,high 178 self.base_function = base_function 179 self.scale,self.offset = scale,offset 180 self.a_scale,self.a_offset = a_scale,a_offset 181 self.a_forward,self.a_inverse = a_forward,a_inverse 182 183 # To minimize function call overhead, use only the linear bounds 184 # tranform if the problem is completely bounded. 185 if numpy.any(unbounded): 186 self.__call__ = self._indefinite 187 else: 188 self.__call__ = self._definite
189
190 - def _indefinite(self, v):
191 """Use this function if some bounds are indefinite""" 192 x = v.copy() 193 x[unbounded] = self.a_inverse(x[unbounded])*self.a_scale + self.a_offset 194 x = x*self.scale+self.offset 195 return self.base_function(x)
196 - def _definite(self, v):
197 """Use this function if all bounds are definite""" 198 return self.base_function(v*self.scale+self.offset)
199 - def _indefinite_checked(self, v):
200 """Use this function if some bounds are indefinite and ranges need 201 to be checked""" 202 if numpy.any(v<0|v>1): return numpy.inf 203 x = v.copy() 204 x[unbounded] = self.a_inverse(x[unbounded])*self.a_scale + self.a_offset 205 x = x*self.scale+self.offset 206 return self.base_function(x)
207 - def _definite_checked(self, v):
208 """Use this function if all bounds are definite, but ranges need 209 to be checked""" 210 if numpy.any(v<0|v>1): return numpy.inf 211 return self.base_function(v*self.scale+self.offset)
212
213 - def decode(v):
214 """Transform from range [0,1] to [a,b].""" 215 if numpy.any(v<0|v>1): 216 raise RuntimeError("value out of range [0,1]") 217 x = v.copy() 218 x[unbounded] = self.a_forward(x[unbounded])*a_scale + a_offset 219 x = x*scale+offset 220 return x
221 - def encode(x):
222 """Transform from range [a,b] to [0,1].""" 223 if numpy.any(x<self.low|x>self.high): 224 raise RuntimeError("value out of range [a,b]") 225 v = (x-offset)/scale 226 v[unbounded] = (self.a_inverse(v[unbounded])-a_offset)/a_scale 227 return v
228 229 # Functional implementation: return transformation functions. 230 # This is faster but less pythonic
231 -def zero_one_mapper(base_function, low, high, asymptote=ArctanAsymptote):
232 """ 233 Map function range into [0,1]**n. 234 235 Returns a pair of functions f and inv. The function f takes an 236 encoded value in 237 encode, which takes x and returns a value 238 in [0,1] and decode, which takes a value in [0,1] and returns x. 239 240 Range is determined by the bounds low and high. Each dimension may be 241 unbounded, semi-definite or bounded. Bounded functions use a linear 242 mapping between low and high. Unbounded functions use an asymptote 243 function, which should be -1 at -inf, 0 at 0 and 1 at +inf, and 244 approximate the identity function between [-0.5,0.5]. 245 246 This can be used to turn any unconstrained optimizer into a [0,1] 247 bounded optimizer by sending infeasible points to infinity. 248 249 Note: Newton-style optimizers will not work in this regime, but 250 instead require a hint about the direction of the unconstrained 251 region. 252 """ 253 254 unbounded_low = numpy.isinf(low) 255 unbounded_high = numpy.isinf(high) 256 unbounded = unbounded_low|unbounded_high 257 258 # Determine linear scale and offset for v = (x-offset)/scale 259 # For semi-infinite ranges, use the known bound as the offset. This 260 # transforms [a,inf) to [0.5,1] and (-inf,b] to [0,0.5]. 261 # Infinite ranges should use offset 0. 262 scale = (high-low) 263 offset = -low 264 scale[unbounded_low | unbounded_high] = 0. 265 offset[unbounded_low] = high[unbounded_low] 266 offset[unbounded_high] = low[unbounded_high] 267 offset[unbounded_low & unbounded_high] = 0. # low&high must be last 268 269 # Determine transformed scale and offset 270 # If the value is unbounded, then use asymptote directly. 271 # If bounded below, then transform [0.5,1] to [0,1] using (v-0.5)/0.5 272 # If bounded above, then transform [0,0.5] to [0,1] using v/0.5 273 a_scale = numpy.ones(scale.shape)*asymptote.scale 274 a_scale[unbounded_low ^ unbounded_high] *= 0.5 275 a_offset = numpy.zeros(offset.shape)+asymptote.offset 276 a_offset[unbounded_high & ~unbounded_low] += 0.5*asymptote.scale 277 278 # Preselect the relevant elements 279 a_scale = a_scale[unbounded] 280 a_offset = a_offset[unbounded] 281 282 283 if numpy.any(unbounded): 284 def function(v): 285 x = v.copy() 286 x[unbounded] = asymptote.inverse(x[unbounded])*a_scale + a_offset 287 x = x*scale+offset 288 return base_function(x)
289 def decode(v): 290 x = v.copy() 291 x[unbounded] = asymptote.inverse(x[unbounded])*a_scale + a_offset 292 x = x*scale+offset 293 return x 294 def encode(x): 295 v = (x-offset)/scale 296 v[unbounded] = (asymptote.function(v[unbounded])-a_offset)/a_scale 297 return v 298 else: 299 def function(v): return base_function( (x-offset)/scale ) 300 def decode(v): return (v*scale+offset) 301 def function(x): return base_function( (x-offset)/scale ) 302 def decode(v): return (v*scale+offset) 303 304 return function, encode, decode 305 306
307 -def bounded(function, low, high):
308 """ 309 Evaluate a function with bounds checking. 310 311 This can be used to turn any unconstrained optimizer into a 312 constrained optimizer by sending infeasible points to infinity. 313 314 Note: Newton-style optimizers will not work in this regime, but 315 instead require a hint about the direction of the unconstrained 316 region. 317 318 Note: unused function which may be removed in future versions. 319 """ 320 def function_wrapper(x): 321 if numpy.any((x<low) | (x>high)): 322 return numpy.Inf 323 else: 324 return function(x)
325 function_wrapper.__doc__ = function.__doc__ 326 return function_wrapper 327