DFO-LS 1.2.1__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of DFO-LS might be problematic. Click here for more details.

dfols/trust_region.py CHANGED
@@ -11,6 +11,15 @@ produces a new vector d which (approximately) solves the trust region subproblem
11
11
  The other outputs: gnew is the gradient of the model at d, and crvmin has
12
12
  information about the curvature of the model at the solution.
13
13
 
14
+ For handling arbitrary constraints, the call is
15
+ d, gnew, crvmin = ctrsbox(xopt, g, H, projections, delta)
16
+ which produces a new vector d approximately solving the constrained trust region subproblem:
17
+ min_{d} g'*d + 0.5*d'*H*d
18
+ s.t. ||d|| <= delta
19
+ xopt + d is feasible w.r.t. the constraint set C
20
+ The other outputs: gnew is the gradient of the model at d, and crvmin has
21
+ information about the curvature of the model at the solution.
22
+
14
23
  We also provide a function for maximising the absolute value of a linear function
15
24
  inside a similar trust region - this is useful for geometry steps.
16
25
  The call
@@ -20,9 +29,16 @@ solves
20
29
  s.t. lower <= x <= upper
21
30
  ||x-xbase|| <= Delta
22
31
  With this value, the variable d=x-xbase solves the problem
23
- min_s abs(c + g' * d)
32
+ min_d abs(c + g' * d)
24
33
  s.t. lower <= xbase + d <= upper
25
34
  ||d|| <= delta
35
+ Again, we have a version of this for handling arbitrary constraints
36
+ The call
37
+ x = ctrsbox_geometry(xbase, c, g, projections, Delta)
38
+ Solves
39
+ min_d abs(c + g' * d)
40
+ s.t. xbase + d is feasible w.r.t. the constraint set C
41
+ ||d|| <= delta
26
42
 
27
43
  Notes
28
44
  ----
@@ -54,7 +70,7 @@ alternative licensing.
54
70
  # Ensure compatibility with Python 2
55
71
  from __future__ import absolute_import, division, print_function, unicode_literals
56
72
 
57
- from math import sqrt
73
+ from math import sqrt, ceil
58
74
  import numpy as np
59
75
  try:
60
76
  import trustregion
@@ -63,14 +79,157 @@ except ImportError:
63
79
  # Fall back to Python implementation
64
80
  USE_FORTRAN = False
65
81
 
82
+ from .util import dykstra, pball, pbox, sumsq, model_value, remove_scaling
66
83
 
67
- from .util import sumsq
84
+ __all__ = ['ctrsbox_sfista', 'ctrsbox_pgd', 'ctrsbox_geometry', 'trsbox', 'trsbox_geometry']
68
85
 
86
+ ZERO_THRESH = 1e-14
69
87
 
70
- __all__ = ['trsbox', 'trsbox_geometry']
88
+ def ctrsbox_sfista(xopt, g, H, projections, delta, h, L_h, prox_uh, argsh=(), argsprox=(), func_tol=1e-3, max_iters=500, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN, scaling_changes=None, sfista_iters_scale=1.0):
89
+ n = xopt.size
90
+ assert xopt.shape == (n,), "xopt has wrong shape (should be vector)"
91
+ assert g.shape == (n,), "g and xopt have incompatible sizes"
92
+ assert len(H.shape) == 2, "H must be a matrix"
93
+ assert H.shape == (n,n), "H and xopt have incompatible sizes"
94
+ assert np.allclose(H, H.T), "H must be symmetric"
95
+ assert delta > 0.0, "delta must be strictly positive"
71
96
 
72
- ZERO_THRESH = 1e-14
97
+ # Initialization
98
+ d = np.zeros(n) # start with zero vector
99
+ y = np.zeros(n)
100
+ t = 1
101
+ k_H = np.linalg.norm(H, 2)
102
+ crvmin = -1.0
103
+
104
+ # Number of iterations & smoothing parameter, from Theorem 10.57 in
105
+ # [A. Beck. First-order methods in optimization, SIAM, 2017]
106
+ # We do not use the values of k and mu given in the theorem statement, but rather the intermediate
107
+ # results on p313 (K1 for number of iterations, and the immediate next line for mu)
108
+ # Note: in the book's notation, Gamma=delta^2, alpha=1, beta=L_h^2/2, Lf=k_H [alpha and beta from Thm 10.51]
109
+ try:
110
+ MAX_LOOP_ITERS = ceil(sfista_iters_scale * delta * (L_h+sqrt(L_h*L_h+2*k_H*func_tol)) / func_tol)
111
+ MAX_LOOP_ITERS = min(MAX_LOOP_ITERS, max_iters)
112
+ except ValueError:
113
+ MAX_LOOP_ITERS = max_iters
114
+ u = 2 * delta / (MAX_LOOP_ITERS * L_h) # smoothing parameter
115
+ # u = 2 * func_tol / (L_h ** 2 + L_h * sqrt(L_h ** 2 + 2 * k_H * func_tol)) # the above choice works better in practice
116
+
117
+ def gradient_Fu(xopt, g, H, u, prox_uh, d):
118
+ # Calculate gradient_Fu,
119
+ # where Fu(d) := g(d) + h_u(d) and h_u(d) is a 1/u-smooth approximation of h.
120
+ # We assume that h is globally Lipschitz continous with constant L_h,
121
+ # then we can let h_u(d) be the Moreau Envelope M_h_u(d) of h.
122
+ return g + H @ d + (xopt + d - prox_uh(remove_scaling(xopt + d, scaling_changes), u, *argsprox)) / u
123
+
124
+ # Lipschitz constant of gradient_Fu
125
+ l = k_H + 1 / u
126
+
127
+ # trust region is a ball of radius delta around xopt
128
+ trproj = lambda w: pball(w, xopt, delta)
129
+
130
+ # combine trust region constraints with user-entered constraints
131
+ P = list(projections) # make a copy of the projections list
132
+ P.append(trproj)
133
+ def proj(d0):
134
+ p = dykstra(P, xopt+d0, max_iter=d_max_iters, tol=d_tol)
135
+ # we want the step only, so we subtract xopt
136
+ # from the new point: proj(xk+d) - xk
137
+ return p - xopt
138
+
139
+ # general step
140
+ model_value_best = model_value(g, H, d, xopt, h, argsh, scaling_changes)
141
+ d_best = d.copy()
142
+ for k in range(MAX_LOOP_ITERS):
143
+ prev_d = d.copy()
144
+ prev_t = t
145
+ # gradient_Fu at y
146
+ g_Fu = gradient_Fu(xopt, g, H, u, prox_uh, d, *argsprox)
147
+
148
+ # main update step
149
+ d = proj(y - g_Fu / l)
150
+ new_model_value = model_value(g, H, d, xopt, h, argsh, scaling_changes)
151
+ if new_model_value < model_value_best:
152
+ d_best = d.copy()
153
+ model_value_best = new_model_value
154
+
155
+ # update true gradient
156
+ # gnew is the gradient of the smoothed function
157
+ gnew = gradient_Fu(xopt, g, H, u, prox_uh, d, *argsprox)
158
+
159
+ # update CRVMIN
160
+ crv = d.dot(H).dot(d)/sumsq(d) if sumsq(d) >= ZERO_THRESH else crvmin
161
+ crvmin = min(crvmin, crv) if crvmin != -1.0 else crv
162
+
163
+ # momentum update
164
+ t = (1 + sqrt(1 + 4*t*t)) / 2
165
+ y = d + (prev_t - 1) * (d - prev_d) / t
166
+ return d, gnew, crvmin
167
+
168
+ def ctrsbox_pgd(xopt, g, H, projections, delta, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN):
169
+ n = xopt.size
170
+ assert xopt.shape == (n,), "xopt has wrong shape (should be vector)"
171
+ assert g.shape == (n,), "g and xopt have incompatible sizes"
172
+ assert len(H.shape) == 2, "H must be a matrix"
173
+ assert H.shape == (n,n), "H and xopt have incompatible sizes"
174
+ assert np.allclose(H, H.T), "H must be symmetric"
175
+ assert delta > 0.0, "delta must be strictly positive"
176
+
177
+ d = np.zeros((n,))
178
+ gnew = g.copy()
179
+ gy = g.copy()
180
+ crvmin = -1.0
181
+ y = d.copy()
182
+ eta = 1.2 # L backtrack scaling factor
183
+ t = 1
184
+
185
+ # Initial guess of L is norm(Hessian)
186
+ L = np.linalg.norm(H, 2)
187
+
188
+ # trust region is a ball of radius delta around xopt
189
+ trproj = lambda w: pball(w, xopt, delta)
190
+
191
+ # combine trust region constraints with user-entered constraints
192
+ P = list(projections) # make a copy of the projections list
193
+ P.append(trproj)
194
+ def proj(d0):
195
+ p = dykstra(P, xopt+d0, max_iter=d_max_iters, tol=d_tol)
196
+ # we want the step only, so we subtract xopt
197
+ # from the new point: proj(xk+d) - xk
198
+ return p - xopt
199
+
200
+ MAX_LOOP_ITERS = 100 * n ** 2
201
+
202
+ # projected GD loop
203
+ for ii in range(MAX_LOOP_ITERS):
204
+ w = y - (1/L)*gy
205
+ prev_d = d.copy()
206
+ d = proj(w)
207
+
208
+ # size of step taken
209
+ s = d - prev_d
210
+ stplen = np.linalg.norm(s)
73
211
 
212
+ # update true gradient
213
+ gnew += H.dot(s)
214
+
215
+ # update CRVMIN
216
+ crv = s.dot(H).dot(s)/sumsq(s) if sumsq(s) >= ZERO_THRESH else crvmin
217
+ crvmin = min(crvmin, crv) if crvmin != -1.0 else crv
218
+
219
+ # exit condition
220
+ if stplen <= ZERO_THRESH:
221
+ break
222
+
223
+ # momentum update
224
+ prev_t = t
225
+ t = (1 + np.sqrt(1 + 4 * t ** 2))/2
226
+ prev_y = y.copy()
227
+ y = d + s*(prev_t - 1)/t
228
+
229
+ # update gradient w.r.t y
230
+ gy += H.dot(y - prev_y)
231
+
232
+ return d, gnew, crvmin
74
233
 
75
234
  def trsbox(xopt, g, H, sl, su, delta, use_fortran=USE_FORTRAN):
76
235
  if use_fortran:
@@ -103,7 +262,7 @@ def trsbox(xopt, g, H, sl, su, delta, use_fortran=USE_FORTRAN):
103
262
  iterc = 0
104
263
  nact = 0 # number of fixed variables
105
264
 
106
- xbdi = np.zeros((n,), dtype=np.int) # fix x_i at bounds? [values -1, 0, 1]
265
+ xbdi = np.zeros((n,), dtype=int) # fix x_i at bounds? [values -1, 0, 1]
107
266
  xbdi[(xopt <= sl) & (g >= 0.0)] = -1
108
267
  xbdi[(xopt >= su) & (g <= 0.0)] = 1
109
268
 
@@ -405,8 +564,63 @@ def ball_step(x0, g, Delta):
405
564
  if sqrt(gsqnorm) < ZERO_THRESH: # Error catching: if g=0, make no step
406
565
  return 0.0
407
566
  else:
408
- return (sqrt(gdotx0**2 + gsqnorm*(Delta**2 - x0sqnorm)) - gdotx0) / gsqnorm
567
+ # Sqrt had negative input on prob 46 in OG DFOLS with noise
568
+ # print("Inside of the sqrt:", gdotx0**2 + gsqnorm*(Delta**2 - x0sqnorm))
569
+ # Got Inside of the sqrt: -3.608971127647144e-42
570
+ # Added max(0,...) here
571
+ return (sqrt(np.maximum(0,gdotx0**2 + gsqnorm*(Delta**2 - x0sqnorm))) - gdotx0) / gsqnorm
572
+
573
+ def ctrsbox_linear(xbase, g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN):
574
+ # Solve the convex program:
575
+ # min_d g' * d
576
+ # s.t. xbase + d is feasible w.r.t. constraint set C
577
+ # ||d||^2 <= Delta^2
578
+
579
+ n = g.size
580
+ d = np.zeros((n,))
581
+ y = d.copy()
582
+ t = 1
583
+ dirn = -g
584
+ cons_dirns = []
585
+
586
+ # If g[i] = 0, never step along this direction
587
+ constant_directions = np.where(np.abs(dirn) < ZERO_THRESH)[0]
588
+ dirn[constant_directions] = 0.0
589
+
590
+ # trust region is a ball of radius delta centered around xbase
591
+ trproj = lambda w: pball(w, xbase, Delta)
409
592
 
593
+ # combine trust region constraints with user-entered constraints
594
+ P = list(projections) # make a copy of the projections list
595
+ P.append(trproj)
596
+ def proj(d0):
597
+ p = dykstra(P, xbase + d0, max_iter=d_max_iters, tol=d_tol)
598
+ # we want the step only, so we subtract
599
+ # xbase from the new point: proj(xk + d) - xk
600
+ return p - xbase
601
+
602
+ MAX_LOOP_ITERS = 100 * n ** 2
603
+
604
+ # projected GD loop
605
+ for ii in range(MAX_LOOP_ITERS):
606
+ w = y + dirn
607
+ prev_d = d.copy()
608
+ d = proj(w)
609
+
610
+ s = d - prev_d
611
+ stplen = np.linalg.norm(s)
612
+
613
+ # exit condition
614
+ if stplen <= ZERO_THRESH:
615
+ break
616
+
617
+ # 'momentum' update
618
+ prev_t = t
619
+ t = (1 + np.sqrt(1 + 4 * t ** 2))/2
620
+ prev_y = y.copy()
621
+ y = d + s*(prev_t - 1)/t
622
+
623
+ return d
410
624
 
411
625
  def trsbox_linear(g, a_in, b_in, Delta, use_fortran=USE_FORTRAN):
412
626
  # Solve the convex program:
@@ -466,6 +680,22 @@ def trsbox_linear(g, a_in, b_in, Delta, use_fortran=USE_FORTRAN):
466
680
  dirn[idx_hit] = 0.0 # no more searching this direction
467
681
  return x
468
682
 
683
+ def ctrsbox_geometry(xbase, c, g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN):
684
+ # Given a Lagrange polynomial defined by: L(x) = c + g' * (x - xbase)
685
+ # Maximise |L(x)| in a box + trust region - that is, solve:
686
+ # max_x abs(c + g' * (x - xbase))
687
+ # s.t. x is feasible w.r.t constraint set C
688
+ # ||x-xbase|| <= Delta
689
+ # Setting s = x-xbase (or x = xbase + s), this is equivalent to:
690
+ # max_s abs(c + g' * s)
691
+ # s.t. xbase + s is is feasible w.r.t constraint set C
692
+ # ||s|| <= Delta
693
+ smin = ctrsbox_linear(xbase, g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=use_fortran) # minimise g' * s
694
+ smax = ctrsbox_linear(xbase, -g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=use_fortran) # maximise g' * s
695
+ if abs(c + np.dot(g, smin)) >= abs(c + np.dot(g, smax)): # choose the one with largest absolute value
696
+ return smin
697
+ else:
698
+ return smax
469
699
 
470
700
  def trsbox_geometry(xbase, c, g, lower, upper, Delta, use_fortran=USE_FORTRAN):
471
701
  # Given a Lagrange polynomial defined by: L(x) = c + g' * (x - xbase)
dfols/util.py CHANGED
@@ -27,11 +27,14 @@ from __future__ import absolute_import, division, print_function, unicode_litera
27
27
 
28
28
  import logging
29
29
  import numpy as np
30
+ import scipy.linalg as LA
30
31
  import sys
31
32
 
32
33
 
33
- __all__ = ['sumsq', 'eval_least_squares_objective', 'model_value', 'random_orthog_directions_within_bounds',
34
- 'random_directions_within_bounds', 'apply_scaling', 'remove_scaling']
34
+ __all__ = ['sumsq', 'eval_least_squares_with_regularisation', 'model_value', 'random_orthog_directions_within_bounds',
35
+ 'random_directions_within_bounds', 'apply_scaling', 'remove_scaling', 'pbox', 'pball', 'dykstra', 'qr_rank']
36
+
37
+ module_logger = logging.getLogger(__name__)
35
38
 
36
39
 
37
40
  def sumsq(x):
@@ -44,9 +47,9 @@ def sumsq(x):
44
47
  return np.dot(x, x)
45
48
 
46
49
 
47
- def eval_least_squares_objective(objfun, x, args=(), verbose=True, eval_num=0, pt_num=0, full_x_thresh=6, check_for_overflow=True):
50
+ def eval_least_squares_with_regularisation(objfun, x, h=None, argsf=(), argsh=(), verbose=True, eval_num=0, pt_num=0, full_x_thresh=6, check_for_overflow=True):
48
51
  # Evaluate least squares function
49
- fvec = objfun(x, *args)
52
+ fvec = objfun(x, *argsf)
50
53
 
51
54
  if check_for_overflow:
52
55
  try:
@@ -59,20 +62,31 @@ def eval_least_squares_objective(objfun, x, args=(), verbose=True, eval_num=0, p
59
62
  else:
60
63
  f = sumsq(fvec)
61
64
 
65
+ # objective = least-squares + regularisation
66
+ obj = f
67
+ if h is not None:
68
+ # Evaluate regularisation term
69
+ hvalue = h(x, *argsh)
70
+ obj = f + hvalue
71
+
62
72
  if verbose:
63
73
  if len(x) < full_x_thresh:
64
- logging.info("Function eval %i at point %i has f = %.15g at x = " % (eval_num, pt_num, f) + str(x))
74
+ module_logger.info("Function eval %i at point %i has obj = %.15g at x = " % (eval_num, pt_num, obj) + str(x))
65
75
  else:
66
- logging.info("Function eval %i at point %i has f = %.15g at x = [...]" % (eval_num, pt_num, f))
76
+ module_logger.info("Function eval %i at point %i has obj = %.15g at x = [...]" % (eval_num, pt_num, obj))
67
77
 
68
- return fvec, f
78
+ return fvec, obj
69
79
 
70
80
 
71
- def model_value(g, H, s):
72
- # Calculate model value (s^T * g + 0.5* s^T * H * s) = s^T * (gopt + 0.5 * H*s)
81
+ def model_value(g, H, s, xopt=(), h=None,argsh=(), scaling_changes=None):
82
+ # Calculate model value (s^T * g + 0.5* s^T * H * s) + h(xopt + s) = s^T * (gopt + 0.5 * H*s) + h(xopt + s)
73
83
  assert g.shape == s.shape, "g and s have incompatible sizes"
74
84
  Hs = H.dot(s)
75
- return np.dot(s, g + 0.5*Hs)
85
+ rtn = np.dot(s, g + 0.5*Hs)
86
+ if h is not None:
87
+ hvalue = h(remove_scaling(xopt+s, scaling_changes), *argsh)
88
+ rtn += hvalue
89
+ return rtn
76
90
 
77
91
 
78
92
  def get_scale(dirn, delta, lower, upper):
@@ -207,3 +221,50 @@ def remove_scaling(x_scaled, scaling_changes):
207
221
  shift, scale = scaling_changes
208
222
  return shift + x_scaled * scale
209
223
 
224
+
225
+ def dykstra(P,x0,max_iter=100,tol=1e-10):
226
+ x = x0.copy()
227
+ p = len(P)
228
+ y = np.zeros((p,x0.shape[0]))
229
+
230
+ n = 0
231
+ cI = float('inf')
232
+ while n < max_iter and cI >= tol:
233
+ cI = 0
234
+ for i in range(0,p):
235
+ # Update iterate
236
+ prev_x = x.copy()
237
+ x = P[i](prev_x - y[i,:])
238
+
239
+ # Update increment
240
+ prev_y = y[i,:].copy()
241
+ y[i,:] = x - (prev_x - prev_y)
242
+
243
+ # Stop condition
244
+ cI += np.linalg.norm(prev_y - y[i,:])**2
245
+
246
+ n += 1
247
+
248
+ return x
249
+
250
+
251
+ def pball(x,c,r):
252
+ return c + (r/np.max([np.linalg.norm(x-c),r]))*(x-c)
253
+
254
+
255
+ def pbox(x,l,u):
256
+ return np.minimum(np.maximum(x,l), u)
257
+
258
+ '''
259
+ Calculates rank of square matrix with QR.
260
+ We use the fact that the rank of a square matrix A
261
+ can be given by the number of nonzero diagonal elements of
262
+ R in the QR factorization of A.
263
+ '''
264
+ def qr_rank(A,tol=1e-15):
265
+ m,n = A.shape
266
+ assert m == n, "Input matrix must be square"
267
+ Q,R = LA.qr(A)
268
+ D = np.abs(np.diag(R))
269
+ rank = np.sum(D > tol)
270
+ return rank, D
@@ -1,16 +0,0 @@
1
- dfols/__init__.py,sha256=WdWULFDmitwdbZ3JEcrbkb5DjXr0ZM6efAqSe-iPsr8,1620
2
- dfols/controller.py,sha256=7puR1_zuu47Vr-xEhqubEJRzLv2Yja3X2O_WeDaDSV0,35652
3
- dfols/diagnostic_info.py,sha256=2kEUkL-MS4eDENUf1r2hOWsntP8OxMDKi_kyHmrC9V4,6081
4
- dfols/hessian.py,sha256=DKOm520rrFFaXVXu-BsD7aj2_ilJohxZ2tnMsnSa8zE,4265
5
- dfols/model.py,sha256=OHi-Y50OajpIauzfGcYPdCSrM8gvXYTGjb3NY7enk9s,18020
6
- dfols/params.py,sha256=1Mplj96q9anYa6d3PCkXgsxp-tc9fP4mNtQxwm7bveE,16749
7
- dfols/solver.py,sha256=RPUQpgPRkcUVMvc6tJ-0bYckg8EesMMeQ1meRJk3u_k,59180
8
- dfols/trust_region.py,sha256=75Z0z2d6ENh8t9t_e95IVwgNaWlZpyZF4rdkMJ54qhc,19152
9
- dfols/util.py,sha256=gpDa71PSFhFMqIJdmndnJ0vE25BiSulscRPS6MlIyaE,8667
10
- dfols/version.py,sha256=GQUgqQ6kgZyC6ohszFAofexRKp-dKwWY81bo7q2hlI8,926
11
- DFO_LS-1.2.1.dist-info/LICENSE.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
12
- DFO_LS-1.2.1.dist-info/METADATA,sha256=nyESs24W-UXP0yYFZ8XX7YGOv39pIPKc8UAvRHrLfEc,7039
13
- DFO_LS-1.2.1.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
14
- DFO_LS-1.2.1.dist-info/top_level.txt,sha256=UfxRhaDN8HQx2_l17KbrDrERJ90OCN7VKkDMpYYbRLU,6
15
- DFO_LS-1.2.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
16
- DFO_LS-1.2.1.dist-info/RECORD,,
@@ -1 +0,0 @@
1
-
dfols/version.py DELETED
@@ -1,25 +0,0 @@
1
- """
2
- Version number
3
- ====
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU General Public License for more details.
14
-
15
- You should have received a copy of the GNU General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
-
18
- The development of this software was sponsored by NAG Ltd. (http://www.nag.co.uk)
19
- and the EPSRC Centre For Doctoral Training in Industrially Focused Mathematical
20
- Modelling (EP/L015803/1) at the University of Oxford. Please contact NAG for
21
- alternative licensing.
22
-
23
- """
24
-
25
- __version__ = '1.2.1'