diff --git a/docs/_autosummary/pycollo.optimal_control_problem.OptimalControlProblem.rst b/docs/_autosummary/pycollo.optimal_control_problem.OptimalControlProblem.rst new file mode 100644 index 0000000..70960c9 --- /dev/null +++ b/docs/_autosummary/pycollo.optimal_control_problem.OptimalControlProblem.rst @@ -0,0 +1,55 @@ +.. + class.rst + +pycollo.optimal\_control\_problem.OptimalControlProblem +======================================================= + +.. currentmodule:: pycollo.optimal_control_problem + +.. autoclass:: OptimalControlProblem + :members: + :show-inheritance: + :inherited-members: + :special-members: + :exclude-members: __weakref__ + + + + .. rubric:: Attributes + + .. autosummary:: + + ~OptimalControlProblem.auxiliary_data + ~OptimalControlProblem.bounds + ~OptimalControlProblem.endpoint_constraints + ~OptimalControlProblem.guess + ~OptimalControlProblem.mesh_iterations + ~OptimalControlProblem.name + ~OptimalControlProblem.num_mesh_iterations + ~OptimalControlProblem.number_endpoint_constraints + ~OptimalControlProblem.number_parameter_variables + ~OptimalControlProblem.number_phases + ~OptimalControlProblem.objective_function + ~OptimalControlProblem.parameter_variables + ~OptimalControlProblem.phases + ~OptimalControlProblem.scaling + ~OptimalControlProblem.settings + ~OptimalControlProblem.solution + ~OptimalControlProblem.time_symbol + + + .. rubric:: Methods + + .. autosummary:: + :nosignatures: + + ~OptimalControlProblem.__init__ + ~OptimalControlProblem.add_phase + ~OptimalControlProblem.add_phases + ~OptimalControlProblem.initialise + ~OptimalControlProblem.new_phase + ~OptimalControlProblem.new_phase_like + ~OptimalControlProblem.new_phases_like + ~OptimalControlProblem.solve + + \ No newline at end of file diff --git a/docs/_autosummary/pycollo.optimal_control_problem.cout.rst b/docs/_autosummary/pycollo.optimal_control_problem.cout.rst new file mode 100644 index 0000000..7da63b5 --- /dev/null +++ b/docs/_autosummary/pycollo.optimal_control_problem.cout.rst @@ -0,0 +1,9 @@ +.. + base.rst + +pycollo.optimal\_control\_problem.cout +====================================== + +.. currentmodule:: pycollo.optimal_control_problem + +.. autofunction:: cout \ No newline at end of file diff --git a/docs/_autosummary/pycollo.optimal_control_problem.kill.rst b/docs/_autosummary/pycollo.optimal_control_problem.kill.rst new file mode 100644 index 0000000..3ebc802 --- /dev/null +++ b/docs/_autosummary/pycollo.optimal_control_problem.kill.rst @@ -0,0 +1,9 @@ +.. + base.rst + +pycollo.optimal\_control\_problem.kill +====================================== + +.. currentmodule:: pycollo.optimal_control_problem + +.. autofunction:: kill \ No newline at end of file diff --git a/docs/_autosummary/pycollo.optimal_control_problem.rst b/docs/_autosummary/pycollo.optimal_control_problem.rst new file mode 100644 index 0000000..bb23450 --- /dev/null +++ b/docs/_autosummary/pycollo.optimal_control_problem.rst @@ -0,0 +1,45 @@ +.. + module.rst + +pycollo.optimal\_control\_problem +================================= + +.. automodule:: pycollo.optimal_control_problem + + + + + + + + .. rubric:: Functions + + .. autosummary:: + :toctree: + :nosignatures: + :template: autosummary/base.rst + + cout + kill + + + + + + .. rubric:: Classes + + .. autosummary:: + :toctree: + :nosignatures: + :template: autosummary/class.rst + + OptimalControlProblem + + + + + + + + + diff --git a/docs/_autosummary/pycollo.phase.Phase.rst b/docs/_autosummary/pycollo.phase.Phase.rst new file mode 100644 index 0000000..f7f9f70 --- /dev/null +++ b/docs/_autosummary/pycollo.phase.Phase.rst @@ -0,0 +1,57 @@ +.. + class.rst + +pycollo.phase.Phase +=================== + +.. currentmodule:: pycollo.phase + +.. autoclass:: Phase + :members: + :show-inheritance: + :inherited-members: + :special-members: + :exclude-members: __weakref__ + + + + .. rubric:: Attributes + + .. autosummary:: + + ~Phase.bounds + ~Phase.control_variables + ~Phase.final_state_variables + ~Phase.final_time_variable + ~Phase.guess + ~Phase.initial_state_variables + ~Phase.initial_time_variable + ~Phase.integral_variables + ~Phase.integrand_functions + ~Phase.mesh + ~Phase.name + ~Phase.number_control_variables + ~Phase.number_integral_variables + ~Phase.number_integrand_functions + ~Phase.number_path_constraints + ~Phase.number_state_equations + ~Phase.number_state_variables + ~Phase.optimal_control_problem + ~Phase.path_constraints + ~Phase.phase_number + ~Phase.scaling + ~Phase.state_equations + ~Phase.state_variables + ~Phase.time_variables + + + .. rubric:: Methods + + .. autosummary:: + :nosignatures: + + ~Phase.__init__ + ~Phase.create_new_copy + ~Phase.create_new_copy_like + + \ No newline at end of file diff --git a/docs/_autosummary/pycollo.phase.rst b/docs/_autosummary/pycollo.phase.rst new file mode 100644 index 0000000..3182855 --- /dev/null +++ b/docs/_autosummary/pycollo.phase.rst @@ -0,0 +1,35 @@ +.. + module.rst + +pycollo.phase +============= + +.. automodule:: pycollo.phase + + + + + + + + + + + + .. rubric:: Classes + + .. autosummary:: + :toctree: + :nosignatures: + :template: autosummary/class.rst + + Phase + + + + + + + + + diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst index c283f48..f7e7609 100644 --- a/docs/_templates/autosummary/class.rst +++ b/docs/_templates/autosummary/class.rst @@ -10,6 +10,7 @@ :show-inheritance: :inherited-members: :special-members: + :exclude-members: __weakref__, __repr__, __str__ {% block methods %} {% if methods %} diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 3a6ad05..64664a1 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -4,10 +4,12 @@ API Reference ============= +.. image:: code_structure.png + :width: 800 + .. autosummary:: :toctree: _autosummary :recursive: pycollo.optimal_control_problem pycollo.phase - diff --git a/docs/code_structure.png b/docs/code_structure.png new file mode 100644 index 0000000..526fa37 Binary files /dev/null and b/docs/code_structure.png differ diff --git a/docs/conf.py b/docs/conf.py index 0736b83..ae5bffb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,6 +24,7 @@ "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", "sphinx.ext.autosummary", "sphinx.ext.mathjax", "sphinx.ext.viewcode", @@ -45,7 +46,6 @@ "python": ("http://docs.python.org/3", None), } - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/pycollo/optimal_control_problem.py b/pycollo/optimal_control_problem.py index 7c515fd..6f90583 100644 --- a/pycollo/optimal_control_problem.py +++ b/pycollo/optimal_control_problem.py @@ -1,13 +1,12 @@ """The main way to define and interact with a Pycollo optimal control problem. -This module contains the main class that the user will interact with to define -and run their optimal control problem when working with Pycollo. Terminolgy is -loosely defined in accordance with "Betts, JT (2010). Practical Methods for -Optimal Control and Estimiation Using Nonlinear Programming (Second Edition)". -See the ``Notes`` section for a full list of symbols used. +Method is used to define the objective function, endpoint constraints, and parameter variables. Each :class:`~.OptimalControlProblem` instance is associated with one or more :class:`~.Phase` instance. + +OCP require bounds and initial guesses to be prescribed before they can numerically be solved, handeled in :mod:`~.bounds` and :mod:`~.guess`. + +Terminolgy is loosely defined in accordance with "Betts, JT (2010). Practical Methods for Optimal Control and Estimiation Using Nonlinear Programming (Second Edition)": Notes: ------- * t: independent parameter (time). * x = [y, u, q, t0, tf, s]: vector of problem variables. * y: vector state variables (which are functions of time). @@ -34,7 +33,7 @@ """ -from typing import AnyStr, Iterable, Tuple +from typing import AnyStr, Iterable, Tuple, Optional import numpy as np @@ -47,7 +46,7 @@ from .phase import Phase from .scaling import EndpointScaling from .settings import Settings -from .typing import OptionalSymsType +from .typing import OptionalSymsType, OptionalExprsType, OptionalBoundsType, TupleSymsType from .utils import check_sym_name_clash, console_out, format_as_data_container __all__ = ["OptimalControlProblem"] @@ -57,24 +56,33 @@ class OptimalControlProblem(): """The main class for Pycollo optimal control problems""" def __init__(self, - name, + name: str, parameter_variables=None, *, - bounds=None, - guess=None, - scaling=None, - endpoint_constraints=None, - objective_function=None, - settings=None, - auxiliary_data=None, - ): + bounds: Optional[EndpointBounds]=None, + guess: Optional[EndpointGuess]=None, + scaling: Optional[EndpointScaling]=None, + endpoint_constraints: Optional=None, + objective_function: Optional[sym.Expr]=None, + settings: Optional[Settings]=None, + auxiliary_data: Optional[dict]=None, + ) -> None: """Initialise the optimal control problem with user-passed objects. Args: - phases (:obj:`Iterable` of :class:`~.Phase`, optional): Phases to be - associated with the optimal control problem at initialisation. - Defaults to None. - parameter_variables () + name: Minimally required to initialise + parameter_variables: Optimised variables which will solve to single + numerical number over all phases. + bounds: Problem specific bounds. See :obj:`.~EndpointBounds` for + more details. Default value is None in which case an empty + :obj:`.~EndpointBounds` object is instantiated and associated with the OCP. + guess: The initial guess at which this OCP is to be solved. + Default value is None in which case an empty :obj:`EndpointGuess` object is instantiated. + scaling: + endpoint_constraints: Endpoint constraints which bound this OCP. Default to None + objective_function: Instance to be minimised. Defaults to None. + settings: All OCP settings. Defaults to default settings in :class:`~.Settings`. + auxiliary_data: Extra data provided by user. Defaults to empty :class:`dict`. """ self.name = name @@ -95,48 +103,48 @@ def __init__(self, @property def name(self) -> str: """The name associated with the optimal control problem. For setter - behaviour, the supplied `name` is cast to a str. + behaviour, the supplied `name` is cast to a :class:`str`. The name is not strictly needed, however it improves the usefulness of Pycollo console output. This is particularly useful in cases where the user may wish to instantiate multiple :obj:`OptimalControlProblem` objects within a single script, or instantiates other Pycollo objects - without providing a valid :class:`~.optimal_control_problem` argument for them - to be linked to at initialisation. + + without providing a valid :mod:`~.optimal_control_problem` argument for them to be linked to at initialisation. """ return self._name @name.setter - def name(self, name: AnyStr): + def name(self, name: AnyStr) -> None: self._name = str(name) @property def phases(self) -> Tuple[Phase, ...]: - """A tuple of all phases associated with the optimal control problem. + """A :class:`tuple` of all phases associated with the optimal control problem. :meth:`~.phase_number` are integers beginning at 1 and are ordered corresponding to the order that they were added to the optimal control problem. As Python uses zero-based indexing the phase numbers - do not directly map to the indexes of phases within :class:`~.phases`. + do not directly map to the indexes of phases within :attr:`~.phases`. Phases are however ordered sequentially corresponding to the - cronological order they were added to the optimal control problem. + chronological order they were added to the optimal control problem. """ return self._phases def add_phase(self, phase: Iterable[Phase]) -> Phase: """Add an already instantiated :class:`~.Phase` to this optimal control problem. - This method is needed as :class:`~.phases` is read only ("private") and + This method is needed as :attr:`~.phases` is read only ("private") and therefore users cannot manually add :class:`~.Phase` objects to an optimal - control problem. :class:`~.phases` is required to be read only as it is an + control problem. :attr:`~.phases` is required to be read only as it is an iterable of :class:`~.Phase` objects and must be protected from accidental errors introduced by user interacting with it incorrectly. Args: - phase (Phase): The phase to be added to the optimal control problem + phase (:class:`~.Phase`): The phase to be added to the optimal control problem Returns: - the phase that has been added. It is the same + the phase that has been added. It is the same """ phase.optimal_control_problem = self return self.phases[-1] @@ -155,7 +163,7 @@ def new_phase(self, control_variables: OptionalSymsType = None) -> Phase: """Create a new :obj:`~.Phase` and add to this optimal control problem. - Provides the same behaviour as manually creating a :obj:`.~Phase` called + Provides the same behaviour as manually creating a :obj:`~.Phase` called `phase` and calling :meth:`~.add_phase`. """ new_phase = Phase(name, optimal_control_problem=self, @@ -163,7 +171,9 @@ def new_phase(self, control_variables=control_variables) return new_phase - def new_phase_like(self, phase_for_copying: Phase, name: str, **kwargs): + def new_phase_like(self, phase_for_copying: Phase, name: str, **kwargs) -> Phase: + """Creates new phase while copying all implemented values from `phase_for_copying` + """ return phase_for_copying.create_new_copy(name, **kwargs) def new_phases_like(self, @@ -174,14 +184,14 @@ def new_phases_like(self, """Creates multiple new phases like an already instantiated phase. For a list of key word arguments and default values see the docstring - for the :meth:`~.new_phase_like` method. + for the :func:`new_phase_like` method. Returns: - The newly instantiated and associated phases. + The newly instantiated and associated phases. Raises: - ValueError: If the same number of names are not supplied as the - number of specified new phases. + ValueError: If the same number of names are not supplied as the + number of specified new phases. """ if len(names) != int(number): msg = ("Must supply a name for each new phase.") @@ -192,27 +202,33 @@ def new_phases_like(self, @property def number_phases(self) -> int: - """Number of phases associated with this optimal control problem.""" + """Returns number of phases associated with this optimal control problem.""" return len(self.phases) @property def time_symbol(self): - """ - + """Dynamic time symbol, not yet supported + Raises: - NotImplementedError: Whenever called to inform the user that these - types of problem are not currently supported. + NotImplementedError: Whenever called to inform the user that these + types of problem are not currently supported. """ msg = ("Pycollo do not currently support dynamic, path or integral " "constraints that are explicit functions of continuous time.") raise NotImplementedError(msg) @property - def parameter_variables(self): + def parameter_variables(self) -> TupleSymsType: + """Static parameter variables which are optimized within given bounds. + + When the instance OCP is created you can supply the parameter variables by OCP_instance.parameter_variables = :class:`tuple` [:class:`Symbol`,...] | :class:`list` [:class:`Symbol`,..] | :obj:`numpy.array` [:class:`Symbol`,...] + + As described in Betts, JT (2010). Bounds and guesses need to be implemented. See :obj:`~.EndPointBounds` and :class:`~.EndPointGuess` how to implement them. + """ return self._s_var_user @parameter_variables.setter - def parameter_variables(self, s_vars): + def parameter_variables(self, s_vars: OptionalSymsType) -> None: self._s_var_user = format_as_data_container("ParameterVariables", s_vars) _ = check_sym_name_clash(self._s_var_user) @@ -221,11 +237,17 @@ def number_parameter_variables(self): len(self._s_var_user) @property - def endpoint_constraints(self): + def endpoint_constraints(self) -> OptionalExprsType: + """Inequality constraints consisting of endpoint variables. + + These constraints are the glue in between the phases. For example, when variables need to be continious throughout multiple phases you can set the y_F of phase A equal to y_0 of phase B. More complex formulations are allowed for example for handeling discontinuities. They are formulated as inequality constraint, but by clever formulating can be handeled as equality constraints (A-B > [0,0]) + + Bounds should be implemented. See :obj:`~.EndPointBounds` how to implement them. + """ return self._b_con_user @endpoint_constraints.setter - def endpoint_constraints(self, b_cons): + def endpoint_constraints(self, b_cons: OptionalExprsType) -> None: self._b_con_user = format_as_data_container( "EndpointConstraints", b_cons, @@ -233,32 +255,41 @@ def endpoint_constraints(self, b_cons): ) @property - def number_endpoint_constraints(self): + def number_endpoint_constraints(self) -> int: + """Returns number of endpoint constraints""" return len(self._b_con_user) @property - def objective_function(self): + def objective_function(self) -> OptionalExprsType: + """Formula to be minimised during the optimization. + + It is a `Bolza` objective function which may be a function of endpoint variables or parameter variables (:attr:`~.integral_variables`, :attr:`~.initial_time_variable`, :attr:`~.final_time_variable`, :attr:`~.initial_state_variable, and :attr:`~.final_state_variable or :attr:`~.parameter_variables` ). + """ return self._J_user @objective_function.setter - def objective_function(self, J): + def objective_function(self, J: OptionalExprsType) -> None: self._J_user = sym.sympify(J) # self._forward_dynamics = True if self._J_user == 1 else False @property - def auxiliary_data(self): + def auxiliary_data(self) -> dict[OptionalSymsType]: + """:class:`dict` containing extra provided data. + + When a symbolic variable (:class:`Symbol`) is indetermined, make sure to assign it to a numerical value or symbolic formulation with this function """ return self._aux_data_user @auxiliary_data.setter - def auxiliary_data(self, aux_data): + def auxiliary_data(self, aux_data: dict[OptionalSymsType, int]) -> None: self._aux_data_user = dict(aux_data) @property - def bounds(self): + def bounds(self) -> EndpointBounds: + """Attribute to provide bounds to :attr:`~.parameter_variables`, and :attr:`~.endpoint_constraints`""" return self._bounds @bounds.setter - def bounds(self, bounds): + def bounds(self, bounds: OptionalBoundsType) -> None: if bounds is None: self._bounds = EndpointBounds(optimal_control_problem=self) else: @@ -266,11 +297,12 @@ def bounds(self, bounds): self._bounds._ocp = self @property - def guess(self): + def guess(self) -> EndpointGuess: + """Attribute to provide initial guess to :attr:`~.parameter_variables`, and :attr:`~.endpoint_constraints`""" return self._guess @guess.setter - def guess(self, guess): + def guess(self, guess: OptionalBoundsType) -> None: if guess is None: self._guess = EndpointGuess(optimal_control_problem=self) else: @@ -278,11 +310,11 @@ def guess(self, guess): self._guess.optimal_control_problem = self @property - def scaling(self): + def scaling(self) -> EndpointScaling: return self._scaling @scaling.setter - def scaling(self, scaling): + def scaling(self, scaling: EndpointScaling) -> None: if scaling is None: self._scaling = EndpointScaling(optimal_control_problem=self) else: @@ -294,15 +326,19 @@ def mesh_iterations(self): return self._mesh_iterations @property - def num_mesh_iterations(self): + def num_mesh_iterations(self) -> int: + """Returns number of mesh iterations""" return len(self._backend.mesh_iterations) @property - def settings(self): + def settings(self) -> Settings: + """Attribute to overwrite default settings. + + See :mod:`~.settings` for all settings options""" return self._settings @settings.setter - def settings(self, settings): + def settings(self, settings: Settings) -> None: if settings is None: self._settings = Settings(optimal_control_problem=self) else: @@ -311,9 +347,24 @@ def settings(self, settings): @property def solution(self): + """ Attribute where all solutions are found after solving the OCP (:func:`~.solve`). + + Solutions are provided by indexing. OCP_instance.solution.attribute[x][y] refers to the y`th attribute in phase x + + Attributes: + objective: Numerical solution of :attr:`~.objective_function` + initial_time: Initial times of phases of solved OCP + final_time: Final times of phases of solved OCP + state: Numerical solution of state_variables` + state_derivative: Numerical solution of :attr:`~.state_equations` + control: Numerical solution of :attr:`~.control` + integral: Numerical solution of :attr:`~.integrand_function` + time: Numerical solution of time + parameter: Numerical solution of :attr:`~.parameter_variables` + """ return self._backend.mesh_iterations[-1].solution - def initialise(self): + def initialise(self) -> None: """Initialise the optimal control problem before solving. The initialisation of the optimal control problem involves the @@ -384,7 +435,7 @@ def _initialise_initial_mesh(self): def _initialise_first_mesh_iteration(self): self._backend.create_mesh_iterations() - def solve(self, display_progress=False): + def solve(self, display_progress=False) -> None: """Solve the optimal control problem. If the initialisation flag is not set to True then the initialisation @@ -408,11 +459,10 @@ def _check_if_initialisation_required_before_solve(self): if not self._is_initialised: self.initialise() - def _solve_iteration(self): + def _solve_iteration(self) -> bool: """Solve a single mesh iteration. - Return: - bool + Return(bool): True is mesh tolerance is met or if maximum number of mesh iterations has been reached. @@ -553,7 +603,7 @@ def time_results(): mesh_results() time_results() - def __str__(self): + def __str__(self) -> str: """Returns name of OCP""" return self.name diff --git a/pycollo/phase.py b/pycollo/phase.py index b79d8e7..d34398c 100644 --- a/pycollo/phase.py +++ b/pycollo/phase.py @@ -1,12 +1,7 @@ -"""Everything needed for defining phases within an optimal control problem. - -Classes: - Phase -""" - +"""Everything needed for defining phases within an optimal control problem.""" import copy -from typing import Optional, Tuple +from typing import Optional, Tuple, Iterable import sympy as sym @@ -14,7 +9,7 @@ from .guess import PhaseGuess from .mesh import PhaseMesh from .scaling import PhaseScaling -from .typing import OptionalExprsType, OptionalSymsType, TupleSymsType +from .typing import OptionalExprsType, OptionalSymsType, TupleSymsType, OptionalBoundsType from .utils import check_sym_name_clash, format_as_data_container __all__ = ["Phase"] @@ -22,49 +17,9 @@ class Phase: """A single continuous time phase as part of an optimal control problem. - - Attributes: - name: The name associated with a problem. Should be something short - like 'A'. - optimal_control_problem: The :obj:`OptimalControlProblem` with which - this phase is to be associated. - state_variables: The continuous time state variables in this phase. - control_variables: The continuous time control variables in this phase. - state_equations: The dynamical state equations associated with this - state variables in this phase. - integrand_functions: The integrand functions corresponding to the - integral variables in this phase. - path_constraints: The continuous time path constraints associated with - this phase. - bounds: The phase bounds on this phase. See :obj:PhaseBounds for more - details. - scaling: The phase scaling on this phase. See :obj:PhaseScaling for - more details. - guess: The initial guess at which this phase is to be solved. - mesh: This initial mesh on which this phase is to be solved. - _name: Protected version of :attr:`name`. - _ocp: Protected version of :attr:`optimal_control_problem`. - _phase_number: Protected integer number associated with this phase. If - not associated with any optimal control problem then defaults to - None until one is associated. These are ordered sequentially - starting at '0' in the order with which phases are added to an - optimal control problem. - _phase_suffix: Protected str which is used in the naming of auto- - generated Pycollo variables such as the endpoint state variables. - _y_var_user: Protected version of :attr:`state_variables`. - _u_var_user: Protected version of :attr:`control_variables`. - _q_var_user: Protected version of :attr:`integral_variables`. - _t_var_user: Protected version of :attr:`time_variables`. - _y_eqn_user: Protected version of :attr:`state_equations`. - _c_con_user: Protected version of :attr:`path_constraints`. - _q_fnc_user: Protected version of :attr:`integrand_functions`. - _t0_USER: Protected version of :attr:`initial_time_variable`. - _tF_USER: Protected version of :attr:`final_time_variable`. - _t0: Internal Pycollo symbol for phase initial time. - _tF: Internal Pycollo symbol for phase final time. - _STRETCH: Convenience expression for phase time scaling stretch. - _SHIFT: Convenience expression for phase time scaling shift. - """ + + State, control, integral and time variables are defined per phase. + Also state equations, path constraints, integrand functions and state endpoint constraints are defined here.""" def __init__(self, name: str, @@ -79,47 +34,47 @@ def __init__(self, scaling: Optional[PhaseScaling] = None, guess: Optional[PhaseGuess] = None, mesh: Optional[PhaseMesh] = None, - ): + ) -> None: """Initialise the Phase object with minimum a name. Args: - name: The name associated with a problem. Should be something short - like 'A'. - optimal_control_problem: The :obj:`OptimalControlProblem` with - which this phase is to be associated. Default value is None in - which case the phase remain uninitialised to an optimal control - problem. - state_variables: The continuous time state variables in this phase. - Default value is None in which case the phase has no associated - state variables and no phase-specific endpoint time or state - variables are created. - control_variables: The continuous time control variables in this - phase. Default value is None in which case the phase has no - associated control variables. - state_equations: The dynamical state equations associated with this - state variables in this phase. Default value is None in which - case no dynamical equations have been added to the phase yet. - integrand_functions: The integrand functions corresponding to the - integral variables in this phase. Default value is None in - which case the phase has no integrand functions associated with - it and no phase-specific integral variables are created. - path_constraints: The continuous time path constraints associated - with this phase. Default value is None in which case the phase - has no path constraints associated with it. - bounds: The phase bounds on this phase. See :obj:PhaseBounds for - more details. Default value is None in which case an empty - :obj:`PhaseBounds` object is instantiated and associated with - the phase. - scaling: The phase scaling on this phase. See :obj:PhaseScaling for - more details. Default value is None in which case an empty - :obj:`PhaseScaling` object is instantiated and associated with - the phase. - guess: The initial guess at which this phase is to be solved. - Default value is None in which case an empty :obj:`PhaseGuess` - object is instantiated and associated with the phase. - mesh: This initial mesh on which this phase is to be solved. - Default value is None in which case an empty :obj:`PhaseMesh` - object is instantiated and associated with the phase. + name: The name associated with a problem. Should be something short + like 'A'. + optimal_control_problem: The OptimalControlProblem with + which this phase is to be associated. Default value is None in + which case the phase remain uninitialised to an optimal control + problem. + state_variables: The continuous time state variables in this phase. + Default value is None in which case the phase has no associated + state variables and no phase-specific endpoint time or state + variables are created. + control_variables: The continuous time control variables in this + phase. Default value is None in which case the phase has no + associated control variables. + state_equations: The dynamical state equations associated with this + state variables in this phase. Default value is None in which + case no dynamical equations have been added to the phase yet. + integrand_functions: The integrand functions corresponding to the + integral variables in this phase. Default value is None in + which case the phase has no integrand functions associated with + it and no phase-specific integral variables are created. + path_constraints: The continuous time path constraints associated + with this phase. Default value is None in which case the phase + has no path constraints associated with it. + bounds: The phase bounds on this phase. See :obj:`.~PhaseBounds` for + more details. Default value is None in which case an empty + :obj:`.~PhaseBounds` object is instantiated and associated with + the phase. + scaling: The phase scaling on this phase. See :obj:PhaseScaling for + more details. Default value is None in which case an empty + :obj:`~.PhaseScaling` object is instantiated and associated with + the phase. + guess: The initial guess at which this phase is to be solved. + Default value is None in which case an empty :obj:`PhaseGuess` + object is instantiated and associated with the phase. + mesh: This initial mesh on which this phase is to be solved. + Default value is None in which case an empty :obj:`PhaseMesh` + object is instantiated and associated with the phase. """ self._name = str(name) @@ -166,8 +121,8 @@ def create_new_copy(self, copy_mesh: bool = True, copy_scaling: bool = True, copy_guess: bool = True, - ): - + ) -> "Phase": + """Creates copy of a Phase instance. Use :meth:`~.create_new_copy_like` to automatically add Phase to OCP instance""" self._check_variables_and_equations() new_phase = Phase(name, optimal_control_problem=self.optimal_control_problem) @@ -217,13 +172,13 @@ def create_new_copy_like(phase_for_copying: "Phase", name: str, **kwargs): return phase_for_copying.create_new_copy(name, **kwargs) @property - def name(self): - """Name of the phase.""" + def name(self) -> str: + """The name associated with a problem. Should be something short like 'A'.""" return self._name @property def optimal_control_problem(self) -> Optional["OptimalControlProblem"]: - """The optimal control problem with which this phase is associated. + """The :obj:`~.OptimalControlProblem` with which this phase is associated. There are two allowable scenarios. In the first scenario a phase may be instantiated without being associated with an optimal control problem. @@ -234,7 +189,7 @@ def optimal_control_problem(self) -> Optional["OptimalControlProblem"]: optimal control problem or is associated with an optimal control problem after the first type of instantiation. In this case the phase is appended to the protected `_phases` attribute of the - :obj:`OptimalControlProblem`, the phase number is set according to its + :obj:`~.OptimalControlProblem`, the phase number is set according to its position in the order of addition to the optimal controls problem's phases, and its phase suffix is set as a string version of the phase number. Finally a replacement of any symbols that may have been used in @@ -247,21 +202,19 @@ def optimal_control_problem(self) -> Optional["OptimalControlProblem"]: accessed after having already been set then an `AttributeError` is raised (see below). The reason this class works like that is to avoid having to allow phases to be disassociated from an - :obj:`OptimalControlProblem` and thus having to handled the complexities + :obj:`~.OptimalControlProblem` and thus having to handled the complexities that would come with the phase renumbering and substitution of any phase-related information that has already been given to the optimal control problem. Raises: - AttributeError: If an :obj:`OptimalControlProblem` has already been - associated with `self`. If a argument of any type other than - :obj:`OptimalControlProblem` is passed to the - `optimal_control_problem` property setter. + AttributeError: If an :obj:`~.OptimalControlProblem` has already + been associated with `self`. If a argument of any type other than :obj:`~.OptimalControlProblem` is passed to the :mod:`~.optimal_control_problem` property setter. """ return self._ocp @optimal_control_problem.setter - def optimal_control_problem(self, ocp): + def optimal_control_problem(self, ocp) -> None: if self._ocp is not None: msg = ('Optimal control problem is already set for this phase and ' 'cannot be reset.') @@ -323,9 +276,7 @@ def initial_state_variables(self) -> TupleSymsType: """Symbols for this phase's state variables at the initial time. Raises: - AttributeError: If `optimal_control_problem` property has not yet - been set to a not None value. See docstring for - `state_variables` for details about why. + AttributeError: If :attr:`~.optimal_control_problem` property has not yet been set to a not None value. See docstring for :attr:`~.state_variables` for details about why. """ try: return self._y_t0_user @@ -339,9 +290,8 @@ def final_state_variables(self) -> TupleSymsType: """Symbols for this phase's state variables at the final time. Raises: - AttributeError: If `optimal_control_problem` property has not yet - been set to a not None value. See docstring for - `state_variables` for details about why. + AttributeError: If `optimal_control_problem` property has not yet + been set to a not None value. See docstring for :attr:`~.state_variables` for details about why. """ try: return self._y_tF_user @@ -352,22 +302,16 @@ def final_state_variables(self) -> TupleSymsType: @property def state_variables(self) -> TupleSymsType: - """Symbols for this phase's state variables in order added by user. - + """:class:`Symbols` for this phase's continuous time state variables in order added by user. + The user may supply either a single symbol or an iterable of symbols. The supplied argument is handled by the `format_as_tuple` method from - the `utils` module. Additional protected attributes `_y_t0_user` and - `_y_tF_user` are set by post-appending either '_PX(t0)' or '_PX(tF)' to - the user supplied symbols where the X is replaced by the phase suffix. - As such if this phase has not yet been associated with an optimal - control problem yet then `self` will not have attributes `_y_t0_user` - and `_y_tF_user` and accessing either the `initial_state` or - `final_state` property will raise an AttributeError. + the :mod:`.~utils` module. Additional protected attributes `_y_t0_user` and `_y_tF_user` are set by post-appending either '_PX(t0)' or '_PX(tF)' to the user supplied symbols where the X is replaced by the phase suffix. As such if this phase has not yet been associated with an optimal control problem yet then `self` will not have attributes `_y_t0_user` and `_y_tF_user` and accessing either the `initial_state` or `final_state` property will raise an AttributeError. """ return self._y_var_user @state_variables.setter - def state_variables(self, y_vars: OptionalSymsType): + def state_variables(self, y_vars: OptionalSymsType) -> None: self._y_var_user = format_as_data_container("StateVariables", y_vars) check_sym_name_clash(self._y_var_user) @@ -406,35 +350,32 @@ def state_variables(self, y_vars: OptionalSymsType): @property def number_state_variables(self) -> int: - """Integer number of state variables in the phase.""" + """Returns number of state variables in the phase.""" return len(self._y_var_user) @property def control_variables(self) -> TupleSymsType: - """Symbols for this phase's control variables in order added by user. + """:class:`Symbols` for this phase's continious time control variables in order added by user. The user may supply either a single symbol or an iterable of symbols. The supplied argument is handled by the `format_as_tuple` method from - the `utils` module. + the :mod:`.~utils` module. """ return self._u_var_user @control_variables.setter - def control_variables(self, u_vars: OptionalSymsType): + def control_variables(self, u_vars: OptionalSymsType) -> None: self._u_var_user = format_as_data_container("ControlVariables", u_vars) check_sym_name_clash(self._u_var_user) @property def number_control_variables(self) -> int: - """Integer number of control variables in the phase.""" + """Returns number of control variables in the phase.""" return len(self._u_var_user) @property def integral_variables(self) -> TupleSymsType: - """Symbols for this phase's integral variables. - - These symbols are auto generated as required by the user-supplied - integrand functions. + """Auto generated integral_variablessymbols as required by the user-supplied integrand functions. """ return self._q_var_user @@ -445,23 +386,23 @@ def time_variables(self) -> TupleSymsType: @property def number_integral_variables(self) -> int: - """Integer number of integral variables in the phase.""" + """Returns number of integral variables in this phase.""" return len(self._q_var_user) @property def state_equations(self) -> Tuple[sym.Expr, ...]: """User-supplied dynamical equations in the phase. - - These equations are the dynamical equations associated with each of the + + These dynamical equations are associated with each of the state variables in the phase. There should therefore be exactly one - state equation for each dynamics symbol. + state equation for each state variable. - State equations can be supplied in a compact form by the user defining additional auxiliary symbols and + State equations can be supplied in a compact form by the user defining additional auxiliary symbols. """ return self._y_eqn_user @state_equations.setter - def state_equations(self, y_eqns: OptionalExprsType): + def state_equations(self, y_eqns: OptionalExprsType) -> None: try: identifiers = self._y_var_user._field_names except AttributeError: @@ -475,7 +416,7 @@ def state_equations(self, y_eqns: OptionalExprsType): @property def number_state_equations(self) -> int: - """Integer number of state equations in the phase. + """Returns number of state equations in the phase. Should be the same as the number of state variables, i.e. there should be a direct mapping between the two. @@ -483,11 +424,17 @@ def number_state_equations(self) -> int: return len(self._y_eqn_user) @property - def path_constraints(self): + def path_constraints(self) -> TupleSymsType: + """The continuous time path constraints associated with this phase. + + Path constraints are inequality constraints as a function of continious time variables (i.e. state variables, control variables). Bounds need to be applied. + The introduction of additional path constraints is not ideal as these increase the complexity of the NLP and its di culty to solve, and should be avoided if possible. + They are formulated as inequality constraint, but by clever formulating can be handeled as equality constraints (A-B > [0,0]) + """ return self._c_con_user @path_constraints.setter - def path_constraints(self, c_cons): + def path_constraints(self, c_con: OptionalExprsType) -> None: self._c_con_user = format_as_data_container( "PathConstraints", c_cons, @@ -495,15 +442,17 @@ def path_constraints(self, c_cons): ) @property - def number_path_constraints(self): + def number_path_constraints(self) -> int: + """Returns number of path constraints""" return len(self._c_con_user) @property - def integrand_functions(self): + def integrand_functions(self) -> TupleSymsType: + """The integrand functions corresponding to the integral variables in this phase.""" return self._q_fnc_user @integrand_functions.setter - def integrand_functions(self, integrands): + def integrand_functions(self, integrands: OptionalExprsType) -> None: self._q_fnc_user = format_as_data_container( "IntegrandFunctions", integrands, @@ -513,48 +462,52 @@ def integrand_functions(self, integrands): for i_q, _ in enumerate(self._q_fnc_user)) @property - def number_integrand_functions(self): + def number_integrand_functions(self) -> int: return len(self._q_fnc_user) @property - def bounds(self): + def bounds(self) -> PhaseBounds: + """The phase bounds on this phase. See :obj:`~.PhaseBounds` for more details.""" return self._bounds @bounds.setter - def bounds(self, bounds): + def bounds(self, bounds: OptionalBoundsType) -> None: if bounds is None: self._bounds = PhaseBounds(phase=self) else: self._bounds = bounds @property - def scaling(self): + def scaling(self) -> PhaseScaling: + """The phase scaling on this phase. See :obj:`~.PhaseScaling` for more details.""" return self._scaling @scaling.setter - def scaling(self, scaling): + def scaling(self, scaling: PhaseScaling) -> None: if scaling is None: self._scaling = PhaseScaling(phase=self) else: self._scaling = scaling @property - def mesh(self): + def mesh(self) -> PhaseMesh: + """This initial mesh on which this phase is to be solved.""" return self._mesh @mesh.setter - def mesh(self, mesh): + def mesh(self, mesh: PhaseMesh) -> None: if mesh is None: self._mesh = PhaseMesh(phase=self) else: self._mesh = mesh @property - def guess(self): + def guess(self) -> PhaseGuess: + """The initial guess at which this phase is to be solved.""" return self._guess @guess.setter - def guess(self, guess): + def guess(self, guess: PhaseGuess) -> None: if guess is None: self._guess = PhaseGuess(phase=self) else: @@ -572,11 +525,8 @@ def _check_variables_and_equations(self): warning to the user describing what is wrong about the supplied state variables and state equations. - Raises - ------ - ValueError - If the state variables and symbols associated with the state - equations are not the same. + Raises: + ValueError: If the state variables and symbols associated with the state equations are not the same. """ try: