def __init__(self, dae, t, poly_order=5, tdp_fun=None): '''Constructor ''' pdq = Pdq(t, poly_order) N = len(pdq.collocationPoints) scheme = CollocationScheme(dae, pdq, tdp_fun=tdp_fun) x0 = cs.MX.sym('x0', dae.nx) X = scheme.x Z = scheme.z z0 = dae.z # Solve the collocation equations w.r.t. (X,Z) var = scheme.combine(['x', 'z']) eq = cs.Function('eq', [var, x0, scheme.u, scheme.p], [cs.vertcat(scheme.eq, scheme.x[:, 0] - x0)]) rf = cs.rootfinder('rf', 'newton', eq) # Initial point for the rootfinder w0 = ce.struct_MX(var) w0['x'] = cs.repmat(x0, 1, N) w0['z'] = cs.repmat(z0, 1, N - 1) sol = var(rf(w0, x0, scheme.u, scheme.p)) sol_X = sol['x'] sol_Z = sol['z'] [sol_Q] = cs.substitute([scheme.q], [X, Z], [sol_X, sol_Z]) self._simulate = cs.Function('CollocationSimulator', [x0, z0, scheme.u, scheme.p], [sol_X[:, -1], sol_Z[:, -1], sol_Q[:, -1], sol_X, sol_Z, sol_Q], ['x0', 'z0', 'u', 'p'], ['xf', 'zf', 'qf', 'X', 'Z', 'Q']) self._pdq = pdq self._dae = dae
def find_equilibrium(self, additional_eqs, guess=None, t_0=0.0, rootfinder_options=None): """Find a equilibrium point for the model. This method solves the root finding problem: f(x,y,u,t_0) = 0 g(x,y,u,t_0) = 0 additional_eqs (x,y,u,t_0) = 0 Use additional_eqs to specify the additional conditions remembering that dim(additional_eqs) = n_u, so the system can be well defined. If no initial guess is provided ("guess" parameter) a guess of ones will be used (not zero to avoid problems with singularities. Returns x_0, y_0, u_0 :param dict rootfinder_options: options to be passed to rootfinder :param additional_eqs: SX :param guess: DM :param t_0: float :return: (DM, DM, DM) """ if rootfinder_options is None: rootfinder_options = dict( nlpsol="ipopt", nlpsol_options=config.SOLVER_OPTIONS["nlpsol_options"]) if guess is None: guess = [1] * (self.n_x + self.n_y + self.n_u) if isinstance(additional_eqs, list): additional_eqs = vertcat(*additional_eqs) eqs = vertcat(self.ode, self.alg, additional_eqs) eqs = substitute(eqs, self.t, t_0) eqs = substitute(eqs, self.tau, 0) f_eqs = Function("f_equilibrium", [vertcat(*self.all_sym[1:-1])], [eqs]) rf = rootfinder("rf_equilibrium", "nlpsol", f_eqs, rootfinder_options) res = rf(guess) return ( res[:self.n_x], res[self.n_x:self.n_x + self.n_y], res[self.n_x + self.n_y:], )
def dxdt(self, h: float, states: Union[MX, SX], controls: Union[MX, SX], params: Union[MX, SX]) -> tuple: """ The dynamics of the system Parameters ---------- h: float The time step states: Union[MX, SX] The states of the system controls: Union[MX, SX] The controls of the system params: Union[MX, SX] The parameters of the system Returns ------- The derivative of the states """ nx = states[0].shape[0] _, _, defect = super(IRK, self).dxdt(h, states, controls, params) # Root-finding function, implicitly defines x_collocation_points as a function of x0 and p vfcn = Function("vfcn", [vertcat(*states[1:]), states[0], controls, params], [defect]).expand() # Create a implicit function instance to solve the system of equations ifcn = rootfinder("ifcn", "newton", vfcn) x_irk_points = ifcn(self.cx(), states[0], controls, params) x = [ states[0] if r == 0 else x_irk_points[(r - 1) * nx:r * nx] for r in range(self.degree + 1) ] # Get an expression for the state at the end of the finite element xf = self.cx.zeros(nx, self.degree + 1) # 0 # for r in range(self.degree + 1): xf[:, r] = xf[:, r - 1] + self._d[r] * x[r] return xf[:, -1], horzcat(states[0], xf[:, -1])
def test_ivp1(self): """Test solving IVP1 with collocation """ x = cs.MX.sym('x') xdot = x N = 10 tf = 1 pdq = cl.Pdq(t=[0, tf], poly_order=N) X = cs.MX.sym('X', 1, N + 1) f = cs.Function('f', [x], [xdot]) F = f.map(N + 1, 'serial') x0 = cs.MX.sym('x0') eq = cs.Function('eq', [cs.vec(X)], [cs.vec(F(X) - pdq.derivative(X))]) rf = cs.rootfinder('rf', 'newton', eq) sol = cs.reshape(rf(cs.DM.zeros(X.shape)), X.shape) nptest.assert_allclose(sol[:, -1], 1 * np.exp(1 * tf))
def get_equilibrium_states(num_masses, y, dot_y, x_end_value): assert y.shape == dot_y.shape assert y.shape[0] == 3 * (2 * num_masses + 1) x = y[0:3 * num_masses] x_end = y[-3:] eq_f = casadi.Function('eq_f', [x, x_end], [dot_y[3 * num_masses:6 * num_masses]]) solver = casadi.rootfinder('solver', 'newton', eq_f, {'abstol': 1e-10}) x_end_value = numpy.asarray(x_end_value).flatten() assert x_end_value[2] == CHAIN_X0[2] x_initial = numpy.linspace(CHAIN_X0, x_end_value, 2 + num_masses).T x_initial = x_initial[:, 1:-1].T.flatten() eq_solution = solver(x_initial, x_end_value) eq_state = numpy.zeros(y.shape) eq_state[0:3 * num_masses] = eq_solution eq_state[-3:] = x_end_value.reshape((-1, 1)) return eq_state
def __init__(self, name, dae, t, order, method='legendre', tdp_fun=None): """Make an integrator based on collocation method """ N = order scheme = CollocationScheme(dae, t=t, order=order, method=method, tdp_fun=tdp_fun) x0 = cs.MX.sym('x0', dae.nx) z0 = dae.z # Solve the collocation equations w.r.t. (x,K,Z) var = scheme.combine(['x', 'K', 'Z']) eq = cs.Function('eq', [var, x0, scheme.u, scheme.p], [cs.vertcat(scheme.eq, scheme.x[:, 0] - x0)]) rf = cs.rootfinder('rf', 'newton', eq) # Initial point for the rootfinder w0 = ce.struct_MX(var) w0['x'] = cs.repmat(x0, 1, scheme.x.shape[1]) w0['K'] = cs.MX.zeros(scheme.K.shape) w0['Z'] = cs.repmat(z0, 1, scheme.Z.shape[1]) sol = var(rf(w0, x0, scheme.u, dae.p)) sol_x = sol['x'] sol_K = sol['K'] sol_Z = sol['Z'] [sol_q, sol_Q, sol_X] = cs.substitute([scheme.q, scheme.Q, scheme.X], [scheme.x, scheme.K, scheme.Z], [sol_x, sol_K, sol_Z]) # TODO: return correct value for zf! # TODO: return only x instead of x and xf? super().__init__(name, [x0, z0, scheme.u, dae.p], [sol_x[:, 1 :], np.repeat(np.nan, dae.nz), sol_q[:, -1], sol_X, sol_Z, sol_Q, sol_x, sol_K, scheme.tc], ['x0', 'z0', 'u', 'p'], ['xf', 'zf', 'qf', 'X', 'Z', 'Q', 'x', 'K', 'tc']) self._scheme = scheme
def test_ivp(self): """Test solving IVP with collocation """ x = cs.MX.sym('x') xdot = x dae = dae_model.SemiExplicitDae(x=x, ode=xdot) N = 4 tf = 1 scheme = cl.CollocationScheme(dae=dae, t=[0, tf], order=N, method='legendre') x0 = cs.MX.sym('x0') var = scheme.combine(['x', 'K']) eqf = cs.Function('eq', [cs.vec(var), x0], [cs.vertcat(scheme.eq, scheme.x[:, 0] - x0)]) rf = cs.rootfinder('rf', 'newton', eqf) sol = var(rf(var(0), 1)) nptest.assert_allclose(sol['x', :, -1], np.atleast_2d(1 * np.exp(1 * tf)))
def _integrate(self, model, t_eval, inputs_dict=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving. If any input parameters that are present in the model are missing from "inputs", then the solution will consist of `ProcessedSymbolicVariable` objects, which must be provided with inputs to obtain their value. """ # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} has_symbolic_inputs = any( isinstance(v, casadi.MX) for v in inputs_dict.values() ) symbolic_inputs = casadi.vertcat( *[v for v in inputs_dict.values() if isinstance(v, casadi.MX)] ) # Create casadi objects for the root-finder inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) y0 = model.y0 # If y0 already satisfies the tolerance for all t then keep it if has_symbolic_inputs is False and all( np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) for t in t_eval ): pybamm.logger.debug("Keeping same solution at all times") return pybamm.Solution( t_eval, y0, model, inputs_dict, termination="success" ) # The casadi algebraic solver can read rhs equations, but leaves them unchanged # i.e. the part of the solution vector that corresponds to the differential # equations will be equal to the initial condition provided. This allows this # solver to be used for initialising the DAE solvers if model.rhs == {}: len_rhs = 0 y0_diff = casadi.DM() y0_alg = y0 else: len_rhs = model.concatenated_rhs.size y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] y_alg = None # Set up t_sym = casadi.MX.sym("t") y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) y_sym = casadi.vertcat(y0_diff, y_alg_sym) t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) alg = model.casadi_algebraic(t_sym, y_sym, inputs) # Check interpolant extrapolation if model.interpolant_extrapolation_events_eval: extrap_event = [ event(0, y0, inputs) for event in model.interpolant_extrapolation_events_eval ] if extrap_event: if (np.concatenate(extrap_event) < self.extrap_tol).any(): extrap_event_names = [] for event in model.events: if ( event.event_type == pybamm.EventType.INTERPOLANT_EXTRAPOLATION and ( event.expression.evaluate( 0, y0.full(), inputs=inputs_dict ) < self.extrap_tol ) ): extrap_event_names.append(event.name[12:]) raise pybamm.SolverError( "CasADi solver failed because the following interpolation " "bounds were exceeded at the initial conditions: {}. " "You may need to provide additional interpolation points " "outside these bounds.".format(extrap_event_names) ) # Set constraints vector in the casadi format # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0. constraints = np.zeros_like(model.bounds[0], dtype=int) # If the lower bound is positive then the variable must always be positive constraints[model.bounds[0] >= 0] = 1 # If the upper bound is negative then the variable must always be negative constraints[model.bounds[1] <= 0] = -1 # Set up rootfinder roots = casadi.rootfinder( "roots", "newton", dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg), { **self.extra_options, "abstol": self.tol, "constraints": list(constraints[len_rhs:]), }, ) timer = pybamm.Timer() integration_time = 0 for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it # We can't do this if there are symbolic inputs if has_symbolic_inputs is False and np.all( abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol ): pybamm.logger.debug( "Keeping same solution at t={}".format(t * model.timescale_eval) ) if y_alg is None: y_alg = y0_alg else: y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: t_eval_inputs_sym = casadi.vertcat(t, symbolic_inputs) # Solve try: timer.reset() y_alg_sol = roots(y0_alg, t_eval_inputs_sym) integration_time += timer.time() success = True message = None # Check final output y_sol = casadi.vertcat(y0_diff, y_alg_sol) fun = model.casadi_algebraic(t, y_sol, inputs) except RuntimeError as err: success = False message = err.args[0] fun = None # If there are no symbolic inputs, check the function is below the tol # Skip this check if there are symbolic inputs if success and ( has_symbolic_inputs is True or (not any(np.isnan(fun)) and np.all(casadi.fabs(fun) < self.tol)) ): # update initial guess for the next iteration y0_alg = y_alg_sol y0 = casadi.vertcat(y0_diff, y0_alg) # update solution array if y_alg is None: y_alg = y_alg_sol else: y_alg = casadi.horzcat(y_alg, y_alg_sol) elif not success: raise pybamm.SolverError( "Could not find acceptable solution: {}".format(message) ) elif any(np.isnan(fun)): raise pybamm.SolverError( "Could not find acceptable solution: solver returned NaNs" ) else: raise pybamm.SolverError( """ Could not find acceptable solution: solver terminated successfully, but maximum solution error ({}) above tolerance ({}) """.format( casadi.mmax(casadi.fabs(fun)), self.tol ) ) # Concatenate differential part y_diff = casadi.horzcat(*[y0_diff] * len(t_eval)) y_sol = casadi.vertcat(y_diff, y_alg) # Return solution object (no events, so pass None to t_event, y_event) sol = pybamm.Solution( [t_eval], y_sol, model, inputs_dict, termination="success" ) sol.integration_time = integration_time return sol
def dxdt(self, h: float, states: Union[MX, SX], controls: Union[MX, SX], params: Union[MX, SX]) -> tuple: """ The dynamics of the system Parameters ---------- h: float The time step states: Union[MX, SX] The states of the system controls: Union[MX, SX] The controls of the system params: Union[MX, SX] The parameters of the system Returns ------- The derivative of the states """ nu = controls.shape[0] nx = states.shape[0] # Choose collocation points time_points = [0] + collocation_points(self.degree, "legendre") # Coefficients of the collocation equation C = self.CX.zeros((self.degree + 1, self.degree + 1)) # Coefficients of the continuity equation D = self.CX.zeros(self.degree + 1) # Dimensionless time inside one control interval time_control_interval = self.CX.sym("time_control_interval") # For all collocation points for j in range(self.degree + 1): # Construct Lagrange polynomials to get the polynomial basis at the collocation point L = 1 for r in range(self.degree + 1): if r != j: L *= (time_control_interval - time_points[r]) / (time_points[j] - time_points[r]) # Evaluate the polynomial at the final time to get the coefficients of the continuity equation lfcn = Function("lfcn", [time_control_interval], [L]) D[j] = lfcn(1.0) # Evaluate the time derivative of the polynomial at all collocation points to get # the coefficients of the continuity equation tfcn = Function("tfcn", [time_control_interval], [tangent(L, time_control_interval)]) for r in range(self.degree + 1): C[j, r] = tfcn(time_points[r]) # Total number of variables for one finite element x0 = states u = controls x_irk_points = [self.CX.sym(f"X_irk_{j}", nx, 1) for j in range(1, self.degree + 1)] x = [x0] + x_irk_points x_irk_points_eq = [] for j in range(1, self.degree + 1): t_norm_init = (j - 1) / self.degree # normalized time # Expression for the state derivative at the collocation point xp_j = 0 for r in range(self.degree + 1): xp_j += C[r, j] * x[r] # Append collocation equations f_j = self.fun(x[j], self.get_u(u, t_norm_init), params)[:, self.idx] x_irk_points_eq.append(h * f_j - xp_j) # Concatenate constraints x_irk_points = vertcat(*x_irk_points) x_irk_points_eq = vertcat(*x_irk_points_eq) # Root-finding function, implicitly defines x_irk_points as a function of x0 and p vfcn = Function("vfcn", [x_irk_points, x0, u, params], [x_irk_points_eq]).expand() # Create a implicit function instance to solve the system of equations ifcn = rootfinder("ifcn", "newton", vfcn) x_irk_points = ifcn(self.CX(), x0, u, params) x = [x0 if r == 0 else x_irk_points[(r - 1) * nx : r * nx] for r in range(self.degree + 1)] # Get an expression for the state at the end of the finite element xf = self.CX.zeros(nx, self.degree + 1) # 0 # for r in range(self.degree + 1): xf[:, r] = xf[:, r - 1] + D[r] * x[r] return xf[:, -1], horzcat(x0, xf[:, -1])
def _integrate(self, model, t_eval, inputs=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs : dict, optional Any input parameters to pass to the model when solving. If any input parameters that are present in the model are missing from "inputs", then the solution will consist of `ProcessedSymbolicVariable` objects, which must be provided with inputs to obtain their value. """ # Record whether there are any symbolic inputs inputs = inputs or {} has_symbolic_inputs = any( isinstance(v, casadi.MX) for v in inputs.values()) # Create casadi objects for the root-finder inputs = casadi.vertcat(*[x for x in inputs.values()]) y0 = model.y0 # The casadi algebraic solver can read rhs equations, but leaves them unchanged # i.e. the part of the solution vector that corresponds to the differential # equations will be equal to the initial condition provided. This allows this # solver to be used for initialising the DAE solvers if model.rhs == {}: y0_diff = casadi.DM() y0_alg = y0 else: len_rhs = model.concatenated_rhs.size y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] y_alg = None # Set up t_sym = casadi.MX.sym("t") y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) y_sym = casadi.vertcat(y0_diff, y_alg_sym) p_sym = casadi.MX.sym("p", inputs.shape[0]) t_p_sym = casadi.vertcat(t_sym, p_sym) alg = model.casadi_algebraic(t_sym, y_sym, p_sym) # Set up rootfinder roots = casadi.rootfinder( "roots", "newton", dict(x=y_alg_sym, p=t_p_sym, g=alg), { **self.extra_options, "abstol": self.tol }, ) for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it # We can't do this if there are symbolic inputs if has_symbolic_inputs is False and np.all( abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol): pybamm.logger.debug("Keeping same solution at t={}".format( t * model.timescale_eval)) if y_alg is None: y_alg = y0_alg else: y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: t_inputs = casadi.vertcat(t, inputs) # Solve try: y_alg_sol = roots(y0_alg, t_inputs) success = True message = None # Check final output y_sol = casadi.vertcat(y0_diff, y_alg_sol) fun = model.casadi_algebraic(t, y_sol, inputs) except RuntimeError as err: success = False message = err.args[0] fun = None # If there are no symbolic inputs, check the function is below the tol # Skip this check if there are symbolic inputs if success and (has_symbolic_inputs is True or np.all(casadi.fabs(fun) < self.tol)): # update initial guess for the next iteration y0_alg = y_alg_sol # update solution array if y_alg is None: y_alg = y_alg_sol else: y_alg = casadi.horzcat(y_alg, y_alg_sol) elif not success: raise pybamm.SolverError( "Could not find acceptable solution: {}".format( message)) else: raise pybamm.SolverError(""" Could not find acceptable solution: solver terminated successfully, but maximum solution error ({}) above tolerance ({}) """.format(casadi.mmax(fun), self.tol)) # Concatenate differential part y_diff = casadi.horzcat(*[y0_diff] * len(t_eval)) y_sol = casadi.vertcat(y_diff, y_alg) # Return solution object (no events, so pass None to t_event, y_event) return pybamm.Solution(t_eval, y_sol, termination="success")
def _setup_rescaler(self): # find a point where arc length == s * length s_noli = cas.MX.sym('s_noli') fcn = cas.Function('fcn', [self._s, s_noli], [self.arclength(self._s) - s_noli * self.length()]) return cas.rootfinder('r', 'newton', fcn)
def calculate_consistent_state(self, model, time=0, y0_guess=None, inputs=None): """ Calculate consistent state for the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model for which to calculate initial conditions. time : float The time at which to calculate the states y0_guess : :class:`np.array` Guess for the rootfinding inputs : dict, optional Any input parameters to pass to the model when solving Returns ------- y0_consistent : array-like, same shape as y0_guess Initial conditions that are consistent with the algebraic equations (roots of the algebraic equations) """ pybamm.logger.info("Start calculating consistent states") if y0_guess is None: y0_guess = model.concatenated_initial_conditions.flatten() # Split y0_guess into differential and algebraic len_rhs = model.rhs_eval(time, y0_guess).shape[0] y0_diff, y0_alg_guess = np.split(y0_guess, [len_rhs]) inputs = inputs or {} # Solve using casadi or scipy if self.root_method == "casadi": # Set up u_stacked = casadi.vertcat(*[x for x in inputs.values()]) u = casadi.MX.sym("u", u_stacked.shape[0]) y_alg = casadi.MX.sym("y_alg", y0_alg_guess.shape[0]) y = casadi.vertcat(y0_diff, y_alg) alg_root = model.casadi_algebraic(time, y, u) # Solve # set error_on_fail to False and just check the final output is small # enough roots = casadi.rootfinder( "roots", "newton", dict(x=y_alg, p=u, g=alg_root), {"abstol": self.root_tol}, ) try: y0_alg = roots(y0_alg_guess, u_stacked).full().flatten() success = True message = None # Check final output fun = model.casadi_algebraic(time, casadi.vertcat(y0_diff, y0_alg), u_stacked) except RuntimeError as err: success = False message = err.args[0] fun = None else: algebraic = model.algebraic_eval jac = model.jac_algebraic_eval def root_fun(y0_alg): "Evaluates algebraic using y0_diff (fixed) and y0_alg (changed by algo)" y0 = np.concatenate([y0_diff, y0_alg]) out = algebraic(time, y0) pybamm.logger.debug( "Evaluating algebraic equations at t={}, L2-norm is {}". format(time * model.timescale, np.linalg.norm(out))) return out if jac: if issparse(jac(0, y0_guess)): def jac_fn(y0_alg): """ Evaluates jacobian using y0_diff (fixed) and y0_alg (varying) """ y0 = np.concatenate([y0_diff, y0_alg]) return jac(0, y0)[:, len_rhs:].toarray() else: def jac_fn(y0_alg): """ Evaluates jacobian using y0_diff (fixed) and y0_alg (varying) """ y0 = np.concatenate([y0_diff, y0_alg]) return jac(0, y0)[:, len_rhs:] else: jac_fn = None # Find the values of y0_alg that are roots of the algebraic equations sol = optimize.root( root_fun, y0_alg_guess, jac=jac_fn, method=self.root_method, tol=self.root_tol, ) pybamm.citations.register("virtanen2020scipy") # Set outputs y0_alg = sol.x success = sol.success fun = sol.fun message = sol.message if success and np.all(fun < self.root_tol * len(y0_alg)): # Return full set of consistent initial conditions (y0_diff unchanged) y0_consistent = np.concatenate([y0_diff, y0_alg]) pybamm.logger.info( "Finish calculating consistent initial conditions") return y0_consistent elif not success: raise pybamm.SolverError( "Could not find consistent initial conditions: {}".format( message)) else: raise pybamm.SolverError(""" Could not find consistent initial conditions: solver terminated successfully, but maximum solution error ({}) above tolerance ({}) """.format(np.max(fun), self.root_tol * len(y0_alg)))
def _integrate(self, model, t_eval, inputs=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs : dict, optional Any input parameters to pass to the model when solving """ y0 = model.y0 y = np.empty((len(y0), len(t_eval))) # Set up inputs = casadi.vertcat(*[x for x in inputs.values()]) t_sym = casadi.MX.sym("t") y_sym = casadi.MX.sym("y_alg", y0.shape[0]) p_sym = casadi.MX.sym("p", inputs.shape[0]) t_p_sym = casadi.vertcat(t_sym, p_sym) alg = model.casadi_algebraic(t_sym, y_sym, p_sym) # Set up rootfinder roots = casadi.rootfinder( "roots", "newton", dict(x=y_sym, p=t_p_sym, g=alg), { **self.extra_options, "abstol": self.tol }, ) for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it if np.all(abs(model.algebraic_eval(t, y0, inputs)) < self.tol): pybamm.logger.debug("Keeping same solution at t={}".format( t * model.timescale_eval)) y[:, idx] = y0 # Otherwise calculate new y0 else: t_inputs = casadi.vertcat(t, inputs) # Solve try: y_sol = roots(y0, t_inputs).full().flatten() success = True message = None # Check final output fun = model.casadi_algebraic(t, y_sol, inputs) except RuntimeError as err: success = False message = err.args[0] fun = None if success and np.all(casadi.fabs(fun) < self.tol): # update initial guess for the next iteration y0 = y_sol # update solution array y[:, idx] = y_sol elif not success: raise pybamm.SolverError( "Could not find acceptable solution: {}".format( message)) else: raise pybamm.SolverError(""" Could not find acceptable solution: solver terminated successfully, but maximum solution error ({}) above tolerance ({}) """.format(casadi.mmax(fun), self.tol)) # Return solution object (no events, so pass None to t_event, y_event) return pybamm.Solution(t_eval, y, termination="success")
H[node_uid_to_index[pipe.downstream_node.uid]]) / L[n] - gamma[n] / L[n] * labda[n] * (ca.sqrt(Q_nom[n]**2 + smpa_a**2) + b[n] + c[n] / ca.sqrt(Q_nom[n]**2 + smpa_d**2)) * Q[n]) * (1 - theta) + ((H[node_uid_to_index[pipe.upstream_node.uid]] - H[node_uid_to_index[pipe.downstream_node.uid]]) / L[n] - gamma[n] / L[n] * labda[n] * (ca.sqrt(Q[n]**2 + smpa_a**2) + b[n] + c[n] / ca.sqrt(Q[n]**2 + smpa_d**2)) * Q[n]) * theta) # Mass conservation equations each node if pipe.upstream_node.uid in mc: mc[pipe.upstream_node.uid] -= Q[n] if pipe.downstream_node.uid in mc: mc[pipe.downstream_node.uid] += Q[n] # Root finder START = ca.vertcat(*start) EQ = ca.vertcat(*eq) MC = ca.vertcat(*mc.values()) f = ca.Function("f", [ca.vertcat(Q, H)], [ca.vertcat(START, EQ, MC)]) F = ca.rootfinder("F", "newton", f) sol = F(sol) print(sol) np.savetxt( "HomotopyH_ref.csv", sol[(len(network.pipes)):((len(network.pipes)) + (len(network.nodes)))], )
def __init__(self, **kwargs): # Check arguments assert ('model_folder' in kwargs) # Log pymoca version logger.debug("Using pymoca {}.".format(pymoca.__version__)) # Transfer model from the Modelica .mo file to CasADi using pymoca if 'model_name' in kwargs: model_name = kwargs['model_name'] else: if hasattr(self, 'model_name'): model_name = self.model_name else: model_name = self.__class__.__name__ # Load model from pymoca backend self.__pymoca_model = pymoca.backends.casadi.api.transfer_model( kwargs['model_folder'], model_name, self.compiler_options()) # Extract the CasADi MX variables used in the model self.__mx = {} self.__mx['time'] = [self.__pymoca_model.time] self.__mx['states'] = [v.symbol for v in self.__pymoca_model.states] self.__mx['derivatives'] = [ v.symbol for v in self.__pymoca_model.der_states ] self.__mx['algebraics'] = [ v.symbol for v in self.__pymoca_model.alg_states ] self.__mx['parameters'] = [ v.symbol for v in self.__pymoca_model.parameters ] self.__mx['constant_inputs'] = [] self.__mx['lookup_tables'] = [] # TODO: implement delayed feedback delayed_feedback_variables = [] for v in self.__pymoca_model.inputs: if v.symbol.name() in delayed_feedback_variables: # Delayed feedback variables are local to each ensemble, and # therefore belong to the collection of algebraic variables, # rather than to the control inputs. self.__mx['algebraics'].append(v.symbol) else: if v.symbol.name() in kwargs.get('lookup_tables', []): self.__mx['lookup_tables'].append(v.symbol) else: # All inputs are constant inputs self.__mx['constant_inputs'].append(v.symbol) # Log variables in debug mode if logger.getEffectiveLevel() == logging.DEBUG: logger.debug("SimulationProblem: Found states {}".format(', '.join( [var.name() for var in self.__mx['states']]))) logger.debug("SimulationProblem: Found derivatives {}".format( ', '.join([var.name() for var in self.__mx['derivatives']]))) logger.debug("SimulationProblem: Found algebraics {}".format( ', '.join([var.name() for var in self.__mx['algebraics']]))) logger.debug("SimulationProblem: Found constant inputs {}".format( ', '.join([var.name() for var in self.__mx['constant_inputs']]))) logger.debug("SimulationProblem: Found parameters {}".format( ', '.join([var.name() for var in self.__mx['parameters']]))) # Initialize an AliasDict for nominals and types self.__nominals = AliasDict(self.alias_relation) self.__python_types = AliasDict(self.alias_relation) for v in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states, self.__pymoca_model.inputs): sym_name = v.symbol.name() # Store the types in an AliasDict self.__python_types[sym_name] = v.python_type # If the nominal is 0.0 or 1.0 or -1.0, ignore: get_variable_nominal returns a default of 1.0 # TODO: handle nominal vectors (update() will need to load them) if ca.MX(v.nominal).is_zero() or ca.MX( v.nominal - 1).is_zero() or ca.MX(v.nominal + 1).is_zero(): continue else: if ca.MX(v.nominal).size1() != 1: logger.error( 'Vector Nominals not supported yet. ({})'.format( sym_name)) self.__nominals[sym_name] = ca.fabs(v.nominal) if logger.getEffectiveLevel() == logging.DEBUG: logger.debug( "SimulationProblem: Setting nominal value for variable {} to {}" .format(sym_name, self.__nominals[sym_name])) # Initialize DAE and initial residuals variable_lists = [ 'states', 'der_states', 'alg_states', 'inputs', 'constants', 'parameters' ] function_arguments = [self.__pymoca_model.time] + [ ca.veccat(*[ v.symbol for v in getattr(self.__pymoca_model, variable_list) ]) for variable_list in variable_lists ] self.__dae_residual = self.__pymoca_model.dae_residual_function( *function_arguments) self.__initial_residual = self.__pymoca_model.initial_residual_function( *function_arguments) if self.__initial_residual is None: self.__initial_residual = ca.MX() # Construct state vector self.__sym_list = self.__mx['states'] + self.__mx['algebraics'] + self.__mx['derivatives'] + \ self.__mx['time'] + self.__mx['constant_inputs'] + self.__mx['parameters'] self.__state_vector = np.full(len(self.__sym_list), np.nan) # A very handy index self.__states_end_index = len(self.__mx['states']) + \ len(self.__mx['algebraics']) + len(self.__mx['derivatives']) # Construct a dict to look up symbols by name (or iterate over) self.__sym_dict = OrderedDict( ((sym.name(), sym) for sym in self.__sym_list)) # Assemble some symbolics, including those needed for a backwards Euler derivative approximation X = ca.vertcat(*self.__sym_list[:self.__states_end_index]) X_prev = ca.vertcat(*[ ca.MX.sym(sym.name() + '_prev') for sym in self.__sym_list[:self.__states_end_index] ]) dt = ca.MX.sym("delta_t") # Make a list of derivative approximations using backwards Euler formulation derivative_approximation_residuals = [] for index, derivative_state in enumerate(self.__mx['derivatives']): derivative_approximation_residuals.append( derivative_state - (X[index] - X_prev[index]) / dt) # Append residuals for derivative approximations dae_residual = ca.vertcat(self.__dae_residual, *derivative_approximation_residuals) # TODO: implement lookup_tables # Make a list of unscaled symbols and a list of their scaled equivalent unscaled_symbols = [] scaled_symbols = [] for sym_name, nominal in self.__nominals.items(): index = self.__get_state_vector_index(sym_name) # If the symbol is a state, Add the symbol to the lists if index <= self.__states_end_index: unscaled_symbols.append(X[index]) scaled_symbols.append(X[index] * nominal) # Also scale previous states unscaled_symbols.append(X_prev[index]) scaled_symbols.append(X_prev[index] * nominal) # Substitute unscaled terms for scaled terms dae_residual = ca.substitute(dae_residual, ca.vertcat(*unscaled_symbols), ca.vertcat(*scaled_symbols)) if logger.getEffectiveLevel() == logging.DEBUG: logger.debug('SimulationProblem: DAE Residual is ' + str(dae_residual)) if X.size1() != dae_residual.size1(): logger.error( 'Formulation Error: Number of states ({}) does not equal number of equations ({})' .format(X.size1(), dae_residual.size1())) # Construct function parameters parameters = ca.vertcat(dt, X_prev, *self.__sym_list[self.__states_end_index:]) # Construct a function res_vals that returns the numerical residuals of a numerical state self.__res_vals = ca.Function("res_vals", [X, parameters], [dae_residual]) # Use rootfinder() to make a function that takes a step forward in time by trying to zero res_vals() options = {'nlpsol': 'ipopt', 'nlpsol_options': self.solver_options()} self.__do_step = ca.rootfinder("next_state", "nlpsol", self.__res_vals, options) # Call parent class for default behaviour. super().__init__()