def _compute_f_h(self, try_step_success): """Compute factor for h based on error norm.""" if try_step_success and np.all(np.isfinite(self.error)): scale = self.atol + np.maximum(np.abs(self.y), np.abs(self.y_new)) * self.rtol err_norm = norm(self.error / scale) # error norm error_accepted = err_norm < 1 else: err_norm = np.inf error_accepted = False # Find step size factor for next step if err_norm == 0.0: f_h = self.f_max else: f_h = np.max([self.f_min, np.min([self.f_max, self.f_safe * err_norm**self.err_ex])]) return f_h, error_accepted, err_norm
def h_start(df, a, b, y, yprime, morder, rtol, atol): """h_shart computes a starting step size to be used in solving initial value problems in ordinary differential equations. This method is developed by H.A. Watts and described in [1]_. This function is a Python translation of the Fortran source code [2]_. The two main modifications are: using the RMS norm from scipy.integrate allowing for complex valued input Parameters ---------- df : callable Right-hand side of the system. The calling signature is fun(t, y). Here t is a scalar. The ndarray y has has shape (n,) and fun must return array_like with the same shape (n,). a : float This is the initial point of integration. b : float This is a value of the independent variable used to define the direction of integration. A reasonable choice is to set `b` to the first point at which a solution is desired. You can also use `b , if necessary, to restrict the length of the first integration step because the algorithm will not compute a starting step length which is bigger than abs(b-a), unless `b` has been chosen too close to `a`. (it is presumed that h_start has been called with `b` different from `a` on the machine being used. y : array_like, shape (n,) This is the vector of initial values of the n solution components at the initial point `a`. yprime : array_like, shape (n,) This is the vector of derivatives of the n solution components at the initial point `a`. (defined by the differential equations in subroutine `df`) morder : int This is the order of the formula which will be used by the initial value method for taking the first integration step. rtol : float Relative tolereance used by the differential equation method. atol : float or array_like Absolute tolereance used by the differential equation method. Returns ------- float An appropriate starting step size to be attempted by the differential equation method. References ---------- .. [1] H.A. Watts, "Starting step size for an ODE solver", Journal of Computational and Applied Mathematics, Vol. 9, No. 2, 1983, pp. 177-191, ISSN 0377-0427. https://doi.org/10.1016/0377-0427(83)90040-7 .. [2] Slatec Fortran code dstrt.f. https://www.netlib.org/slatec/src/ """ # needed to pass scipy unit test: if y.size == 0: return np.inf # compensate for modified call list neq = y.size spy = np.empty_like(y) pv = np.empty_like(y) etol = atol + rtol * np.abs(y) # `small` is a small positive machine dependent constant which is used for # protecting against computations with numbers which are too small relative # to the precision of floating point arithmetic. `small` should be set to # (approximately) the smallest positive DOUBLE PRECISION number such that # (1. + small) > 1. on the machine being used. The quantity small**(3/8) # is used in computing increments of variables for approximating # derivatives by differences. Also the algorithm will not compute a # starting step length which is smaller than 100*small*ABS(A). # `big` is a large positive machine dependent constant which is used for # preventing machine overflows. A reasonable choice is to set big to # (approximately) the square root of the largest DOUBLE PRECISION number # which can be held in the machine. big = sqrt(np.finfo(y.dtype).max) small = np.nextafter(np.finfo(y.dtype).epsneg, 1.0) # following dhstrt.f from here dx = b - a absdx = abs(dx) relper = small**0.375 # compute an approximate bound (dfdxb) on the partial derivative of the # equation with respect to the independent variable. protect against an # overflow. also compute a bound (fbnd) on the first derivative locally. da = copysign(max(min(relper * abs(a), absdx), 100.0 * small * abs(a)), dx) da = da or relper * dx sf = df(a + da, y) # evaluate yp = sf - yprime delf = norm(yp) dfdxb = big if delf < big * abs(da): dfdxb = delf / abs(da) fbnd = norm(sf) # compute an estimate (dfdub) of the local lipschitz constant for the # system of differential equations. this also represents an estimate of the # norm of the jacobian locally. three iterations (two when neq=1) are used # to estimate the lipschitz constant by numerical differences. the first # perturbation vector is based on the initial derivatives and direction of # integration. the second perturbation vector is formed using another # evaluation of the differential equation. the third perturbation vector # is formed using perturbations based only on the initial values. # components that are zero are always changed to non-zero values (except # on the first iteration). when information is available, care is taken to # ensure that components of the perturbation vector have signs which are # consistent with the slopes of local solution curves. also choose the # largest bound (fbnd) for the first derivative. # perturbation vector size is held constant for all iterations. compute # this change from the size of the vector of initial values. dely = relper * norm(y) dely = dely or relper dely = copysign(dely, dx) delf = norm(yprime) fbnd = max(fbnd, delf) if delf: # use initial derivatives for first perturbation spy[:] = yprime yp[:] = yprime else: # cannot have a null perturbation vector spy[:] = 0.0 yp[:] = 1.0 delf = norm(yp) dfdub = 0.0 lk = min(neq + 1, 3) for k in range(1, lk + 1): # define perturbed vector of initial values pv[:] = y + dely / delf * yp if k == 2: # use a shifted value of the independent variable in computing # one estimate yp[:] = df(a + da, pv) # evaluate pv[:] = yp - sf else: # evaluate derivatives associated with perturbed vector and # compute corresponding differences yp[:] = df(a, pv) # evaluate pv[:] = yp - yprime # choose largest bounds on the first derivative and a local lipschitz # constant fbnd = max(fbnd, norm(yp)) delf = norm(pv) if delf >= big * abs(dely): # protect against an overflow dfdub = big break dfdub = max(dfdub, delf / abs(dely)) if k == lk: break # choose next perturbation vector delf = delf or 1.0 if k == 2: dy = y.copy() # vec dy[:] = np.where(dy, dy, dely / relper) else: dy = pv.copy() # abs removed (complex) dy[:] = np.where(dy, dy, delf) spy[:] = np.where(spy, spy, yp) # use correct direction if possible. yp[:] = np.where(spy, np.copysign(dy.real, spy.real), dy.real) if np.issubdtype(y.dtype, np.complexfloating): yp[:] += 1j * np.where(spy, np.copysign(dy.imag, spy.imag), dy.imag) delf = norm(yp) # compute a bound (ydpb) on the norm of the second derivative ydpb = dfdxb + dfdub * fbnd # define the tolerance parameter upon which the starting step size is to be # based. a value in the middle of the error tolerance range is selected. tolexp = np.log10(etol) tolsum = tolexp.sum() tolmin = min(tolexp.min(), big) tolp = 10.0**(0.5 * (tolsum / neq + tolmin) / (morder + 1)) # compute a starting step size based on the above first and second # derivative information # restrict the step length to be not bigger than abs(b-a). # (unless b is too close to a) h = absdx if ydpb == 0.0 and fbnd == 0.0: # both first derivative term (fbnd) and second derivative term (ydpb) # are zero if tolp < 1.0: h = absdx * tolp elif ydpb == 0.0: # only second derivative term (ydpb) is zero if tolp < fbnd * absdx: h = tolp / fbnd else: # second derivative term (ydpb) is non-zero srydpb = sqrt(0.5 * ydpb) if tolp < srydpb * absdx: h = tolp / srydpb # further restrict the step length to be not bigger than 1/dfdub if dfdub: # `if` added (div 0) h = min(h, 1.0 / dfdub) # finally, restrict the step length to be not smaller than # 100*small*abs(a). however, if a=0. and the computed h underflowed to # zero, the algorithm returns small*abs(b) for the step length. h = max(h, 100.0 * small * abs(a)) h = h or small * abs(b) # now set direction of integration h = copysign(h, dx) return h
def _estimate_error_norm(self, K, h, scale): return norm(self._estimate_error(K, h) / scale)
def __init__(self, fun, t0, y0, t_bound, max_step=np.inf, rtol=1e-3, atol=1e-6, vectorized=False, first_step=None, k_max=12, **extraneous): if not (isinstance(k_max, int) and k_max > 0 and k_max < 13): raise ValueError("`k_max` should be an integer between 1 and 12.") warn_extraneous(extraneous) super(SWAG, self).__init__( fun, t0, y0, t_bound, vectorized, support_complex=True) self.max_step = validate_max_step(max_step) self.rtol, self.atol = validate_tol(rtol, atol, self.y) # starting step size self.yp = self.fun(self.t, self.y) # initial evaluation if first_step is None: b = self.t + copysign(min(abs(self.t_bound - self.t), self.max_step), self.direction) self.h = h_start(self.fun, self.t, b, self.y, self.yp, 1, self.rtol, self.atol) else: h_abs = validate_first_step(first_step, t0, t_bound) self.h = copysign(h_abs, self.direction) # constants small = np.nextafter(np.finfo(self.y.dtype).epsneg, 1) self.twou = 2.0 * small self.fouru = 4.0 * small self.two = (2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0, 4096.0, 8192.0) self.gstr = (0.5, 0.0833, 0.0417, 0.0264, 0.0188, 0.0143, 0.0114, 0.00936, 0.00789, 0.00679, 0.00592, 0.00524, 0.00468) iq = np.arange(1, k_max + 2) self.iqq = 1.0 / (iq * (iq + 1)) # added self.k_max = k_max # added self.eps = 1.0 # tolerances in wt self.p5eps = 0.5 # tolerances in wt # allocate arrays self.phi = np.empty((self.n, k_max + 2), self.y.dtype, 'F') self.psi = np.empty(k_max) self.alpha = np.empty(k_max) self.beta = np.empty(k_max) self.sig = np.empty(k_max + 1) self.v = np.empty(k_max) self.w = np.empty(k_max) self.g = np.empty(k_max + 1) self.gi = np.empty(k_max - 1) self.iv = np.zeros(max(0, k_max - 2), np.short) # Tolerances are dealt with like in scipy: wt is like scipy's scale # and will be update each step. This is only the initial value: self.wt = self.atol + self.rtol * 0.5*( np.abs(self.y) + np.abs(self.y - self.h*self.yp)) # initialization # from *** block 0 *** of dsteps.f, under IF START: _round = 0.0 if self.y.size: # to pass scipy's unit tests _round = self.twou * norm(self.y / self.wt) if self.p5eps < 100.0 * _round: # The compensated summation of the original code that would be # executed if nornd == False has been removed. Instead, this # warning is given to the user. warn("Numerical rounding may limit the accuracy " "at this tolerance.") self.phi[:, 0] = self.yp self.phi[:, 1] = 0.0 self.sig[0] = 1.0 self.g[0] = 1.0 self.g[1] = 0.5 self.hold = 0.0 self.k = 1 self.kold = 0 self.kprev = 0 self.phase1 = True self.ivc = 0 self.kgi = 0 self.ns = 0 # from ddes.f, for stiffness detection self.kle4 = 0
def _step_impl(self): # current state x = self.t y = self.y.copy() self.y_old = self.y # added, for dense output # load variables (hold != self.step_size, rounding matters) (hold, h, wt, k, kold, phi, yp, psi, alpha, beta, sig, v, w, g, phase1, ns, kprev, ivc, iv, kgi, gi, gstr, iqq, eps, p5eps) = ( self.hold, self.h, self.wt, self.k, self.kold, self.phi, self.yp, self.psi, self.alpha, self.beta, self.sig, self.v, self.w, self.g, self.phase1, self.ns, self.kprev, self.ivc, self.iv, self.kgi, self.gi, self.gstr, self.iqq, self.eps, self.p5eps) # from *** ddes.f *** min_step = self.fouru * abs(x) # added # stiffness detection if kold > 4: self.kle4 = 0 else: self.kle4 += 1 if self.kle4 > 50 and self.k_max > 4: # This warning is issued once, after 50 consequtive steps are # taken with order <= 4, while k_max > 4. warn("Your problem appears to be stiff (for this tolerance).") self.kle4 = 0 # extrapolate if too close to t_bound d = self.t_bound - x if abs(d) <= min_step: self.kold = 0 # for dense output y[:] += d * yp # ouput self.t = self.t_bound self.y = y return True, None # don't allow to step over t_bound if self.direction * (h - d) > 0: h = d # limit h to max_step if self.max_step != np.inf: h = min(self.max_step, abs(h)) h = copysign(h, self.direction) # (***first executable statement dsteps) if abs(h) < min_step: return False, self.TOO_SMALL_STEP # If error tolerance is too small, increase it to an acceptable value # or rather terminate the integration with an error _round = self.twou * norm(y / wt) if p5eps < _round: eps = 2.0 * _round * (1.0 + self.fouru) return False, ("tolerance too tight.\n" f"suggested minimal increase factor: {eps}") ifail = 0 # *** begin block 1 *** # Compute coefficients of formulas for this step. Avoid computing # those quantities not changed when step size is not changed. while True: kp1 = k + 1 km1 = k - 1 km2 = k - 2 # ns is the number of dsteps taken with size h, including the # current one. When k < ns, no coefficients change if h != hold: ns = 0 if ns <= kold: ns += 1 if k >= ns: # Compute those components of alpha(*), beta(*), psi(*), sig(*) # which are changed nsm1 = ns - 1 # added psi_old = psi[nsm1:km1].copy() # added psi[nsm1] = h * ns alpha[nsm1] = 1.0 / ns beta[nsm1] = 1.0 sig[ns] = 1.0 for i, temp2 in enumerate(psi_old, start=ns): temp1 = h + temp2 alp = h / temp1 # added psi[i] = temp1 alpha[i] = alp beta[i] = beta[i-1] * psi[i-1] / temp2 sig[i+1] = (i + 1) * alp * sig[i] # compute coefficients g(*) # initialize v(*) and set w(*). if ns == 1: w[:k] = v[:k] = iqq[:k] ivc = kgi = 0 if k != 1: kgi = 1 gi[0] = w[1] else: # if order was raised, update diagonal part of v(*) if k > kprev: if ivc != 0: ivc -= 1 jv = kp1 - iv[ivc] else: jv = 1 w[km1] = v[km1] = iqq[km1] if k == 2: kgi = 1 gi[0] = w[1] for j, alp in enumerate(alpha[jv:nsm1], start=jv): i = km1 - j v[i] -= alp * v[i+1] w[i] = v[i] if k == ns and jv < nsm1: kgi = nsm1 gi[kgi-1] = w[1] # update v(*) and set w(*) limit1 = kp1 - ns v[:limit1] -= alpha[nsm1] * v[1:limit1+1] w[:limit1+1] = v[:limit1+1] g[ns] = w[0] if limit1 != 1: kgi = ns gi[nsm1] = w[1] if k < kold: iv[ivc] = limit1 + 2 ivc += 1 # compute the g(*) in the work vector w(*) kprev = k for i, alp in enumerate(alpha[ns:k], start=ns): limit2 = k - i w[:limit2] -= alp * w[1:limit2+1] g[i+1] = w[0] # *** end block 1 *** # *** begin block 2 *** # Predict a solution p(*), evaluate derivatives using predicted # solution, estimate local error at order k and errors at orders # k, k-1, k-2 as if constant step size were used. # change phi to phi star phi[:, ns:k] *= beta[ns:k] # predict solution and differences phi[:, kp1] = phi[:, k] phi[:, k] = 0.0 p = h * (phi[:, :k] @ g[:k]) + y for i in range(k, 0, -1): phi[:, i-1] += phi[:, i] xold = x x += h absh = abs(h) yp[:] = self.fun(x, p) # evaluate # added update of wt: wt[:] = self.atol + self.rtol * 0.5*(np.abs(p) + np.abs(y)) # estimate errors at orders k, k-1, k-2 temp3 = 1.0 / wt temp4 = yp - phi[:, 0] if k > 2: erkm2 = absh * norm((phi[:, km2] + temp4) * temp3) erkm2 *= sig[km2] * gstr[km2-1] if k > 1: erkm1 = absh * norm((phi[:, km1] + temp4) * temp3) erkm1 *= sig[km1] * gstr[km2] erk = absh * norm(temp4 * temp3) err = erk * (g[km1] - g[k]) erk *= sig[k] * gstr[km1] # test if order should be lowered knew = k if k > 2 and max(erkm1, erkm2) < erk: knew = km1 elif k == 2 and erkm1 < 0.5 * erk: knew = km1 # test if step successful if err <= eps: # success break # else: failure # *** end block 2 *** # *** begin block 3 *** # The step is unsuccessful. restore x, phi(*,*), psi(*). if third # consecutive failure, set order to one. If step fails more than # three times, consider an optimal step size. Double error # tolerance and return if estimated step size is too small for # machine precision. # restore x, phi(*,*) and psi(*) phase1 = False x = xold phi[:, :k] -= phi[:, 1:kp1] phi[:, :k] /= beta[:k] psi[:km1] = psi[1:k] - h # On third failure, set order to one. # Thereafter, use optimal step size. NFS[()] += 1 ifail += 1 temp2 = 0.5 if ifail >= 4 and p5eps < 0.25 * erk: temp2 = sqrt(p5eps / erk) if ifail >= 3: knew = 1 h *= temp2 k = knew ns = 0 if abs(h) < min_step: return False, self.TOO_SMALL_STEP # *** end block 3 *** # end while loop # *** begin block 4 *** # The step is successful. Correct the predicted solution, evaluate the # derivatives using the corrected solution and update the differences. # Determine best order and step size for next step. kold = k hold = h # correct and evaluate y[:] = h * g[k] * (yp - phi[:, 0]) + p yp[:] = self.fun(x, y) # evaluate # p does not need to store y_old for dense output anymore. # update differences for next step phi[:, k] = yp - phi[:, 0] phi[:, kp1] = phi[:, k] - phi[:, kp1] phi[:, :k] += phi[:, k, np.newaxis] # Estimate error at order k+1 unless: # - in first phase when always raise order, # - already decided to lower order, # - step size not constant so estimate unreliable if knew == km1 or k == self.k_max: phase1 = False erkp1 = 0.0 if phase1: # raise order k = kp1 erk = erkp1 elif knew == km1: # lower order, as already decided in block 2 k = km1 erk = erkm1 elif k < ns: erkp1 = gstr[k] * absh * norm(phi[:, kp1] / wt) # Using estimated error at order k+1, determine appropriate order # for next step if k == 1: if erkp1 < 0.5 * erk and k < self.k_max: # raise order k = kp1 erk = erkp1 # else: no order change elif erkm1 <= min(erk, erkp1): # lower order k = km1 erk = erkm1 elif not (erkp1 > erk or k == self.k_max): # Here erkp1 < erk < max(erkm1, erkm2) else order would # have been lowered in block 2. Thus order is to be raised k = kp1 erk = erkp1 # else: no order change # else: no order change # With new order determine appropriate step size for next step if phase1 or p5eps >= erk * self.two[k]: hnew = h + h elif p5eps >= erk: # keep step size (double, or don't increase at all) hnew = h else: # calculate reduced step size r = (p5eps / erk) ** (1.0 / (k + 1)) hnew = absh * max(0.5, min(0.9, r)) hnew = copysign(max(hnew, min_step), h) h = hnew # *** end block 4 *** # output self.t = x self.y = y # store the non-mutable variables for the next step: (self.h, self.hold, self.k, self.kold, self.phase1, self.ns, self.kprev, self.ivc, self.kgi) = ( h, hold, k, kold, phase1, ns, kprev, ivc, kgi) return True, None
def _step_impl(self): from scipy.integrate._ivp.bdf import (change_D, solve_bdf_system, NEWTON_MAXITER, MIN_FACTOR, MAX_FACTOR, MAX_ORDER) from scipy.integrate._ivp.common import norm t = self.t D = self.D max_step = self.max_step min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t) if self.h_abs > max_step: h_abs = max_step change_D(D, self.order, max_step / self.h_abs) self.n_equal_steps = 0 elif self.h_abs < min_step: h_abs = min_step change_D(D, self.order, min_step / self.h_abs) self.n_equal_steps = 0 else: h_abs = self.h_abs atol = self.atol rtol = self.rtol order = self.order alpha = self.alpha gamma = self.gamma error_const = self.error_const J = self.J LU = self.LU current_jac = self.jac is None step_accepted = False while not step_accepted: if h_abs < min_step: return False, self.TOO_SMALL_STEP h = h_abs * self.direction t_new = t + h if self.direction * (t_new - self.t_bound) > 0: t_new = self.t_bound change_D(D, order, np.abs(t_new - t) / h_abs) self.n_equal_steps = 0 LU = None h = t_new - t h_abs = np.abs(h) y_predict = np.sum(D[:order + 1], axis=0) scale = atol + rtol * np.abs(y_predict) psi = np.dot(D[1:order + 1].T, gamma[1:order + 1]) / alpha[order] converged = False c = h / alpha[order] while not converged: if LU is None: LU = self.lu(self.I - c * J) converged, n_iter, y_new, d = solve_bdf_system( self.fun, t_new, y_predict, c, psi, LU, self.solve_lu, scale, self.newton_tol) if not converged: if current_jac: break J = self.jac(t_new, y_predict) LU = None current_jac = True if not converged: factor = 0.5 h_abs *= factor change_D(D, order, factor) self.n_equal_steps = 0 LU = None continue safety = round(0.9 * (2 * NEWTON_MAXITER + 1) / (2 * NEWTON_MAXITER + n_iter), ndigits=15) scale = atol + rtol * np.abs(y_new) error = error_const[order] * d error_norm = norm(error / scale) if error_norm > 1: factor = max(MIN_FACTOR, safety * error_norm**(-1 / (order + 1))) h_abs *= factor change_D(D, order, factor) self.n_equal_steps = 0 # As we didn't have problems with convergence, we don't # reset LU here. else: step_accepted = True self.n_equal_steps += 1 self.t = t_new self.y = y_new self.h_abs = h_abs self.J = J self.LU = LU # Update differences. The principal relation here is # D^{j + 1} y_n = D^{j} y_n - D^{j} y_{n - 1}. Keep in mind that D # contained difference for previous interpolating polynomial and # d = D^{k + 1} y_n. Thus this elegant code follows. D[order + 2] = d - D[order + 1] D[order + 1] = d for i in reversed(range(order + 1)): D[i] += D[i + 1] if self.n_equal_steps < order + 1: return True, None if order > 1: error_m = error_const[order - 1] * D[order] error_m_norm = norm(error_m / scale) else: error_m_norm = np.inf if order < MAX_ORDER: error_p = error_const[order + 1] * D[order + 2] error_p_norm = norm(error_p / scale) else: error_p_norm = np.inf error_norms = np.array([error_m_norm, error_norm, error_p_norm]) with np.errstate(divide='ignore'): factors = error_norms**(-1 / np.arange(order, order + 3)) delta_order = np.argmax(factors) - 1 order += delta_order self.order = order factor = min(MAX_FACTOR, safety * np.max(factors)) # # This is the custom modification for PriNCe if round(self.h_abs * factor, ndigits=15) > self.max_step: if round(self.h_abs, ndigits=15) != self.max_step: change_D(D, order, max_step / self.h_abs) self.h_abs = self.max_step self.n_equal_steps = 0 self.LU = None self.n_equal_steps = 0 return True, None # custom modications end self.h_abs *= factor change_D(D, order, factor) self.n_equal_steps = 0 self.LU = None return True, None
def _step_impl(self): t = self.t y = self.y f = self.f n = y.size max_step = self.max_step atol = self.atol rtol = self.rtol min_step = 1e-20 #10 * np.abs(np.nextafter(t, self.direction * np.inf) - t) if self.h_abs > max_step: h_abs = max_step h_abs_old = None error_norm_old = None elif self.h_abs < min_step: h_abs = min_step h_abs_old = None error_norm_old = None else: h_abs = self.h_abs h_abs_old = self.h_abs_old error_norm_old = self.error_norm_old J = self.J LU_real = self.LU_real LU_complex = self.LU_complex current_jac = self.current_jac jac = self.jac rejected = False step_accepted = False message = None while not step_accepted: if h_abs < min_step: return False, self.TOO_SMALL_STEP h = h_abs * self.direction t_new = t + h if self.direction * (t_new - self.t_bound) > 0: t_new = self.t_bound h = t_new - t # may introduce numerical rounding errors h_abs = np.abs(h) if self.sol is None: Z0 = np.zeros((3, y.shape[0])) else: Z0 = self.sol(t + h * C).T - y scale = atol + np.abs(y) * rtol converged = False while not converged: if LU_real is None or LU_complex is None: if self.mass_matrix is None: LU_real = self.lu(MU_REAL / h * self.I - J) LU_complex = self.lu(MU_COMPLEX / h * self.I - J) else: try: LU_real = self.lu(MU_REAL / h * self.mass_matrix - J) LU_complex = self.lu(MU_COMPLEX / h * self.mass_matrix - J) except ValueError as e: # import pdb; pdb.set_trace() return False, 'LU decomposition failed ({})'.format( e) if BPRINT: print('solving system at t={} with dt={}'.format(t, h)) U_matrix = np.triu(LU_real[0], k=0) L_matrix = np.tril(LU_real[0], k=-1) + self.I self.info['cond']['LU_real'].append( np.linalg.cond(U_matrix * L_matrix)) U_matrix = np.triu(LU_complex[0], k=0) L_matrix = np.tril(LU_complex[0], k=-1) + self.I self.info['cond']['LU_complex'].append( np.linalg.cond(U_matrix * L_matrix)) self.info['cond']['t'].append(t) self.info['cond']['h'].append(h) print('\tcond(LU_real) = {:.3e}'.format( self.info['cond']['LU_real'][-1])) print('\tcond(LU_complex) = {:.3e}'.format( self.info['cond']['LU_complex'][-1])) converged, n_iter, Z, rate = solve_collocation_system( self.fun, t, y, h, Z0, scale, self.newton_tol, LU_real, LU_complex, self.solve_lu, self.mass_matrix) if not converged: if BPRINT: print('no convergence at t={} with dt={}'.format(t, h)) if current_jac: # we only allow one Jacobian computation per time step if BPRINT: print(' Jac had already been updated') break J = self.jac(t, y, f) current_jac = True LU_real = None LU_complex = None ## End of the convergence loop if not converged: if BPRINT: print(' --> dt will be reduced') h_abs *= 0.5 LU_real = None LU_complex = None continue y_new = y + Z[-1] if self.constant_dt: step_accepted = True error_norm = 0. else: ZE = Z.T.dot(E) / h if self.mass_matrix is None: error = self.solve_lu(LU_real, f + ZE) error_norm = norm(error / scale) else: # see Hairer II, chapter IV.8, page 127 error = self.solve_lu(LU_real, f + self.mass_matrix.dot(ZE)) if self.index_algebraic_vars is not None: error[ self. index_algebraic_vars] = 0. # ideally error*(h**index) error_norm = np.linalg.norm( error / scale) / (n - self.nvars_algebraic)**0.5 # we exclude the algebraic components, as they would otherwise artificially lower the error norm # error_norm = norm(error / scale) else: error_norm = norm(error / scale) scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol error_norm = norm(error / scale) safety = 0.9 * (2 * NEWTON_MAXITER + 1) / (2 * NEWTON_MAXITER + n_iter) if BPRINT: print('\t1st error estimate: {:.3e}'.format(error_norm)) if rejected and error_norm > 1: # if error_norm > 1: if BPRINT: print('\t rejected') if self.mass_matrix is None: error = self.solve_lu(LU_real, self.fun(t, y + error) + ZE) error_norm = norm(error / scale) else: error = self.solve_lu( LU_real, self.fun(t, y + error) + self.mass_matrix.dot(ZE)) if self.index_algebraic_vars is not None: error[ self. index_algebraic_vars] = 0. # ideally error*(h**index) error_norm = np.linalg.norm(error / scale) / ( n - self.nvars_algebraic)**0.5 # we exclude the algebraic components, as they would otherwise artificially lower the error norm # error_norm = norm(error / scale) else: error_norm = norm(error / scale) if BPRINT: print( '\t2nd error estimate: {:.3e}'.format(error_norm)) if error_norm > 1: if BPRINT and y_new.size < 10: print('\terror=', error / scale) factor = predict_factor(h_abs, h_abs_old, error_norm, error_norm_old) h_abs *= max(MIN_FACTOR, safety * factor) LU_real = None LU_complex = None rejected = True else: if BPRINT: print('\terror estimate is small enough') step_accepted = True ## Step is converged and accepted recompute_jac = jac is not None and n_iter > 2 and rate > 1e-3 if self.constant_dt: factor = self.max_step / h_abs # return to the maximum value else: factor = predict_factor(h_abs, h_abs_old, error_norm, error_norm_old) factor = min(MAX_FACTOR, safety * factor) if not recompute_jac and factor < 1.2: factor = 1 else: LU_real = None LU_complex = None f_new = self.fun(t_new, y_new) if recompute_jac: J = jac(t_new, y_new, f_new) current_jac = True elif jac is not None: current_jac = False self.h_abs_old = self.h_abs self.error_norm_old = error_norm self.h_abs = h_abs * factor self.y_old = y self.t = t_new self.y = y_new self.f = f_new self.Z = Z self.LU_real = LU_real self.LU_complex = LU_complex self.current_jac = current_jac self.J = J self.t_old = t self.sol = self._compute_dense_output() if self.bPrintProgress: print('t=', t) return step_accepted, message
def solve_collocation_system(fun, t, y, h, Z0, scale, tol, LU_real, LU_complex, solve_lu, mass_matrix=None): """Solve the collocation system. Parameters ---------- fun : callable Right-hand side of the system. t : float Current time. y : ndarray, shape (n,) Current state. h : float Step to try. Z0 : ndarray, shape (3, n) Initial guess for the solution. It determines new values of `y` at ``t + h * C`` as ``y + Z0``, where ``C`` is the Radau method constants. scale : float Problem tolerance scale, i.e. ``rtol * abs(y) + atol``. tol : float Tolerance to which solve the system. This value is compared with the normalized by `scale` error. LU_real, LU_complex LU decompositions of the system Jacobians. solve_lu : callable Callable which solves a linear system given a LU decomposition. The signature is ``solve_lu(LU, b)``. mass_matrix : {None, array_like, sparse_matrix}, optional Defined the constant mass matrix of the system, with shape (n,n). It may be singular, thus defining a problem of the differential- algebraic type (DAE). The default value is None (equivalent to an identity mass matrix). Returns ------- converged : bool Whether iterations converged. n_iter : int Number of completed iterations. Z : ndarray, shape (3, n) Found solution. rate : float The rate of convergence. """ # raise Exception('custom radau') n = y.shape[0] M_real = MU_REAL / h M_complex = MU_COMPLEX / h W = TI.dot(Z0) Z = Z0 F = np.empty((3, n)) ch = h * C dW_norm_old = None dW = np.empty_like(W) converged = False rate = None for k in range(NEWTON_MAXITER): if BPRINT: print('\titer {}/{}'.format(k, NEWTON_MAXITER)) for i in range(3): F[i] = fun(t + ch[i], y + Z[i]) if not np.all(np.isfinite(F)): if BPRINT: print('\t\tF contains non real numbers...') break if mass_matrix is None: f_real = F.T.dot(TI_REAL) - M_real * W[0] f_complex = F.T.dot(TI_COMPLEX) - M_complex * (W[1] + 1j * W[2]) else: f_real = F.T.dot(TI_REAL) - M_real * mass_matrix.dot(W[0]) f_complex = F.T.dot( TI_COMPLEX) - M_complex * mass_matrix.dot(W[1] + 1j * W[2]) if BPRINT: print('\t\tresiduals: ||f_real||={:.3e}'.format(norm(f_real))) print('\t\t ||f_cplx||={:.3e}'.format(norm(f_complex))) dW_real = solve_lu(LU_real, f_real) dW_complex = solve_lu(LU_complex, f_complex) dW[0] = dW_real dW[1] = dW_complex.real dW[2] = dW_complex.imag dW_norm = norm(dW / scale) if BPRINT: print('\t\tdW_norm={:.3e}'.format(dW_norm)) if dW_norm_old is not None: rate = dW_norm / dW_norm_old if BPRINT and rate is not None: print('\t\trate={:.3e}'.format(rate)) print('\t\testimated true error: ||dW||={:.3E}'.format( rate / (1 - rate) * dW_norm)) if (rate is not None and (rate >= 1 or rate**(NEWTON_MAXITER - k) / (1 - rate) * dW_norm > tol)): # Newton loop diverges or does not converge fast enough if BPRINT: print( '\tfinal loop convergence would reach ||dW||={:.3e}>tol--> Newton failed' .format(rate**(NEWTON_MAXITER - k) / (1 - rate) * dW_norm)) break W += dW Z = T.dot(W) if (dW_norm == 0 or rate is not None and rate / (1 - rate) * dW_norm < tol): if BPRINT: print('\t\tconverged') converged = True break dW_norm_old = dW_norm return converged, k + 1, Z, rate
def _step_impl(self): t = self.t y = self.y max_step = self.max_step rtol = self.rtol atol = self.atol min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t) if self.line_search_class == None: self.line_search_class = BacktrackingLineSearch( ctol=self.ls_ctol, max_iter=self.ls_max_iter, dec_scale=self.ls_dec_scale) if self.h_abs > max_step: h_abs = max_step elif self.h_abs < min_step: h_abs = min_step else: h_abs = self.h_abs order = self.order step_accepted = False while not step_accepted: if h_abs < min_step: return False, self.TOO_SMALL_STEP h = h_abs * self.direction t_new = t + h if self.direction * (t_new - self.t_bound) > 0: t_new = self.t_bound h = t_new - t h_abs = np.abs(h) y_new, f_new, error, step = rk_step(self.fun, t, y, self.f, h, self.A, self.B, self.C, self.E, self.K) scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol error_norm = norm(error / scale) if error_norm == 0.0: h_abs *= MAX_FACTOR step_accepted = True elif error_norm < 1: h_abs *= min(MAX_FACTOR, max(1, SAFETY * error_norm**(-1 / (order + 1)))) step_accepted = True else: h_abs *= max(MIN_FACTOR, SAFETY * error_norm**(-1 / (order + 1))) if step_accepted: # check whether the step doesn't increase the energy value # This is basically an afterthought, but we can # refine the idea. # Note that the notation here becomes optimizerish # but this takes into account the difference energy, gradient = self.get_energy_gradient(y) (energy_at_x, grad_at_x, step_scale, new_step) = self.line_search_class.line_search( y, energy, gradient, step, self.get_energy_gradient) y_new = y + new_step f_new = -grad_at_x self.y_old = y self.t = t_new self.y = y_new self.h_abs = h_abs self.f = f_new return True, None
def _step_impl(self): t = self.t y = self.y max_step = self.max_step rtol = self.rtol atol = self.atol min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t) if self.h_abs > max_step: h_abs = max_step elif self.h_abs < min_step: h_abs = min_step else: h_abs = self.h_abs order = self.order step_accepted = False while not step_accepted: if h_abs < min_step: return False, self.TOO_SMALL_STEP h = h_abs * self.direction t_new = t + h if self.direction * (t_new - self.t_bound) > 0: t_new = self.t_bound h = t_new - t h_abs = np.abs(h) y_new, f_new, error = rk_step(self.fun, t, y, self.f, h, self.A, self.B, self.C, self.E, self.K) scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol error_norm = norm(error / scale) if error_norm == 0.0: h_abs *= MAX_FACTOR step_accepted = True elif error_norm < 1: h_abs *= min(MAX_FACTOR, max(1, SAFETY * error_norm ** (-1 / (order + 1)))) step_accepted = True else: if (h_abs<self.min_step_user): step_accepted = True logger.debug("nmin step size reached") else: h_abs *= max(MIN_FACTOR, SAFETY * error_norm ** (-1 / (order + 1))) self.y_old = y self.t = t_new self.y = y_new self.h_abs = h_abs self.f = f_new return True, None