Exemple #1
0
    def fit(cls, x, y, k=3, monotonicity=0, curvature=0,
            num_test_points=100, epsilon=1e-7, delta=1e-4, interior_pts=None):
        """
        fit() returns a tck tuple like scipy.interpolate.splrep, but adjusts
        the weights to meet the desired constraints to the curvature of the spline curve.

        :param monotonicity:
            - is an integer, magnitude is ignored
            - if positive, causes spline to be monotonically increasing
            - if negative, causes spline to be monotonically decreasing
            - if 0, leaves spline monotonicity unconstrained

        :param curvature:
            - is an integer, magnitude is ignored
            - if positive, causes spline curvature to be positive (convex)
            - if negative, causes spline curvature to be negative (concave)
            - if 0, leaves spline curvature unconstrained

        :param num_test_points:
            - sets the number of points that the constraints will be applied at across
              the range of the spline

        :param epsilon:
            - offset of monotonicity and curvature constraints from zero, ensuring strict
              monotonicity
            - if epsilon is set to less than the tolerance of the solver, errors will result

        :param delta:
            - amount the first and last knots are extended outside the range of the splined points
            - ensures that the spline evaluates correctly at the first and last nodes, as
              well as the distance delta beyond these nodes

        :param interior_pts:
            - optional list of interior knots to use

        :returns: A tuple of spline knots, weights, and order.
        """
        x = np.asarray(x)
        y = np.asarray(y)
        N = len(x)

        if interior_pts is None:
            # Generate knots: This algorithm is based on the Fitpack algorithm by p.dierckx
            # The original code lives here: http://www.netlib.org/dierckx/
            if k % 2 == 1:
                interior_pts = x[k // 2 + 1:-k // 2]
            else:
                interior_pts = (x[k // 2 + 1:-k // 2] + x[k // 2:-k // 2 - 1]) / 2
        t = np.concatenate(
            (np.full(k + 1, x[0] - delta), interior_pts, np.full(k + 1, x[-1] + delta)))
        num_knots = len(t)

        # Casadi Variable Symbols
        c = SX.sym('c', num_knots)
        x_sym = SX.sym('x')

        # Casadi Representation of Spline Function & Derivatives
        expr = cls(t, c, k)(x_sym)
        free_vars = [c, x_sym]
        bspline = Function('bspline', free_vars, [expr])
        J = jacobian(expr, x_sym)
        # bspline_prime = Function('bspline_prime', free_vars, [J])
        H = jacobian(J, x_sym)
        bspline_prime_prime = Function('bspline_prime_prime', free_vars, [H])

        # Objective Function
        xpt = SX.sym('xpt')
        ypt = SX.sym('ypt')
        sq_diff = Function('sq_diff', [xpt, ypt], [
                             (ypt - bspline(c, xpt))**2])
        sq_diff = sq_diff.map(N, 'serial')
        f = sum2(sq_diff(SX(x), SX(y)))

        # Setup Curvature Constraints
        delta_c_max = np.full(num_knots - 1, inf)
        delta_c_min = np.full(num_knots - 1, -inf)
        max_slope_slope = np.full(num_test_points, inf)
        min_slope_slope = np.full(num_test_points, -inf)
        if monotonicity != 0:
            if monotonicity < 0:
                delta_c_max = np.full(num_knots - 1, -epsilon)
            else:
                delta_c_min = np.full(num_knots - 1, epsilon)
        if curvature != 0:
            if curvature < 0:
                max_slope_slope = np.full(num_test_points, -epsilon)
            else:
                min_slope_slope = np.full(num_test_points, epsilon)
        monotonicity_constraints = vertcat(*[
            c[i + 1] - c[i] for i in range(num_knots - 1)])
        x_linspace = np.linspace(x[0], x[-1], num_test_points)
        curvature_constraints = vertcat(*[
            bspline_prime_prime(c, SX(x)) for x in x_linspace])
        g = vertcat(monotonicity_constraints, curvature_constraints)
        lbg = np.concatenate((delta_c_min, min_slope_slope))
        ubg = np.concatenate((delta_c_max, max_slope_slope))

        # Perform mini-optimization problem to calculate the the values of c
        nlp = {'x': c, 'f': f, 'g': g}
        my_solver = "ipopt"
        solver = nlpsol("solver", my_solver, nlp, {'print_time': 0, 'expand': True, 'ipopt': {'print_level': 0}})
        sol = solver(lbg=lbg, ubg=ubg)
        stats = solver.stats()
        return_status = stats['return_status']
        if return_status not in ['Solve_Succeeded', 'Solved_To_Acceptable_Level', 'SUCCESS']:
            raise Exception("Spline fitting failed with status {}".format(return_status))

        # Return the new tck tuple
        return (t, np.array(sol['x']).ravel(), k)
Exemple #2
0
class PenaltyOption(OptionGeneric):
    """
    A placeholder for a penalty

    Attributes
    ----------
    node: Node
        The node within a phase on which the penalty is acting on
    quadratic: bool
        If the penalty is quadratic
    rows: Union[list, tuple, range, np.ndarray]
        The index of the rows in the penalty to keep
    cols: Union[list, tuple, range, np.ndarray]
        The index of the columns in the penalty to keep
    expand: bool
        If the penalty should be expanded or not
    target: np.array(target)
        A target to track for the penalty
    target_plot_name: str
        The plot name of the target
    target_to_plot: np.ndarray
        The subset of the target to plot
    plot_target: bool
        If the target should be plotted
    custom_function: Callable
        A user defined function to call to get the penalty
    node_idx: Union[list, tuple, Node]
        The index in nlp to apply the penalty to
    dt: float
        The delta time
    function: Function
        The casadi function of the penalty
    weighted_function: Function
        The casadi function of the penalty weighted
    derivative: bool
        If the minimization is applied on the numerical derivative of the state [f(t+1) - f(t)]
    explicit_derivative: bool
        If the minimization is applied to derivative of the penalty [f(t, t+1)]
    transition: bool
        If the penalty is a transition
    phase_pre_idx: int
        The index of the nlp of pre when penalty is transition
    phase_post_idx: int
        The index of the nlp of post when penalty is transition
    is_internal: bool
        If the penalty is from the user or from bioptim
    multi_thread: bool
        If the penalty is multithreaded

    Methods
    -------
    set_penalty(self, penalty: Union[MX, SX], all_pn: PenaltyNodeList)
        Prepare the dimension and index of the penalty (including the target)
    _set_dim_idx(self, dim: Union[list, tuple, range, np.ndarray], n_rows: int)
        Checks if the variable index is consistent with the requested variable.
    _check_target_dimensions(self, all_pn: PenaltyNodeList, n_time_expected: int)
        Checks if the variable index is consistent with the requested variable.
        If the function returns, all is okay
    _set_penalty_function(self, all_pn: Union[PenaltyNodeList, list, tuple], fcn: Union[MX, SX])
        Finalize the preparation of the penalty (setting function and weighted_function)
    add_target_to_plot(self, all_pn: PenaltyNodeList, combine_to: str)
        Interface to the plot so it can be properly added to the proper plot
    _finish_add_target_to_plot(self, all_pn: PenaltyNodeList)
        Internal interface to add (after having check the target dimensions) the target to the plot if needed
    add_or_replace_to_penalty_pool(self, ocp, nlp)
        Doing some configuration on the penalty and add it to the list of penalty
    _add_penalty_to_pool(self, all_pn: PenaltyNodeList)
        Return the penalty pool for the specified penalty (abstract)
    clear_penalty(self, ocp, nlp)
        Resets a penalty. A negative penalty index creates a new empty penalty (abstract)
    _get_penalty_node_list(self, ocp, nlp) -> PenaltyNodeList
        Get the actual node (time, X and U) specified in the penalty
    """

    def __init__(
        self,
        penalty: Any,
        phase: int = 0,
        node: Union[Node, list, tuple] = Node.DEFAULT,
        target: np.ndarray = None,
        quadratic: bool = None,
        weight: float = 1,
        derivative: bool = False,
        explicit_derivative: bool = False,
        integrate: bool = False,
        index: list = None,
        rows: Union[list, tuple, range, np.ndarray] = None,
        cols: Union[list, tuple, range, np.ndarray] = None,
        states_mapping: BiMapping = None,
        custom_function: Callable = None,
        is_internal: bool = False,
        multi_thread: bool = None,
        expand: bool = False,
        **params: Any,
    ):
        """
        Parameters
        ----------
        penalty: PenaltyType
            The actual penalty
        phase: int
            The phase the penalty is acting on
        node: Union[Node, list, tuple]
            The node within a phase on which the penalty is acting on
        target: np.ndarray
            A target to track for the penalty
        quadratic: bool
            If the penalty is quadratic
        weight: float
            The weighting applied to this specific penalty
        derivative: bool
            If the function should be evaluated at X and X+1
        explicit_derivative: bool
            If the function should be evaluated at [X, X+1]
        index: int
            The component index the penalty is acting on
        custom_function: Callable
            A user defined function to call to get the penalty
        is_internal: bool
            If the penalty is internally defined [True] or by the user
        **params: dict
            Generic parameters for the penalty
        """

        super(PenaltyOption, self).__init__(phase=phase, type=penalty, **params)
        self.node: Union[Node, list, tuple] = node
        self.quadratic = quadratic

        if index is not None and rows is not None:
            raise ValueError("rows and index cannot be defined simultaneously since they are the same variable")
        self.rows = rows if rows is not None else index
        self.cols = cols
        self.expand = expand

        self.target = None
        if target is not None:
            self.target = np.array(target)
            if len(self.target.shape) == 0:
                self.target = self.target[np.newaxis]
            if len(self.target.shape) == 1:
                self.target = self.target[:, np.newaxis]
        self.target_plot_name = None
        self.target_to_plot = None
        self.plot_target = True

        self.states_mapping = states_mapping

        self.custom_function = custom_function

        self.node_idx = []
        self.dt = 0
        self.weight = weight
        self.function: Union[Function, None] = None
        self.weighted_function: Union[Function, None] = None
        self.weighted_function_non_threaded: Union[Function, None] = None
        self.derivative = derivative
        self.explicit_derivative = explicit_derivative
        self.integrate = integrate
        self.transition = False
        self.phase_pre_idx = None
        self.phase_post_idx = None
        if self.derivative and self.explicit_derivative:
            raise ValueError("derivative and explicit_derivative cannot be both True")
        self.is_internal = is_internal

        self.multi_thread = multi_thread

    def set_penalty(self, penalty: Union[MX, SX], all_pn: PenaltyNodeList):
        """
        Prepare the dimension and index of the penalty (including the target)

        Parameters
        ----------
        penalty: Union[MX, SX],
            The actual penalty function
        all_pn: PenaltyNodeList
            The penalty node elements
        """

        self.rows = self._set_dim_idx(self.rows, penalty.rows())
        self.cols = self._set_dim_idx(self.cols, penalty.columns())
        if self.target is not None:
            self._check_target_dimensions(all_pn, len(all_pn.t))
            if self.plot_target:
                self._finish_add_target_to_plot(all_pn)
        self._set_penalty_function(all_pn, penalty)
        self._add_penalty_to_pool(all_pn)

    def _set_dim_idx(self, dim: Union[list, tuple, range, np.ndarray], n_rows: int):
        """
        Checks if the variable index is consistent with the requested variable.

        Parameters
        ----------
        dim: Union[list, tuple, range]
            The dimension to set
        n_rows: int
            The expected row shape

        Returns
        -------
        The formatted indices
        """

        if dim is None:
            dim = range(n_rows)
        else:
            if isinstance(dim, int):
                dim = [dim]
            if max(dim) > n_rows:
                raise RuntimeError(f"{self.name} index cannot be higher than nx ({n_rows})")
        dim = np.array(dim)
        if not np.issubdtype(dim.dtype, np.integer):
            raise RuntimeError(f"{self.name} index must be a list of integer")
        return dim

    def _check_target_dimensions(self, all_pn: PenaltyNodeList, n_time_expected: int):
        """
        Checks if the variable index is consistent with the requested variable.
        If the function returns, all is okay

        Parameters
        ----------
        all_pn: PenaltyNodeList
            The penalty node elements
        n_time_expected: Union[list, tuple]
            The expected shape (n_rows, ns) of the data to track
        """

        n_dim = len(self.target.shape)
        if n_dim != 2 and n_dim != 3:
            raise RuntimeError(f"target cannot be a vector (it can be a matrix with time dimension equals to 1 though)")
        if self.target.shape[-1] == 1:
            self.target = np.repeat(self.target, n_time_expected, axis=-1)

        shape = (len(self.rows), n_time_expected) if n_dim == 2 else (len(self.rows), len(self.cols), n_time_expected)
        if self.target.shape != shape:
            raise RuntimeError(
                f"target {self.target.shape} does not correspond to expected size {shape} for penalty {self.name}"
            )

        # If the target is on controls and control is constant, there will be one value missing
        if all_pn is not None:
            if (
                all_pn.nlp.control_type == ControlType.CONSTANT
                and all_pn.nlp.ns in all_pn.t
                and self.target.shape[-1] == all_pn.nlp.ns
            ):
                if all_pn.t[-1] != all_pn.nlp.ns:
                    raise NotImplementedError("Modifying target for END not being last is not implemented yet")
                self.target = np.concatenate((self.target, np.nan * np.zeros((self.target.shape[0], 1))), axis=1)

    def _set_penalty_function(self, all_pn: Union[PenaltyNodeList, list, tuple], fcn: Union[MX, SX]):
        """
        Finalize the preparation of the penalty (setting function and weighted_function)

        Parameters
        ----------
        all_pn: PenaltyNodeList
            The nodes
        fcn: Union[MX, SX]
            The value of the penalty function
        """

        # Sanity checks
        if self.transition and self.explicit_derivative:
            raise ValueError("transition and explicit_derivative cannot be true simultaneously")
        if self.transition and self.derivative:
            raise ValueError("transition and derivative cannot be true simultaneously")
        if self.derivative and self.explicit_derivative:
            raise ValueError("derivative and explicit_derivative cannot be true simultaneously")

        if self.transition:
            ocp = all_pn[0].ocp
            nlp = all_pn[0].nlp
            nlp_post = all_pn[1].nlp
            name = self.name.replace("->", "_").replace(" ", "_")
            states_pre = nlp.states.cx_end
            states_post = nlp_post.states.cx
            controls_pre = nlp.controls.cx_end
            controls_post = nlp_post.controls.cx
            state_cx = vertcat(states_pre, states_post)
            control_cx = vertcat(controls_pre, controls_post)

        else:
            ocp = all_pn.ocp
            nlp = all_pn.nlp
            name = self.name
            if self.integrate:
                state_cx = horzcat(*([all_pn.nlp.states.cx] + all_pn.nlp.states.cx_intermediates_list))
                control_cx = all_pn.nlp.controls.cx
            else:
                state_cx = all_pn.nlp.states.cx
                control_cx = all_pn.nlp.controls.cx
            if self.explicit_derivative:
                if self.derivative:
                    raise RuntimeError("derivative and explicit_derivative cannot be simultaneously true")
                state_cx = horzcat(state_cx, all_pn.nlp.states.cx_end)
                control_cx = horzcat(control_cx, all_pn.nlp.controls.cx_end)

        param_cx = nlp.cx(nlp.parameters.cx)

        # Do not use nlp.add_casadi_func because all functions must be registered
        self.function = biorbd.to_casadi_func(
            name, fcn[self.rows, self.cols], state_cx, control_cx, param_cx, expand=self.expand
        )
        if self.derivative:
            state_cx = horzcat(all_pn.nlp.states.cx_end, all_pn.nlp.states.cx)
            control_cx = horzcat(all_pn.nlp.controls.cx_end, all_pn.nlp.controls.cx)
            self.function = biorbd.to_casadi_func(
                f"{name}",
                self.function(all_pn.nlp.states.cx_end, all_pn.nlp.controls.cx_end, param_cx)
                - self.function(all_pn.nlp.states.cx, all_pn.nlp.controls.cx, param_cx),
                state_cx,
                control_cx,
                param_cx,
            )

        modified_fcn = self.function(state_cx, control_cx, param_cx)

        dt_cx = nlp.cx.sym("dt", 1, 1)
        weight_cx = nlp.cx.sym("weight", 1, 1)
        target_cx = nlp.cx.sym("target", modified_fcn.shape)
        modified_fcn = modified_fcn - target_cx

        if self.weight:
            modified_fcn = modified_fcn ** 2 if self.quadratic else modified_fcn
            modified_fcn = weight_cx * modified_fcn * dt_cx
        else:
            modified_fcn = modified_fcn * dt_cx

        # Do not use nlp.add_casadi_func because all of them must be registered
        self.weighted_function = Function(
            name, [state_cx, control_cx, param_cx, weight_cx, target_cx, dt_cx], [modified_fcn]
        )
        self.weighted_function_non_threaded = self.weighted_function

        if ocp.n_threads > 1 and self.multi_thread and len(self.node_idx) > 1:
            self.function = self.function.map(len(self.node_idx), "thread", ocp.n_threads)
            self.weighted_function = self.weighted_function.map(len(self.node_idx), "thread", ocp.n_threads)
        else:
            self.multi_thread = False  # Override the multi_threading, since only one node is optimized

        if self.expand:
            self.function = self.function.expand()
            self.weighted_function = self.weighted_function.expand()

    def add_target_to_plot(self, all_pn: PenaltyNodeList, combine_to: str):
        """
        Interface to the plot so it can be properly added to the proper plot

        Parameters
        ----------
        all_pn: PenaltyNodeList
            The penalty node elements
        combine_to: str
            The name of the underlying plot to combine the tracking data to
        """

        if self.target is None or combine_to is None:
            return

        self.target_plot_name = combine_to
        if self.target.shape[1] == all_pn.nlp.ns:
            self.target_to_plot = np.concatenate((self.target, np.nan * np.ndarray((self.target.shape[0], 1))), axis=1)
        else:
            self.target_to_plot = self.target

    def _finish_add_target_to_plot(self, all_pn: PenaltyNodeList):
        """
        Internal interface to add (after having check the target dimensions) the target to the plot if needed

        Parameters
        ----------
        all_pn: PenaltyNodeList
            The penalty node elements

        """

        def plot_function(t, x, u, p):
            if isinstance(t, (list, tuple)):
                return self.target_to_plot[:, [self.node_idx.index(_t) for _t in t]]
            else:
                return self.target_to_plot[:, self.node_idx.index(t)]

        if self.target_to_plot is not None:
            if self.target_to_plot.shape[1] > 1:
                plot_type = PlotType.STEP
            else:
                plot_type = PlotType.POINT

            all_pn.ocp.add_plot(
                self.target_plot_name,
                plot_function,
                color="tab:red",
                plot_type=plot_type,
                phase=all_pn.nlp.phase_idx,
                axes_idx=Mapping(self.rows),
                node_idx=self.node_idx,
            )

    def add_or_replace_to_penalty_pool(self, ocp, nlp):
        """
        Doing some configuration on the penalty and add it to the list of penalty

        Parameters
        ----------
        ocp: OptimalControlProgram
            A reference to the ocp
        nlp: NonLinearProgram
            A reference to the current phase of the ocp
        """
        if not self.name:
            if self.type.name == "CUSTOM":
                self.name = self.custom_function.__name__
            else:
                self.name = self.type.name

        penalty_type = self.type.get_type()
        if self.node == Node.TRANSITION:
            all_pn = []

            # Make sure the penalty behave like a PhaseTransition, even though it may be an Objective or Constraint
            self.node = Node.END
            self.node_idx = [0]
            self.transition = True
            self.dt = 1
            self.phase_pre_idx = nlp.phase_idx
            self.phase_post_idx = (nlp.phase_idx + 1) % ocp.n_phases
            if not self.states_mapping:
                self.states_mapping = BiMapping(range(nlp.states.shape), range(nlp.states.shape))

            all_pn.append(self._get_penalty_node_list(ocp, nlp))
            all_pn[0].u = [nlp.U[-1]]  # Make an exception to the fact that U is not available for the last node

            nlp = ocp.nlp[(nlp.phase_idx + 1) % ocp.n_phases]
            self.node = Node.START
            all_pn.append(self._get_penalty_node_list(ocp, nlp))

            self.node = Node.TRANSITION

            penalty_type.validate_penalty_time_index(self, all_pn[0])
            penalty_type.validate_penalty_time_index(self, all_pn[1])
            self.clear_penalty(ocp, all_pn[0].nlp)

        else:
            all_pn = self._get_penalty_node_list(ocp, nlp)
            penalty_type.validate_penalty_time_index(self, all_pn)
            self.clear_penalty(all_pn.ocp, all_pn.nlp)
            self.dt = penalty_type.get_dt(all_pn.nlp)
            self.node_idx = all_pn.t

        penalty_function = self.type.value[0](self, all_pn, **self.params)
        self.set_penalty(penalty_function, all_pn)

    def _add_penalty_to_pool(self, all_pn: PenaltyNodeList):
        """
        Return the penalty pool for the specified penalty (abstract)

        Parameters
        ----------
        all_pn: PenaltyNodeList
            The penalty node elements
        """

        raise RuntimeError("get_dt cannot be called from an abstract class")

    def clear_penalty(self, ocp, nlp):
        """
        Resets a penalty. A negative penalty index creates a new empty penalty (abstract)

        Parameters
        ----------
        ocp: OptimalControlProgram
            A reference to the ocp
        nlp: NonLinearProgram
            A reference to the current phase of the ocp
        """

        raise RuntimeError("_reset_penalty cannot be called from an abstract class")

    def _get_penalty_node_list(self, ocp, nlp) -> PenaltyNodeList:
        """
        Get the actual node (time, X and U) specified in the penalty

        Parameters
        ----------
        ocp: OptimalControlProgram
            A reference to the ocp
        nlp: NonLinearProgram
            A reference to the current phase of the ocp

        Returns
        -------
        The actual node (time, X and U) specified in the penalty
        """

        if not isinstance(self.node, (list, tuple)):
            self.node = (self.node,)

        t = []
        for node in self.node:
            if isinstance(node, int):
                if node < 0 or node > nlp.ns:
                    raise RuntimeError(f"Invalid node, {node} must be between 0 and {nlp.ns}")
                t.append(node)
            elif node == Node.START:
                t.append(0)
            elif node == Node.MID:
                if nlp.ns % 2 == 1:
                    raise (ValueError("Number of shooting points must be even to use MID"))
                t.append(nlp.ns // 2)
            elif node == Node.INTERMEDIATES:
                t.extend(list(i for i in range(1, nlp.ns - 1)))
            elif node == Node.PENULTIMATE:
                if nlp.ns < 2:
                    raise (ValueError("Number of shooting points must be greater than 1"))
                t.append(nlp.ns - 1)
            elif node == Node.END:
                t.append(nlp.ns)
            elif node == Node.ALL_SHOOTING:
                t.extend(range(nlp.ns))
            elif node == Node.ALL:
                t.extend(range(nlp.ns + 1))
            else:
                raise RuntimeError(" is not a valid node")

        x = [nlp.X[idx] for idx in t]
        u = [nlp.U[idx] for idx in t if idx != nlp.ns]
        return PenaltyNodeList(ocp, nlp, t, x, u, nlp.parameters.cx)