def nr_step(self): """ Single step using Newton-Raphson method. Returns ------- float maximum absolute mismatch """ system = self.system # evaluate discrete, differential, algebraic, and Jacobians system.dae.clear_fg() system.l_update_var(self.models, niter=self.niter, err=self.mis[-1]) system.s_update_var(self.models) system.f_update(self.models) system.g_update(self.models) system.l_update_eq(self.models) system.fg_to_dae() if self.config.method == 'NR': system.j_update(models=self.models) elif self.config.method == 'dishonest': if self.niter < self.config.n_factorize: system.j_update(self.models) # prepare and solve linear equations self.inc = -matrix([matrix(system.dae.f), matrix(system.dae.g)]) self.A = sparse([[system.dae.fx, system.dae.gx], [system.dae.fy, system.dae.gy]]) if not self.config.linsolve: self.inc = self.solver.solve(self.A, self.inc) else: self.inc = self.solver.linsolve(self.A, self.inc) system.dae.x += np.ravel(np.array(self.inc[:system.dae.n])) system.dae.y += np.ravel(np.array(self.inc[system.dae.n:])) # find out variables associated with maximum mismatches fmax = 0 if system.dae.n > 0: fmax_idx = np.argmax(np.abs(system.dae.f)) fmax = system.dae.f[fmax_idx] logger.debug("Max. diff mismatch %.10g on %s", fmax, system.dae.x_name[fmax_idx]) gmax_idx = np.argmax(np.abs(system.dae.g)) gmax = system.dae.g[gmax_idx] logger.debug("Max. algeb mismatch %.10g on %s", gmax, system.dae.y_name[gmax_idx]) mis = max(abs(fmax), abs(gmax)) if self.niter == 0: self.mis[0] = mis else: self.mis.append(mis) system.vars_to_models() return mis
def nr_step(self): """ Single stepping for Newton Raphson method Returns ------- """ system = self.system # evaluate discrete, differential, algebraic, and jacobians system.e_clear() system.l_update_var() system.f_update() system.g_update() system.l_check_eq() system.l_set_eq() system.fg_to_dae() system.j_update() # prepare and solve linear equations self.inc = -matrix([matrix(system.dae.f), matrix(system.dae.g)]) self.A = sparse([[system.dae.fx, system.dae.gx], [system.dae.fy, system.dae.gy]]) self.inc = self.solver.solve(self.A, self.inc) system.dae.x += np.ravel(np.array(self.inc[:system.dae.n])) system.dae.y += np.ravel(np.array(self.inc[system.dae.n:])) mis = np.max(np.abs(system.dae.fg)) self.mis.append(mis) system.vars_to_models() return mis
def _debug_ac(self, xy_idx): """ Debug Ac matrix by printing out equations and derivatives associated with the max. mismatch variable. Parameters ---------- xy_idx Index of the maximum mismatch into the `xy` array. """ xy_idx = xy_idx.tolist() assoc_eqns = self.Ac[:, xy_idx] assoc_vars = self.Ac[xy_idx, :] eqns_idx = np.where(np.ravel(matrix(assoc_eqns)))[0] vars_idx = np.where(np.ravel(matrix(assoc_vars)))[0] logger.debug('Max. correction is for variable %s [%d]', self.system.dae.xy_name[xy_idx], xy_idx) logger.debug('Associated equation rhs is %20g', self.system.dae.fg[xy_idx]) logger.debug('') logger.debug(f'{"xy_index":<10} {"Equation (row)":<20} {"Derivative":<20} {"Eq. Mismatch":<20}') for eq in eqns_idx: eq = eq.tolist() logger.debug(f'{eq:<10} {self.system.dae.xy_name[eq]:<20} {assoc_eqns[eq]:<20g} ' f'{self.system.dae.fg[eq]:<20g}') logger.debug('') logger.debug(f'{"xy_index":<10} {"Variable (col)":<20} {"Derivative":<20} {"Eq. Mismatch":<20}') for v in vars_idx: v = v.tolist() logger.debug(f'{v:<10} {self.system.dae.xy_name[v]:<20} {assoc_vars[v]:<20g} ' f'{self.system.dae.fg[v]:<20g}')
def linsolve(self, A, b): """ Solve linear equation set ``Ax = b`` and returns the solutions in a 1-D array. This function performs both symbolic and numeric factorizations every time, and can be slower than ``Solver.solve``. Parameters ---------- A Sparse matrix b RHS of the equation Returns ------- The solution in a 1-D np array. """ if self.sparselib == 'umfpack': try: umfpack.linsolve(A, b) except ArithmeticError: logger.error('Singular matrix. Case is not solvable') return np.ravel(b) elif self.sparselib == 'klu': try: klu.linsolve(A, b) except ArithmeticError: logger.error('Singular matrix. Case is not solvable') return np.ravel(b) elif self.sparselib in ('spsolve', 'cupy'): ccs = A.CCS size = A.size data = np.array(ccs[2]).reshape((-1,)) indices = np.array(ccs[1]).reshape((-1,)) indptr = np.array(ccs[0]).reshape((-1,)) A = csc_matrix((data, indices, indptr), shape=size) if self.sparselib == 'spsolve': x = spsolve(A, b) return np.ravel(x) elif self.sparselib == 'cupy': # delayed import for startup speed import cupy as cp # NOQA from cupyx.scipy.sparse import csc_matrix as csc_cu # NOQA from cupyx.scipy.sparse.linalg.solve import lsqr as cu_lsqr # NOQA cu_A = csc_cu(A) cu_b = cp.array(np.array(b).reshape((-1,))) x = cu_lsqr(cu_A, cu_b) return np.ravel(cp.asnumpy(x[0]))
def test_init(self): """ Test if the TDS initialization is successful. This function update ``dae.f`` and ``dae.g`` and checks if the residuals are zeros. """ system = self.system # fg_update is called in TDS.init() system.j_update(models=system.exist.pflow_tds) # reset diff. RHS where `check_init == False` system.dae.f[system.no_check_init] = 0.0 # warn if variables are initialized at limits if system.config.warn_limits: for model in system.exist.pflow_tds.values(): for item in model.discrete.values(): item.warn_init_limit() if np.max(np.abs(system.dae.fg)) < self.config.tol: logger.debug('Initialization tests passed.') return True # otherwise, show suspect initialization error fail_idx = np.ravel(np.where(abs(system.dae.fg) >= self.config.tol)) nan_idx = np.ravel(np.where(np.isnan(system.dae.fg))) bad_idx = np.hstack([fail_idx, nan_idx]) fail_names = [system.dae.xy_name[int(i)] for i in fail_idx] nan_names = [system.dae.xy_name[int(i)] for i in nan_idx] bad_names = fail_names + nan_names title = 'Suspect initialization issue! Simulation may crash!' err_data = { 'Name': bad_names, 'Var. Value': system.dae.xy[bad_idx], 'Eqn. Mismatch': system.dae.fg[bad_idx], } tab = Tab( title=title, header=err_data.keys(), data=list(map(list, zip(*err_data.values()))), ) logger.error(tab.draw()) if system.options.get('verbose') == 1: breakpoint() system.exit_code += 1 return False
def solve(self, A, b): """ Solve linear system ``Ax = b`` using numeric factorization ``N`` and symbolic factorization ``F``. Store the solution in ``b``. This function caches the symbolic factorization in ``self.F`` and is faster in general. Will attempt ``Solver.linsolve`` if the cached symbolic factorization is invalid. Parameters ---------- A Sparse matrix for the equation set coefficients. F The symbolic factorization of A or a matrix with the same non-zero shape as ``A``. N Numeric factorization of A. b RHS of the equation. Returns ------- numpy.ndarray The solution in a 1-D ndarray """ self.A = A self.b = b if self.factorize is True: self.F = self._symbolic(self.A) self.factorize = False try: self.N = self._numeric(self.A, self.F) self._solve(self.A, self.F, self.N, self.b) return np.ravel(self.b) except ValueError: logger.debug('Unexpected symbolic factorization.') self.F = self._symbolic(self.A) self.N = self._numeric(self.A, self.F) self._solve(self.A, self.F, self.N, self.b) return np.ravel(self.b) except ArithmeticError: logger.error('Jacobian matrix is singular.') diag = self.A[0:self.A.size[0]**2:self.A.size[0] + 1] idx = (np.argwhere(np.array(matrix(diag)).ravel() == 0.0)).ravel() logger.error('The xy indices of associated variables:') logger.error(idx) return np.ravel(matrix(np.nan, self.b.size, 'd'))
def nr_step(self): """ Single step using Newton-Raphson method. Returns ------- float maximum absolute mismatch """ system = self.system # evaluate discrete, differential, algebraic, and Jacobians system.dae.clear_fg() system.l_update_var(self.models, niter=self.niter, err=self.mis[-1]) system.s_update_var(self.models) system.f_update(self.models) system.g_update(self.models) system.l_update_eq(self.models) system.fg_to_dae() if self.config.method == 'NR': system.j_update(models=self.models) elif self.config.method == 'dishonest': if self.niter < self.config.n_factorize: system.j_update(self.models) # prepare and solve linear equations self.inc = -matrix([matrix(system.dae.f), matrix(system.dae.g)]) self.A = sparse([[system.dae.fx, system.dae.gx], [system.dae.fy, system.dae.gy]]) if not self.config.linsolve: self.inc = self.solver.solve(self.A, self.inc) else: self.inc = self.solver.linsolve(self.A, self.inc) system.dae.x += np.ravel(np.array(self.inc[:system.dae.n])) system.dae.y += np.ravel(np.array(self.inc[system.dae.n:])) mis = np.max(np.abs(system.dae.fg)) if self.niter == 0: self.mis[0] = mis else: self.mis.append(mis) system.vars_to_models() return mis
def solve(self, A, b): """ Solve linear system ``Ax = b`` using numeric factorization ``N`` and symbolic factorization ``F``. Store the solution in ``b``. This function caches the symbolic factorization in ``self.F`` and is faster in general. Will attempt ``Solver.linsolve`` if the cached symbolic factorization is invalid. Parameters ---------- A Sparse matrix for the equation set coefficients. F The symbolic factorization of A or a matrix with the same non-zero shape as ``A``. N Numeric factorization of A. b RHS of the equation. Returns ------- numpy.ndarray The solution in a 1-D ndarray """ self.A = A self.b = b if self.sparselib in ('umfpack', 'klu'): if self.factorize is True: self.F = self._symbolic(self.A) self.factorize = False try: self.N = self._numeric(self.A, self.F) self._solve(self.A, self.F, self.N, self.b) return np.ravel(self.b) except ValueError: logger.debug('Unexpected symbolic factorization.') self.F = self._symbolic(self.A) self.N = self._numeric(self.A, self.F) self._solve(self.A, self.F, self.N, self.b) return np.ravel(self.b) except ArithmeticError: logger.error('Jacobian matrix is singular.') return np.ravel(matrix(np.nan, self.b.size, 'd')) elif self.sparselib in ('spsolve', 'cupy'): return self.linsolve(A, b)
def _debug_g(self, y_idx): """ Print out the associated variables with the given algebraic equation index. Parameters ---------- y_idx Index of the equation into the `g` array. Diff. eqns. are not counted in. """ y_idx = y_idx.tolist() logger.debug( f'Max. algebraic mismatch associated with {self.system.dae.y_name[y_idx]} [y_idx={y_idx}]' ) assoc_vars = self.system.dae.gy[y_idx, :] vars_idx = np.where(np.ravel(matrix(assoc_vars)))[0] logger.debug('') logger.debug(f'{"y_index":<10} {"Variable":<20} {"Derivative":<20}') for v in vars_idx: v = v.tolist() logger.debug( f'{v:<10} {self.system.dae.y_name[v]:<20} {assoc_vars[v]:<20g}' ) pass
def test_initialization(self): """ Update f and g to see if initialization is successful """ system = self.system system.e_clear(models=self.pflow_tds_models) system.l_update_var(models=self.pflow_tds_models) system.f_update(models=self.pflow_tds_models) system.g_update(models=self.pflow_tds_models) system.l_check_eq(models=self.pflow_tds_models) system.l_set_eq(models=self.pflow_tds_models) system.fg_to_dae() system.j_update(models=self.pflow_tds_models) if np.max(np.abs(system.dae.fg)) < self.config.tol: logger.debug('Initialization tests passed.') return True else: logger.warning('Suspect initialization issue!') fail_idx = np.where(abs(system.dae.fg) >= self.config.tol) fail_names = [ system.dae.xy_name[int(i)] for i in np.ravel(fail_idx) ] logger.warning(f"Check variables {', '.join(fail_names)}") return False
def _debug_g(self, y_idx): """ Print out the associated variables with the given algebraic equation index. Parameters ---------- y_idx Index of the equation into the `g` array. Diff. eqns. are not counted in. """ y_idx = y_idx.tolist() logger.debug('--> Iteration Number: niter = %d', self.niter) logger.debug('Max. algebraic equation mismatch:') logger.debug(' <%s> [y_idx=%d]', self.system.dae.y_name[y_idx], y_idx) logger.debug(' Variable value = %.4f', self.system.dae.y[y_idx]) logger.debug(' Mismatch value = %.4f', self.system.dae.g[y_idx]) assoc_vars = self.system.dae.gy[y_idx, :] vars_idx = np.where(np.ravel(matrix(assoc_vars)))[0] logger.debug('Related variable values:') logger.debug(f'{"y_index":<10} {"Variable":<20} {"Derivative":<20}') for v in vars_idx: v = v.tolist() logger.debug('%10d %20s %20g', v, self.system.dae.y_name[v], assoc_vars[v])
def solve(self, A, b): # delayed import for startup speed from cupyx.scipy.sparse import csc_matrix as csc_cu # NOQA from cupyx.scipy.sparse.linalg import lsqr as cu_lsqr # NOQA A_csc = self.to_csc(A) cu_A = csc_cu(A_csc) cu_b = cupy.array(np.array(b).ravel()) x = cu_lsqr(cu_A, cu_b) return np.ravel(cupy.asnumpy(x[0]))
def store_switch_times(self, models=None): models = self._get_models(models) out = [] for instance in models.values(): out.extend(instance.get_times()) out = np.ravel(np.array(out)) out = np.unique(out) out = out[np.where(out >= 0)] out = np.sort(out) self.switch_times = out return self.switch_times
def test_init(self): """ Update f and g to see if initialization is successful. """ system = self.system self.fg_update(system.exist.pflow_tds) system.j_update(models=system.exist.pflow_tds) # reset diff. RHS where `check_init == False` system.dae.f[system.no_check_init] = 0.0 # warn if variables are initialized at limits if system.config.warn_limits: for model in system.exist.pflow_tds.values(): for item in model.discrete.values(): item.warn_init_limit() if np.max(np.abs(system.dae.fg)) < self.config.tol: logger.debug('Initialization tests passed.') return True # otherwise, show suspect initialization error fail_idx = np.where(abs(system.dae.fg) >= self.config.tol) fail_names = [system.dae.xy_name[int(i)] for i in np.ravel(fail_idx)] title = 'Suspect initialization issue! Simulation may crash!' err_data = { 'Name': fail_names, 'Var. Value': system.dae.xy[fail_idx], 'Eqn. Mismatch': system.dae.fg[fail_idx], } tab = Tab( title=title, header=err_data.keys(), data=list(map(list, zip(*err_data.values()))), ) logger.error(tab.draw()) if system.options.get('verbose') == 1: breakpoint() system.exit_code += 1 return False
def store_switch_times(self, models=None): """ Store event switching time in a sorted Numpy array at ``System.switch_times``. Returns ------- array-like self.switch_times """ models = self._get_models(models) out = [] for instance in models.values(): out.extend(instance.get_times()) out = np.ravel(np.array(out)) out = np.unique(out) out = out[np.where(out >= 0)] out = np.sort(out) self.switch_times = out return self.switch_times
def store_switch_times(self, models): """ Store event switching time in a sorted Numpy array at ``System.switch_times``. Returns ------- array-like self.switch_times """ out = [] for instance in models.values(): out.extend(instance.get_times()) out = np.ravel(np.array(out)) out = np.append(out, out + 1e-4) out = np.unique(out) out = out[np.where(out >= 0)] out = np.sort(out) self.switch_times = out self.n_switches = len(self.switch_times) return self.switch_times
def _solve_g(self, verbose): system = self.system dae = system.dae self.converged = False self.niter = 0 self.mis = [] # check if the next step is critical time if self.is_switch_time(): self._last_switch_t = system.switch_times[self._switch_idx] system.switch_action(self.pflow_tds_models) while True: system.e_clear(models=self.pflow_tds_models) system.l_update_var(models=self.pflow_tds_models) system.g_update(models=self.pflow_tds_models) inc = -matrix(system.dae.g) system.j_update(models=self.pflow_tds_models) inc = self.solver.solve(dae.gy, inc) dae.y += np.ravel(np.array(inc)) system.vars_to_models() mis = np.max(np.abs(inc)) self.mis.append(mis) if verbose: print(f't={dae.t:<.4g}, iter={self.niter:<g}, mis={mis:<.4g}') if mis < self.config.tol: self.converged = True break elif self.niter > self.config.max_iter: raise NoConvergence(f'Convergence not reached after {self.config.max_iter} iterations') elif mis >= 1000 and (mis > 1e4 * self.mis[0]): raise NoConvergence('Mismatch increased too fast. Convergence not likely.') self.niter += 1
def _implicit_step(self): """ Integrate for a single given step. This function has an internal Newton-Raphson loop for algebraized semi-explicit DAE. The function returns the convergence status when done but does NOT progress simulation time. Returns ------- bool Convergence status in ``self.converged``. """ system = self.system dae = self.system.dae self.mis = [] self.niter = 0 self.converged = False self.x0 = np.array(dae.x) self.y0 = np.array(dae.y) self.f0 = np.array(dae.f) while True: system.e_clear(models=self.pflow_tds_models) system.l_update_var(models=self.pflow_tds_models) system.f_update(models=self.pflow_tds_models) system.g_update(models=self.pflow_tds_models) system.l_check_eq(models=self.pflow_tds_models) system.l_set_eq(models=self.pflow_tds_models) system.fg_to_dae() # lazy jacobian update if dae.t == 0 or self.niter > 3 or (dae.t - self._last_switch_t < 0.2): system.j_update(models=self.pflow_tds_models) self.solver.factorize = True # solve trapezoidal rule integration In = spdiag([1] * dae.n) self.Ac = sparse([[In - self.h * 0.5 * dae.fx, dae.gx], [-self.h * 0.5 * dae.fy, dae.gy]], 'd') # reset q as well q = dae.x - self.x0 - self.h * 0.5 * (dae.f + self.f0) for item in system.antiwindups: if len(item.x_set) > 0: for key, val in item.x_set: np.put(q, key[np.where(item.zi == 0)], 0) qg = np.hstack((q, dae.g)) inc = self.solver.solve(self.Ac, -matrix(qg)) # check for np.nan first if np.isnan(inc).any(): logger.error(f'NaN found in solution. Convergence not likely') self.niter = self.config.max_iter + 1 self.busted = True break # reset really small values to avoid anti-windup limiter flag jumps inc[np.where(np.abs(inc) < 1e-12)] = 0 # set new values dae.x += np.ravel(np.array(inc[:dae.n])) dae.y += np.ravel(np.array(inc[dae.n: dae.n + dae.m])) system.vars_to_models() # calculate correction mis = np.max(np.abs(inc)) self.mis.append(mis) self.niter += 1 # converged if mis <= self.config.tol: self.converged = True break # non-convergence cases if self.niter > self.config.max_iter: logger.debug(f'Max. iter. {self.config.max_iter} reached for t={dae.t:.6f}, ' f'h={self.h:.6f}, mis={mis:.4g} ' f'({system.dae.xy_name[np.argmax(inc)]})') break if mis > 1000 and (mis > 1e8 * self.mis[0]): logger.error(f'Error increased too quickly. Convergence not likely.') self.busted = True break if not self.converged: dae.x = np.array(self.x0) dae.y = np.array(self.y0) dae.f = np.array(self.f0) system.vars_to_models() return self.converged
def solve(self, A, b): A_csc = self.to_csc(A) x = spsolve(A_csc, b) return np.ravel(x)
def linsolve(self, A, b): try: klu.linsolve(A, b) except ArithmeticError: logger.error('Singular matrix. Case is not solvable') return np.ravel(b)