source: sasview/park-1.2.1/park/rangemap.py @ cfe1feb

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since cfe1feb was 3570545, checked in by Mathieu Doucet <doucetm@…>, 13 years ago

Adding park Part 2

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