Beispiel #1
0
    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
Beispiel #2
0
    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:],
        )
Beispiel #3
0
    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])
Beispiel #4
0
    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))
Beispiel #5
0
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
Beispiel #6
0
    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
Beispiel #7
0
    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)))
Beispiel #8
0
    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
Beispiel #9
0
    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")
Beispiel #11
0
 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)
Beispiel #12
0
    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")
Beispiel #14
0
                  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)))],
)
Beispiel #15
0
    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__()