From 327292272a80e096627dac83ab109533f79e7be7 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 18 Apr 2023 17:14:20 +0200 Subject: [PATCH 001/237] Draft the ODE discipline. --- src/gemseo/disciplines/ode_discipline.py | 157 +++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/gemseo/disciplines/ode_discipline.py diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py new file mode 100644 index 0000000000..45b5bf0d65 --- /dev/null +++ b/src/gemseo/disciplines/ode_discipline.py @@ -0,0 +1,157 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Copyright 2023 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Francois Gallard +# OTHER AUTHORS - MACROSCOPIC CHANGES +"""A discipline for ODEs. + +ODE stands for Ordinary Differential Equation. +""" +from __future__ import annotations + +from typing import Any +from typing import Mapping + +from gemseo.algos.ode.ode_problem import ODEProblem +from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory +from gemseo.core.discipline import MDODiscipline +from gemseo.core.grammars.base_grammar import BaseGrammar +from gemseo.core.grammars.defaults import Defaults +from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( + MDODisciplineAdapterGenerator, +) +from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array +from gemseo.utils.data_conversion import split_array_to_dict_of_arrays + + +class ODEDiscipline(MDODiscipline): + r"""A discipline based on the solution to an ODE.""" + + discipline: MDODiscipline + """The discipline.""" + + time_var_name: str + """The name of the time variable.""" + + state_var_names: list[str] + """The names of the state variables.""" + + ode_solver_name: str + """The name of the solver used to solve the ODE.""" + + ode_solver_options: Mapping[str:Any] + """The options to pass to the ODE solver.""" + + ode_problem: ODEProblem + """The problem to be solved by the ODE solver.""" + + initial_time: float + """The start of the time interval to perform the integration on.""" + + final_time: float + """The end of the time interval to perform the integration on.""" + + input_grammar: BaseGrammar + """The input grammar of the discipline.""" + + output_grammar: BaseGrammar + """The output grammar of the discipline.""" + + default_inputs: Defaults + """The default inputs for the discipline.""" + + ode_factory: ODESolversFactory + """The factory to build the ODE.""" + + def __int__( + self, + discipline: MDODiscipline, + time_var_name: str, + state_var_names: list[str], + initial_time: float, + final_time: float, + ode_solver_name: str, + ode_solver_options: Mapping[str:Any], + name: str = None, + ): + self.discipline = discipline + self.time_var_name = time_var_name + self.state_var_names = state_var_names + self.ode_solver_name = ode_solver_name + self.ode_solver_options = ode_solver_options + self.ode_problem = None + self.initial_time = initial_time + self.final_time = final_time + + super(self, ODEDiscipline).__init__( + name=name, grammar_type=discipline.grammar_type + ) + self.input_grammar.update(self.discipline.input_grammar) + self.output_grammar.update(self.discipline.output_grammar) + self.default_inputs = self.discipline.default_inputs.copy() + self.ode_factory = ODESolversFactory() + + self.__generator_default_inputs = {} + + self._init_ode_problem() + + def _init_ode_problem(self): + """Initialize the ODE problem with the user-defined parameters.""" + initial_state = concatenate_dict_of_arrays_to_array( + self.discipline.default_outputs, names=self.state_var_names + ) + ode_func = MDODisciplineAdapterGenerator(self.discipline).get_function( + input_names=[self.time_var_name], + output_names=self.state_var_names, + default_inputs=self.__generator_default_inputs, + ) + self.ode_problem = ODEProblem( + ode_func, + initial_state=initial_state, + initial_time=self.initial_time, + final_time=self.final_time, + jac=ode_func.jac, + ) + + self.__names_to_sizes = { + name: self.discipline.default_outputs[name].size + for name in self.state_var_names + } + + def _run(self): + self.__generator_default_inputs.update(self.local_data) + self.ode_problem.time_vector = self.local_data[self.time_var_name] + ode_result = self.ode_factory.execute( + self.ode_problem, self.ode_solver_name, **self.ode_solver_options + ) + spl_state = split_array_to_dict_of_arrays( + ode_result.state_vector, self.__names_to_sizes, self.state_var_names + ) + self.local_data.update(spl_state) -- GitLab From 715a81d7e1c64d11c315f22e8762b7d0bb3a9d20 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 14:28:57 +0100 Subject: [PATCH 002/237] Fix merge after upstream rebase. --- src/gemseo/algos/ode/ode_solvers_factory.py | 4 ++-- tests/algos/ode/test_lib_scipy_ode.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gemseo/algos/ode/ode_solvers_factory.py b/src/gemseo/algos/ode/ode_solvers_factory.py index b4db0be753..6fc0b483ac 100644 --- a/src/gemseo/algos/ode/ode_solvers_factory.py +++ b/src/gemseo/algos/ode/ode_solvers_factory.py @@ -29,7 +29,7 @@ from gemseo.algos.ode.ode_solver_lib import ODESolverLib if TYPE_CHECKING: from gemseo.algos.ode.ode_problem import ODEProblem - from gemseo.algos.opt_result import OptimizationResult + from gemseo.algos.ode.ode_problem import ODEResult class ODESolversFactory(BaseAlgoFactory): @@ -43,7 +43,7 @@ class ODESolversFactory(BaseAlgoFactory): problem: ODEProblem, algo_name: str = "RK45", **options: Any, - ) -> OptimizationResult: + ) -> ODEResult: """Execute the solver. Find the appropriate library and execute the solver on the problem to diff --git a/tests/algos/ode/test_lib_scipy_ode.py b/tests/algos/ode/test_lib_scipy_ode.py index 6684098626..a2f285aac8 100644 --- a/tests/algos/ode/test_lib_scipy_ode.py +++ b/tests/algos/ode/test_lib_scipy_ode.py @@ -33,6 +33,7 @@ from numpy.linalg import norm from gemseo.algos.ode.lib_scipy_ode import ScipyODEAlgos from gemseo.algos.ode.ode_problem import ODEProblem +from gemseo.algos.ode.ode_problem import ODEResult from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.problems.ode.orbital_dynamics import OrbitalDynamics from gemseo.problems.ode.van_der_pol import VanDerPol @@ -101,7 +102,9 @@ def test_ode_problem_1d(time_vector) -> None: assert problem.result.time_vector.size == 0 algo_name = "DOP853" - ODESolversFactory().execute(problem, algo_name, first_step=1e-6) + assert isinstance( + ODESolversFactory().execute(problem, algo_name, first_step=1e-6), ODEResult + ) analytical_solution = exp(problem.result.time_vector) assert sqrt(sum((problem.result.state_vector - analytical_solution) ** 2)) < 1e-6 -- GitLab From 2e6133100cb4a59a1d09a25eb5bc150c390bf743 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 19 Apr 2023 11:45:37 +0200 Subject: [PATCH 003/237] Add default values for parameters. --- src/gemseo/disciplines/ode_discipline.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 45b5bf0d65..a2f65a60dd 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -29,6 +29,7 @@ # Contributors: # INITIAL AUTHORS - API and implementation and/or documentation # :author: Francois Gallard +# :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES """A discipline for ODEs. @@ -69,6 +70,9 @@ class ODEDiscipline(MDODiscipline): ode_solver_options: Mapping[str:Any] """The options to pass to the ODE solver.""" + name: str + """The name of the discipline.""" + ode_problem: ODEProblem """The problem to be solved by the ODE solver.""" @@ -93,12 +97,12 @@ class ODEDiscipline(MDODiscipline): def __int__( self, discipline: MDODiscipline, - time_var_name: str, state_var_names: list[str], - initial_time: float, - final_time: float, - ode_solver_name: str, - ode_solver_options: Mapping[str:Any], + time_var_name: str = "time", + initial_time: float = 0, + final_time: float = 1, + ode_solver_name: str = "RK45", + ode_solver_options: Mapping[str:Any] = None, name: str = None, ): self.discipline = discipline -- GitLab From 23a644b77f81c53fe011df97ceef53ba73fcc423 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 19 Apr 2023 11:47:39 +0200 Subject: [PATCH 004/237] Create test file for the ODEDiscipline. --- src/gemseo/disciplines/ode_discipline.py | 14 ----------- tests/disciplines/test_ode_discipline.py | 32 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 tests/disciplines/test_ode_discipline.py diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index a2f65a60dd..5ef3e6a176 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -12,20 +12,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# Copyright 2023 IRT Saint Exupéry, https://www.irt-saintexupery.com -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Contributors: # INITIAL AUTHORS - API and implementation and/or documentation # :author: Francois Gallard diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py new file mode 100644 index 0000000000..7352c160c6 --- /dev/null +++ b/tests/disciplines/test_ode_discipline.py @@ -0,0 +1,32 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Isabelle Santos +# OTHER AUTHORS - MACROSCOPIC CHANGES +"""Tests for disciplines based on ODEs. + +ODE stands for Ordinary Differential Equation. +""" +from __future__ import annotations + +from gemseo.core.discipline import MDODiscipline +from gemseo.disciplines.ode_discipline import ODEDiscipline + + +def test_ode_discipline(): + mdo_disc = MDODiscipline() + ode_disc = ODEDiscipline(mdo_disc) + assert ode_disc.initial_time == 0 -- GitLab From f1c95a1aeaca88c1f86cd39efcba3375c3a5b788 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 25 Apr 2023 15:18:12 +0200 Subject: [PATCH 005/237] Improve variable names. --- tests/disciplines/test_ode_discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 7352c160c6..69fa82e032 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -27,6 +27,6 @@ from gemseo.disciplines.ode_discipline import ODEDiscipline def test_ode_discipline(): - mdo_disc = MDODiscipline() - ode_disc = ODEDiscipline(mdo_disc) - assert ode_disc.initial_time == 0 + mdo_discipline = MDODiscipline(grammar_type=MDODiscipline.GrammarType.JSON) + ode_discipline = ODEDiscipline(mdo_discipline, "x") + assert ode_discipline.initial_time == 0 -- GitLab From 1aa33caebf15892f8edaa1e8713d45e01a080a6e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 25 Apr 2023 15:22:33 +0200 Subject: [PATCH 006/237] Initial commit for springs problem. --- src/gemseo/problems/springs/__init__.py | 20 ++++++++++++++ src/gemseo/problems/springs/springs.py | 27 +++++++++++++++++++ .../problems/springs/springs_input.json | 8 ++++++ .../problems/springs/springs_output.json | 8 ++++++ tests/disciplines/test_ode_discipline.py | 6 +++++ 5 files changed, 69 insertions(+) create mode 100644 src/gemseo/problems/springs/__init__.py create mode 100644 src/gemseo/problems/springs/springs.py create mode 100644 src/gemseo/problems/springs/springs_input.json create mode 100644 src/gemseo/problems/springs/springs_output.json diff --git a/src/gemseo/problems/springs/__init__.py b/src/gemseo/problems/springs/__init__.py new file mode 100644 index 0000000000..bee901460e --- /dev/null +++ b/src/gemseo/problems/springs/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Isabelle Santos +# OTHER AUTHORS - MACROSCOPIC CHANGES +"""A discipline to describe the motion of a series of springs.""" +from __future__ import annotations diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py new file mode 100644 index 0000000000..30c8eb1d8d --- /dev/null +++ b/src/gemseo/problems/springs/springs.py @@ -0,0 +1,27 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Isabelle Santos +# OTHER AUTHORS - MACROSCOPIC CHANGES +r"""The discipline for describing the motion of a series of springs.""" +from __future__ import annotations + +from gemseo.disciplines.ode_discipline import ODEDiscipline + + +class Springs(ODEDiscipline): + def __init__(self) -> None: + super().__init__() diff --git a/src/gemseo/problems/springs/springs_input.json b/src/gemseo/problems/springs/springs_input.json new file mode 100644 index 0000000000..39dd188845 --- /dev/null +++ b/src/gemseo/problems/springs/springs_input.json @@ -0,0 +1,8 @@ +{ + "name": "springs_input", + "required": [], + "properties": {}, + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "id": "#SobieskiAerodynamics_input" +} diff --git a/src/gemseo/problems/springs/springs_output.json b/src/gemseo/problems/springs/springs_output.json new file mode 100644 index 0000000000..1f4f5e56e2 --- /dev/null +++ b/src/gemseo/problems/springs/springs_output.json @@ -0,0 +1,8 @@ +{ + "name": "springs_output", + "required": [], + "properties": {}, + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "id": "#SobieskiAerodynamics_input" +} diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 69fa82e032..bcbe2ecc88 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -22,6 +22,7 @@ ODE stands for Ordinary Differential Equation. """ from __future__ import annotations +from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline @@ -30,3 +31,8 @@ def test_ode_discipline(): mdo_discipline = MDODiscipline(grammar_type=MDODiscipline.GrammarType.JSON) ode_discipline = ODEDiscipline(mdo_discipline, "x") assert ode_discipline.initial_time == 0 + + +def test_springs(): + springs_discipline = create_discipline("Springs") + assert springs_discipline is not None -- GitLab From 313683e74664f099000bd0795d5ab828f8548719 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 26 Apr 2023 11:17:16 +0200 Subject: [PATCH 007/237] update test --- src/gemseo/core/discipline.py | 2 +- src/gemseo/disciplines/ode_discipline.py | 5 +- src/gemseo/problems/springs/springs.py | 66 ++++++++++++++++++++++-- tests/disciplines/test_ode_discipline.py | 13 +++-- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/gemseo/core/discipline.py b/src/gemseo/core/discipline.py index 779a2b40cc..c1761be4ba 100644 --- a/src/gemseo/core/discipline.py +++ b/src/gemseo/core/discipline.py @@ -277,7 +277,7 @@ class MDODiscipline(Serializable): self.data_processor = None self.input_grammar = None self.output_grammar = None - + print(name, grammar_type) # Allow to re-execute the same discipline twice, only if did not fail # and not running self.re_exec_policy = self.ReExecutionPolicy.DONE diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 5ef3e6a176..e546a5b104 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -100,9 +100,8 @@ class ODEDiscipline(MDODiscipline): self.initial_time = initial_time self.final_time = final_time - super(self, ODEDiscipline).__init__( - name=name, grammar_type=discipline.grammar_type - ) + print(name, discipline.grammar_type) + super().__init__(name=name, grammar_type=discipline.grammar_type) self.input_grammar.update(self.discipline.input_grammar) self.output_grammar.update(self.discipline.output_grammar) self.default_inputs = self.discipline.default_inputs.copy() diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 30c8eb1d8d..cc08c0d2e7 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -16,12 +16,72 @@ # INITIAL AUTHORS - API and implementation and/or documentation # :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES -r"""The discipline for describing the motion of a series of springs.""" +r"""The discipline for describing the motion of masses connected by springs. + +Consider a system of $n$ point masses with masses $m_1$, $m_2$,... $m_n$ connected in +series by springs. The displacement of the point masses relative to the position at rest +are denoted by $x_1$, $x_2$,... $x_n$. Each spring has stiffness $k_1$, $k_2$,... +$k_{n+1}$. + +Motion is assumed to only take place in one dimension, along the axes of the springs. + +The extremities of the first and last spring are fixed. This means that by convention, +$x_0 = x_{n+1} = 0$. + + +For $n=2$, the system is as follows: + +.. asciiart:: + + | | + | k1 ________ k2 ________ k3 | + | /\ /\ | | /\ /\ | | /\ /\ | + |__/ \ / \ __| m1 |__/ \ / \ __| m2 |__/ \ / \ __| + | \ / \ / | | \ / \ / | | \ / \ / | + | \/ \/ |________| \/ \/ |________| \/ \/ | + | | | | + ---|---> ---|---> + | x1 | x2 + + +The force of a spring with stiffness $k$ is + +.. math:: + + \vec{F} = -kx + +where $x$ is the displacement of the extremity of the spring. + +Newton's law applied to any point mass $m_i$ can be written as + +.. math:: + + m_i \ddot{x_i} = \sum \vec{F} = k_i (x_{i-1} - x_i) + k_{i+1} (x_{i+1} - x_i) + = k_i x_{i-1} + k_{i+1} x_{i+1} - (k_i + k_{i+1}) x_i + + +This can be re-written as a système of 1st order ordinary differential equations: + +.. math:: + + \left\{ \begin{cases} + \dot{x_i} &= y_i \\ + \dot{y_i} &= + - \frac{k_i + k_{i+1}}{m_i}x_i + + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} + \end{cases} \right. +""" from __future__ import annotations +from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline class Springs(ODEDiscipline): - def __init__(self) -> None: - super().__init__() + def __init__(self, n_springs) -> None: + super().__init__(grammar_type=MDODiscipline.GrammarType.SIMPLE) + + self.n_springs = n_springs + + def _run(self) -> None: + pass diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index bcbe2ecc88..0ebcffdbab 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -28,11 +28,16 @@ from gemseo.disciplines.ode_discipline import ODEDiscipline def test_ode_discipline(): - mdo_discipline = MDODiscipline(grammar_type=MDODiscipline.GrammarType.JSON) - ode_discipline = ODEDiscipline(mdo_discipline, "x") - assert ode_discipline.initial_time == 0 + mdo_discipline = MDODiscipline( + name="disc", grammar_type=MDODiscipline.GrammarType.SIMPLE + ) + ode_disc = ODEDiscipline( + discipline=mdo_discipline, state_var_names=["x"], name="ode" + ) + assert ode_disc.initial_time == 0 def test_springs(): - springs_discipline = create_discipline("Springs") + kwargs = {"n_springs": 2} + springs_discipline = create_discipline("Springs", **kwargs) assert springs_discipline is not None -- GitLab From a4dd46fb3fa2ee96c9813f2b4360336931b2ac90 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 26 Apr 2023 11:55:54 +0200 Subject: [PATCH 008/237] Fix name of init function. --- src/gemseo/disciplines/ode_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index e546a5b104..9085a798f5 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -80,7 +80,7 @@ class ODEDiscipline(MDODiscipline): ode_factory: ODESolversFactory """The factory to build the ODE.""" - def __int__( + def __init__( self, discipline: MDODiscipline, state_var_names: list[str], @@ -91,6 +91,7 @@ class ODEDiscipline(MDODiscipline): ode_solver_options: Mapping[str:Any] = None, name: str = None, ): + """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline.""" self.discipline = discipline self.time_var_name = time_var_name self.state_var_names = state_var_names @@ -100,7 +101,6 @@ class ODEDiscipline(MDODiscipline): self.initial_time = initial_time self.final_time = final_time - print(name, discipline.grammar_type) super().__init__(name=name, grammar_type=discipline.grammar_type) self.input_grammar.update(self.discipline.input_grammar) self.output_grammar.update(self.discipline.output_grammar) -- GitLab From 259d13291661f665f0c846bcee47d540848cbc3a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 26 Apr 2023 11:56:39 +0200 Subject: [PATCH 009/237] Use core function to instantiate a discipline. --- tests/disciplines/test_ode_discipline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 0ebcffdbab..0868b60565 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -28,8 +28,10 @@ from gemseo.disciplines.ode_discipline import ODEDiscipline def test_ode_discipline(): - mdo_discipline = MDODiscipline( - name="disc", grammar_type=MDODiscipline.GrammarType.SIMPLE + mdo_discipline = create_discipline( + "AnalyticDiscipline", + expressions={"y": "x"}, + grammar_type=MDODiscipline.GrammarType.SIMPLE, ) ode_disc = ODEDiscipline( discipline=mdo_discipline, state_var_names=["x"], name="ode" -- GitLab From 5696c0b043ab3c0af99e4fdc719f5a3580e6ee26 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 26 Apr 2023 16:40:37 +0200 Subject: [PATCH 010/237] Continue writing init function for Springs problem. --- src/gemseo/problems/springs/springs.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index cc08c0d2e7..b2930dcbc3 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -73,15 +73,24 @@ This can be re-written as a système of 1st order ordinary differential equation """ from __future__ import annotations +from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline class Springs(ODEDiscipline): def __init__(self, n_springs) -> None: - super().__init__(grammar_type=MDODiscipline.GrammarType.SIMPLE) - self.n_springs = n_springs + self.state_var_names = ["x" + str(e) for e in range(1, self.n_springs + 1)] + self.discipline = create_discipline( + "AnalyticDiscipline", + expressions={}, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + + super().__init__( + discipline=self.discipline, state_var_names=self.state_var_names + ) def _run(self) -> None: pass -- GitLab From 99e6db8a186c8214436c3594394bcf93b907048c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Apr 2023 16:20:51 +0200 Subject: [PATCH 011/237] Remove print statement. --- src/gemseo/core/discipline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gemseo/core/discipline.py b/src/gemseo/core/discipline.py index c1761be4ba..4a651d41bb 100644 --- a/src/gemseo/core/discipline.py +++ b/src/gemseo/core/discipline.py @@ -277,7 +277,6 @@ class MDODiscipline(Serializable): self.data_processor = None self.input_grammar = None self.output_grammar = None - print(name, grammar_type) # Allow to re-execute the same discipline twice, only if did not fail # and not running self.re_exec_policy = self.ReExecutionPolicy.DONE -- GitLab From 1052185178701f15b393be32c17667bb1ec6c221 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Apr 2023 16:36:38 +0200 Subject: [PATCH 012/237] Fix test. --- tests/disciplines/test_ode_discipline.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 0868b60565..fa855f0fb8 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -30,12 +30,10 @@ from gemseo.disciplines.ode_discipline import ODEDiscipline def test_ode_discipline(): mdo_discipline = create_discipline( "AnalyticDiscipline", - expressions={"y": "x"}, + expressions={"state": "time"}, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - ode_disc = ODEDiscipline( - discipline=mdo_discipline, state_var_names=["x"], name="ode" - ) + ode_disc = ODEDiscipline(discipline=mdo_discipline, state_var_names=[]) assert ode_disc.initial_time == 0 -- GitLab From bf0f71e07eacf91b26876c07c46fbb6fc8507e23 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 May 2023 15:59:55 +0200 Subject: [PATCH 013/237] Make a class of each mass. --- src/gemseo/problems/springs/springs.py | 27 +++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index b2930dcbc3..f4b6f07cbf 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -78,19 +78,32 @@ from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline -class Springs(ODEDiscipline): - def __init__(self, n_springs) -> None: - self.n_springs = n_springs - self.state_var_names = ["x" + str(e) for e in range(1, self.n_springs + 1)] +class Mass(ODEDiscipline): + """Describe the behavior of a single mass held by two springs.""" + + def __init__(self, mass: float) -> None: + self.mass = mass self.discipline = create_discipline( "AnalyticDiscipline", expressions={}, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - super().__init__( - discipline=self.discipline, state_var_names=self.state_var_names - ) + super().__init__(discipline=self.discipline) + + def _run(self) -> None: + pass + + +class ChainedMasses(ODEDiscipline): + """Describes the behavior of a series of masses connected by springs.""" + + def __init__(self, stiff: list) -> None: + super().__init__() + self.n_masses = len(stiff) - 1 + state_var_names = ["x" + str(e) for e in range(1, self.n_masses + 1)] + self.input_grammar.update_from_names(state_var_names) + self.output_grammar.update_from_names(state_var_names) def _run(self) -> None: pass -- GitLab From 994c43b1c394173569437ee3c5aa65403478694a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 May 2023 16:00:55 +0200 Subject: [PATCH 014/237] Functions to define the ODEs. --- src/gemseo/problems/springs/springs.py | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index f4b6f07cbf..0280ee99ef 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -73,11 +73,50 @@ This can be re-written as a système of 1st order ordinary differential equation """ from __future__ import annotations +from numpy import array +from numpy.typing import NDArray + from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +def rhs_function( + time: float, + state: NDArray[float], + mass: float, + left_stiff: float, + left_state: NDArray[float], + right_stiff: float, + right_state: NDArray[float], +) -> NDArray[float]: + """Right-hand side function of the differential equation for a single spring.""" + return array( + [ + state[1], + ( + -(left_stiff + right_stiff) * state[0] + + left_stiff * left_state + + right_stiff * right_state + ) + / mass, + ] + ) + + +def rhs_jacobian( + time: float, + state: NDArray[float], + mass: float, + left_stiff: float, + left_state: NDArray[float], + right_stiff: float, + right_state: NDArray[float], +) -> NDArray[float]: + """Jacobian of the right-hand side of the ODE for a single spring.""" + return array([[0, -(left_stiff + right_stiff) * state[0] / mass], [1, 0]]) + + class Mass(ODEDiscipline): """Describe the behavior of a single mass held by two springs.""" -- GitLab From 47568d44968b26e789b11ceec2ae85e217a4edf5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 May 2023 10:39:45 +0200 Subject: [PATCH 015/237] Add type hints. --- src/gemseo/disciplines/ode_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 9085a798f5..2e829ebdcb 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -111,7 +111,7 @@ class ODEDiscipline(MDODiscipline): self._init_ode_problem() - def _init_ode_problem(self): + def _init_ode_problem(self) -> None: """Initialize the ODE problem with the user-defined parameters.""" initial_state = concatenate_dict_of_arrays_to_array( self.discipline.default_outputs, names=self.state_var_names @@ -134,7 +134,7 @@ class ODEDiscipline(MDODiscipline): for name in self.state_var_names } - def _run(self): + def _run(self) -> None: self.__generator_default_inputs.update(self.local_data) self.ode_problem.time_vector = self.local_data[self.time_var_name] ode_result = self.ode_factory.execute( -- GitLab From 1b9cdefdc4fc13fa0690687d745890b9ce1a0301 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 May 2023 17:22:34 +0200 Subject: [PATCH 016/237] Type hints. --- src/gemseo/disciplines/ode_discipline.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 2e829ebdcb..206e104ad2 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -91,7 +91,18 @@ class ODEDiscipline(MDODiscipline): ode_solver_options: Mapping[str:Any] = None, name: str = None, ): - """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline.""" + """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. + + Args: + discipline: The discipline. + state_var_names: The names of the state variables. + time_var_name: The name of the time variable. + initial_time: The start of the time interval to perform the integration on. + final_time: The end of the time interval to perform the integration on. + ode_solver_name: The name of the solver used to solve the ODE. + ode_solver_options: The options to pass to the ODE solver. + name: The name of the discipline. + """ self.discipline = discipline self.time_var_name = time_var_name self.state_var_names = state_var_names -- GitLab From c37f3cce16762973788283650c203da99981bb5e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 May 2023 17:23:14 +0200 Subject: [PATCH 017/237] `initial_time` and `final_time` are attributes of the ODEProblem. --- src/gemseo/disciplines/ode_discipline.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 206e104ad2..1bdfc9758a 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -26,6 +26,8 @@ from __future__ import annotations from typing import Any from typing import Mapping +from numpy.typing import NDArray + from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.discipline import MDODiscipline @@ -62,12 +64,6 @@ class ODEDiscipline(MDODiscipline): ode_problem: ODEProblem """The problem to be solved by the ODE solver.""" - initial_time: float - """The start of the time interval to perform the integration on.""" - - final_time: float - """The end of the time interval to perform the integration on.""" - input_grammar: BaseGrammar """The input grammar of the discipline.""" @@ -109,8 +105,6 @@ class ODEDiscipline(MDODiscipline): self.ode_solver_name = ode_solver_name self.ode_solver_options = ode_solver_options self.ode_problem = None - self.initial_time = initial_time - self.final_time = final_time super().__init__(name=name, grammar_type=discipline.grammar_type) self.input_grammar.update(self.discipline.input_grammar) @@ -120,9 +114,11 @@ class ODEDiscipline(MDODiscipline): self.__generator_default_inputs = {} - self._init_ode_problem() + self._init_ode_problem(initial_time, final_time) - def _init_ode_problem(self) -> None: + def _init_ode_problem( + self, initial_time: float, final_time: float, time_vector: NDArray[float] + ) -> None: """Initialize the ODE problem with the user-defined parameters.""" initial_state = concatenate_dict_of_arrays_to_array( self.discipline.default_outputs, names=self.state_var_names @@ -134,10 +130,11 @@ class ODEDiscipline(MDODiscipline): ) self.ode_problem = ODEProblem( ode_func, - initial_state=initial_state, - initial_time=self.initial_time, - final_time=self.final_time, jac=ode_func.jac, + initial_state=initial_state, + initial_time=initial_time, + final_time=final_time, + time_vector=time_vector, ) self.__names_to_sizes = { -- GitLab From d07731c241bf327b239dad3f79563f61e23420f2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 May 2023 17:24:00 +0200 Subject: [PATCH 018/237] Right-hand side functions should not be used outside of this module. --- src/gemseo/problems/springs/springs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 0280ee99ef..0320b44db8 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -81,7 +81,7 @@ from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline -def rhs_function( +def _rhs_function( time: float, state: NDArray[float], mass: float, @@ -104,7 +104,7 @@ def rhs_function( ) -def rhs_jacobian( +def _rhs_jacobian( time: float, state: NDArray[float], mass: float, -- GitLab From a5910cb58b1c9bd850e9024543097e8ff4b65278 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 May 2023 17:24:54 +0200 Subject: [PATCH 019/237] General structure for Springs module. --- src/gemseo/problems/springs/springs.py | 53 +++++++++++++++++++------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 0320b44db8..c9e3dbe4fc 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -120,29 +120,56 @@ def _rhs_jacobian( class Mass(ODEDiscipline): """Describe the behavior of a single mass held by two springs.""" - def __init__(self, mass: float) -> None: + mass: float + """The value of the mass.""" + + left_stiff: float + """The stiffness of the spring on the left-hand side of the mass.""" + + right_stiff: float + """The stiffness of the spring on the right-hand side of the mass.""" + + def __init__( + self, mass: float, left_stiff: float, right_stiff: float, state_name: str = "x" + ) -> None: self.mass = mass + self.left_stiff = left_stiff + self.right_stiff = right_stiff + self.state_name = state_name + self.discipline = create_discipline( "AnalyticDiscipline", - expressions={}, + expressions={state_name: state_name}, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - super().__init__(discipline=self.discipline) - - def _run(self) -> None: - pass + super().__init__( + discipline=self.discipline, + state_var_names=[self.state_name], + time_var_name="time", + ) -class ChainedMasses(ODEDiscipline): +class ChainedMasses: """Describes the behavior of a series of masses connected by springs.""" - def __init__(self, stiff: list) -> None: - super().__init__() - self.n_masses = len(stiff) - 1 - state_var_names = ["x" + str(e) for e in range(1, self.n_masses + 1)] - self.input_grammar.update_from_names(state_var_names) - self.output_grammar.update_from_names(state_var_names) + def __init__(self, stiffnesses: list, masses: list) -> None: + self.n_masses = len(masses) + if not len(stiffnesses) == self.n_masses + 1: + from warnings import warn + + warn("Stiffnesses and masses have incoherent lengths.") + + self.state_var_names = ["x" + str(e) for e in range(1, self.n_masses + 1)] + self.disciplines = [ + Mass(m, ls, rs, svn) + for m, ls, rs, svn in zip( + masses, + stiffnesses[: self.n_masses], + stiffnesses[:-1], + self.state_var_names, + ) + ] def _run(self) -> None: pass -- GitLab From 08084498c9e8f42c7f60264fd69921e0574186f4 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 May 2023 17:25:43 +0200 Subject: [PATCH 020/237] More tests. --- tests/disciplines/test_ode_discipline.py | 37 ++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index fa855f0fb8..8c9e812b33 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -22,9 +22,13 @@ ODE stands for Ordinary Differential Equation. """ from __future__ import annotations +from warnings import catch_warnings +from warnings import simplefilter + from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.problems.springs.springs import ChainedMasses def test_ode_discipline(): @@ -34,10 +38,37 @@ def test_ode_discipline(): grammar_type=MDODiscipline.GrammarType.SIMPLE, ) ode_disc = ODEDiscipline(discipline=mdo_discipline, state_var_names=[]) + assert ode_disc is not None assert ode_disc.initial_time == 0 + assert ode_disc.final_time == 1 + + +def test_bad_discipline(): + pass + + +def test_execute_ode_discipline(): + mdo_discipline = create_discipline( + "AnalyticDiscipline", + expressions={"state": "time"}, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_disc = ODEDiscipline(discipline=mdo_discipline, state_var_names=["state"]) + ode_disc.execute() -def test_springs(): - kwargs = {"n_springs": 2} - springs_discipline = create_discipline("Springs", **kwargs) +def test_chained_masses(): + kwargs = {"stiffnesses": [1, 1, 1], "masses": [1, 1]} + springs_discipline = ChainedMasses(**kwargs) assert springs_discipline is not None + + +def test_chained_masses_wrong_lengths(): + kwargs = {"stiffnesses": [1], "masses": [1]} + with catch_warnings() as warn: + simplefilter("default") + springs_discipline = ChainedMasses(**kwargs) + assert springs_discipline is not None + assert len(warn) == 1 + assert issubclass(warn[-1].category, UserWarning) + assert "lengths" in str(warn[-1].message) -- GitLab From 724bc4c6ee8425d373f7cb306f4e81e4dd69d973 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 7 Jun 2023 15:26:31 +0200 Subject: [PATCH 021/237] Fix merge. --- src/gemseo/disciplines/ode_discipline.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 1bdfc9758a..328ee0e280 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -64,6 +64,12 @@ class ODEDiscipline(MDODiscipline): ode_problem: ODEProblem """The problem to be solved by the ODE solver.""" + initial_time: float + """The start of the time interval to perform the integration on.""" + + final_time: float + """The end of the time interval to perform the integration on.""" + input_grammar: BaseGrammar """The input grammar of the discipline.""" @@ -106,7 +112,12 @@ class ODEDiscipline(MDODiscipline): self.ode_solver_options = ode_solver_options self.ode_problem = None - super().__init__(name=name, grammar_type=discipline.grammar_type) + self.initial_time = initial_time + self.final_time = final_time + + super(self, ODEDiscipline).__init__( + name=name, grammar_type=discipline.grammar_type + ) self.input_grammar.update(self.discipline.input_grammar) self.output_grammar.update(self.discipline.output_grammar) self.default_inputs = self.discipline.default_inputs.copy() -- GitLab From 9a1b006910c971599e9305a67e1f9355496ba811 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 10 May 2023 09:28:42 +0200 Subject: [PATCH 022/237] Fix discipline creation. --- src/gemseo/disciplines/ode_discipline.py | 7 +------ tests/disciplines/test_ode_discipline.py | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 328ee0e280..7ef8dd1ea2 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -26,8 +26,6 @@ from __future__ import annotations from typing import Any from typing import Mapping -from numpy.typing import NDArray - from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.discipline import MDODiscipline @@ -127,9 +125,7 @@ class ODEDiscipline(MDODiscipline): self._init_ode_problem(initial_time, final_time) - def _init_ode_problem( - self, initial_time: float, final_time: float, time_vector: NDArray[float] - ) -> None: + def _init_ode_problem(self, initial_time: float, final_time: float) -> None: """Initialize the ODE problem with the user-defined parameters.""" initial_state = concatenate_dict_of_arrays_to_array( self.discipline.default_outputs, names=self.state_var_names @@ -145,7 +141,6 @@ class ODEDiscipline(MDODiscipline): initial_state=initial_state, initial_time=initial_time, final_time=final_time, - time_vector=time_vector, ) self.__names_to_sizes = { diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 8c9e812b33..24fea02118 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -39,8 +39,6 @@ def test_ode_discipline(): ) ode_disc = ODEDiscipline(discipline=mdo_discipline, state_var_names=[]) assert ode_disc is not None - assert ode_disc.initial_time == 0 - assert ode_disc.final_time == 1 def test_bad_discipline(): -- GitLab From 59e78a280f81db468e4eb567ac2a6122a738a0ec Mon Sep 17 00:00:00 2001 From: Francois GALLARD Date: Wed, 10 May 2023 10:46:01 +0200 Subject: [PATCH 023/237] ODEDiscipline executes with bugs --- src/gemseo/algos/ode/lib_scipy_ode.py | 5 +- src/gemseo/disciplines/ode_discipline.py | 62 ++++++++++++++++++------ tests/disciplines/test_ode_discipline.py | 33 +++++++------ 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/gemseo/algos/ode/lib_scipy_ode.py b/src/gemseo/algos/ode/lib_scipy_ode.py index 1c9353df8a..ecea84cd58 100644 --- a/src/gemseo/algos/ode/lib_scipy_ode.py +++ b/src/gemseo/algos/ode/lib_scipy_ode.py @@ -136,8 +136,11 @@ class ScipyODEAlgos(ODESolverLib): LOGGER.warning(solution.message) self.problem.result.state_vector = solution.y - self.problem.result.time_vector = solution.t + if self.problem.time_vector is None: + self.problem.time_vector = solution.t self.problem.result.n_func_evaluations = solution.nfev self.problem.result.n_jac_evaluations = solution.njev + print("lib scipy solution.y ",solution.y) + return self.problem.result diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 7ef8dd1ea2..4b32627381 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -26,6 +26,9 @@ from __future__ import annotations from typing import Any from typing import Mapping +from numpy import concatenate, array +from numpy.typing import NDArray + from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.discipline import MDODiscipline @@ -50,6 +53,9 @@ class ODEDiscipline(MDODiscipline): state_var_names: list[str] """The names of the state variables.""" + state_dot_var_names: list[str] + """The names of the state variables derivatives wrt time.""" + ode_solver_name: str """The name of the solver used to solve the ODE.""" @@ -84,9 +90,11 @@ class ODEDiscipline(MDODiscipline): self, discipline: MDODiscipline, state_var_names: list[str], + state_dot_var_names: list[str], + time_vector:NDArray, time_var_name: str = "time", - initial_time: float = 0, - final_time: float = 1, + initial_time: float = 0., + final_time: float = 1., ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -106,9 +114,14 @@ class ODEDiscipline(MDODiscipline): self.discipline = discipline self.time_var_name = time_var_name self.state_var_names = state_var_names + self.state_dot_var_names = state_dot_var_names self.ode_solver_name = ode_solver_name self.ode_solver_options = ode_solver_options self.ode_problem = None + self.ode_mdo_func =None + self.time_vector=time_vector + + assert len(self.state_var_names)==len(self.state_dot_var_names) self.initial_time = initial_time self.final_time = final_time @@ -117,7 +130,7 @@ class ODEDiscipline(MDODiscipline): name=name, grammar_type=discipline.grammar_type ) self.input_grammar.update(self.discipline.input_grammar) - self.output_grammar.update(self.discipline.output_grammar) + self.output_grammar.update(self.discipline.output_grammar, exclude_names=state_dot_var_names) self.default_inputs = self.discipline.default_inputs.copy() self.ode_factory = ODESolversFactory() @@ -125,36 +138,57 @@ class ODEDiscipline(MDODiscipline): self._init_ode_problem(initial_time, final_time) + + def __ode_func(self, time, state): + return self.ode_mdo_func(concatenate((array([time]),state))) + + def __ode_func_jac(self, time,state): + return self.ode_mdo_func.jac(concatenate((array([time]), state))) + def _init_ode_problem(self, initial_time: float, final_time: float) -> None: """Initialize the ODE problem with the user-defined parameters.""" - initial_state = concatenate_dict_of_arrays_to_array( - self.discipline.default_outputs, names=self.state_var_names - ) - ode_func = MDODisciplineAdapterGenerator(self.discipline).get_function( - input_names=[self.time_var_name], - output_names=self.state_var_names, - default_inputs=self.__generator_default_inputs, + try: + initial_state = concatenate_dict_of_arrays_to_array( + self.discipline.default_inputs, names=self.state_var_names + ) + except KeyError as err: + raise ValueError(f"Missing default inputs {err} in discipline.") + self.ode_mdo_func = MDODisciplineAdapterGenerator(self.discipline).get_function( + input_names=[self.time_var_name]+self.state_var_names, + output_names=self.state_dot_var_names, + #default_inputs=self.__generator_default_inputs, ) + + self.ode_problem = ODEProblem( - ode_func, - jac=ode_func.jac, + self.__ode_func, + #jac=self.__ode_func_jac, initial_state=initial_state, initial_time=initial_time, final_time=final_time, + time_vector=self.time_vector ) self.__names_to_sizes = { - name: self.discipline.default_outputs[name].size + name: self.discipline.default_inputs[name].size for name in self.state_var_names } def _run(self) -> None: self.__generator_default_inputs.update(self.local_data) self.ode_problem.time_vector = self.local_data[self.time_var_name] + if self.ode_solver_options is not None: + options=self.ode_solver_options + else: + options={} ode_result = self.ode_factory.execute( - self.ode_problem, self.ode_solver_name, **self.ode_solver_options + self.ode_problem, self.ode_solver_name, **options ) + if not ode_result.is_converged: + raise RuntimeError(f"ODE solver {ode_result.solver_name} failed to converge. Message ={ode_result.solver_message}") + print("ode_result state_vector",ode_result.state_vector) spl_state = split_array_to_dict_of_arrays( ode_result.state_vector, self.__names_to_sizes, self.state_var_names ) + print("Split state ",spl_state) self.local_data.update(spl_state) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 24fea02118..35f46cf3c9 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,36 +25,39 @@ from __future__ import annotations from warnings import catch_warnings from warnings import simplefilter +import pytest +from numpy import array, linspace + from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.springs.springs import ChainedMasses +@pytest.fixture +def oscillator(): + def oscillator_ode(time = array([0.]), state=array([0., 1.])): + state_dot= array([state[1], -2*state[0]]) + print("state_dot",state_dot) + return state_dot -def test_ode_discipline(): mdo_discipline = create_discipline( - "AnalyticDiscipline", - expressions={"state": "time"}, + "AutoPyDiscipline", + py_func=oscillator_ode , grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - ode_disc = ODEDiscipline(discipline=mdo_discipline, state_var_names=[]) - assert ode_disc is not None + return mdo_discipline + +def test_ode_discipline(oscillator): + ode_disc = ODEDiscipline(discipline=oscillator, state_var_names=["state"], state_dot_var_names=["state_dot"], final_time=10, + time_vector=linspace(0.,10,30)) + out=ode_disc.execute() + raise ValueError(out["state"]) def test_bad_discipline(): pass -def test_execute_ode_discipline(): - mdo_discipline = create_discipline( - "AnalyticDiscipline", - expressions={"state": "time"}, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_disc = ODEDiscipline(discipline=mdo_discipline, state_var_names=["state"]) - ode_disc.execute() - - def test_chained_masses(): kwargs = {"stiffnesses": [1, 1, 1], "masses": [1, 1]} springs_discipline = ChainedMasses(**kwargs) -- GitLab From 16723980f6f2ed5522a76d15ca63f048263d929c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 May 2023 15:07:08 +0200 Subject: [PATCH 024/237] Test multiple conditions that should raise an error. --- tests/disciplines/test_ode_discipline.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 35f46cf3c9..e6a37ed5f3 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -22,9 +22,6 @@ ODE stands for Ordinary Differential Equation. """ from __future__ import annotations -from warnings import catch_warnings -from warnings import simplefilter - import pytest from numpy import array, linspace @@ -64,12 +61,14 @@ def test_chained_masses(): assert springs_discipline is not None -def test_chained_masses_wrong_lengths(): - kwargs = {"stiffnesses": [1], "masses": [1]} - with catch_warnings() as warn: - simplefilter("default") - springs_discipline = ChainedMasses(**kwargs) - assert springs_discipline is not None - assert len(warn) == 1 - assert issubclass(warn[-1].category, UserWarning) - assert "lengths" in str(warn[-1].message) +@pytest.mark.parametrize( + "kwargs", + [ + {"stiffnesses": [1], "masses": [1]}, + {"stiffnesses": [1], "masses": [1, 1]}, + {"stiffnesses": [1, 1, 1], "masses": [1]}, + ], +) +def test_chained_masses_wrong_lengths(kwargs): + with pytest.raises(ValueError, match="incoherent lengths"): + springs_discipline = ChainedMasses(**kwargs) # noqa: F841 -- GitLab From d43902b57ac4fbfb8f5341fccaf9b942e6912a07 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 31 May 2023 10:49:24 +0200 Subject: [PATCH 025/237] General structure of the documentation for ODEDisciplines. --- doc_src/disciplines/ode_discipline.rst | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 doc_src/disciplines/ode_discipline.rst diff --git a/doc_src/disciplines/ode_discipline.rst b/doc_src/disciplines/ode_discipline.rst new file mode 100644 index 0000000000..12f3d5a2a8 --- /dev/null +++ b/doc_src/disciplines/ode_discipline.rst @@ -0,0 +1,32 @@ +.. + Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com + + This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 + International License. To view a copy of this license, visit + http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative + Commons, PO Box 1866, Mountain View, CA 94042, USA. + +.. + Contributors: + :author: Isabelle Santos + +.. _odediscipline: + +What is an ODE discipline? +========================== + +ODE stands for Ordinary Differential Equation. + +An ODE discipline is + +How to build an ODE discipline? +=============================== + +From an existing discipline +*************************** + +From an existing ODE problem +**************************** + +How to execute an ODE discipline? +================================= -- GitLab From e611d77d5cb415682c25ef8fa0a70fcacd717d8c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 8 Jun 2023 10:54:59 +0200 Subject: [PATCH 026/237] Fix math in docstring. --- src/gemseo/problems/springs/springs.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index c9e3dbe4fc..acf39b0d4e 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -18,18 +18,18 @@ # OTHER AUTHORS - MACROSCOPIC CHANGES r"""The discipline for describing the motion of masses connected by springs. -Consider a system of $n$ point masses with masses $m_1$, $m_2$,... $m_n$ connected in -series by springs. The displacement of the point masses relative to the position at rest -are denoted by $x_1$, $x_2$,... $x_n$. Each spring has stiffness $k_1$, $k_2$,... -$k_{n+1}$. +Consider a system of :math:`n` point masses with masses :math:`m_1$, :math:`m_2$,... +:math:`m_n` connected in series by springs. The displacement of the point masses relative +to the position at rest are denoted by :math:`x_1`, :math:`x_2`,... :math:`x_n`. Each +spring has stiffness :math:`k_1`, :math:`k_2`,... :math:`k_{n+1}`. Motion is assumed to only take place in one dimension, along the axes of the springs. The extremities of the first and last spring are fixed. This means that by convention, -$x_0 = x_{n+1} = 0$. +:math:`x_0 = x_{n+1} = 0`. -For $n=2$, the system is as follows: +For :math:`n=2`, the system is as follows: .. asciiart:: @@ -44,15 +44,15 @@ For $n=2$, the system is as follows: | x1 | x2 -The force of a spring with stiffness $k$ is +The force of a spring with stiffness :math:`k` is .. math:: \vec{F} = -kx -where $x$ is the displacement of the extremity of the spring. +where :math:`x` is the displacement of the extremity of the spring. -Newton's law applied to any point mass $m_i$ can be written as +Newton's law applied to any point mass :math:`m_i` can be written as .. math:: -- GitLab From 1faee2f2edd738f5768359dbacc78272d65026f2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 23 Jun 2023 08:54:45 +0200 Subject: [PATCH 027/237] Recover lost code. --- src/gemseo/disciplines/ode_discipline.py | 251 ++++++++++++++++------- tests/disciplines/test_ode_discipline.py | 31 +-- 2 files changed, 193 insertions(+), 89 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 4b32627381..b773d6be3a 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -26,30 +26,29 @@ from __future__ import annotations from typing import Any from typing import Mapping -from numpy import concatenate, array +from numpy import array +from numpy import concatenate +from numpy import vsplit from numpy.typing import NDArray from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.discipline import MDODiscipline -from gemseo.core.grammars.base_grammar import BaseGrammar -from gemseo.core.grammars.defaults import Defaults from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( MDODisciplineAdapterGenerator, ) from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array -from gemseo.utils.data_conversion import split_array_to_dict_of_arrays -class ODEDiscipline(MDODiscipline): +class BaseODEDiscipline(MDODiscipline): r"""A discipline based on the solution to an ODE.""" - discipline: MDODiscipline - """The discipline.""" - time_var_name: str """The name of the time variable.""" + time_vector: NDArray + """The times at which the solution is evaluated.""" + state_var_names: list[str] """The names of the state variables.""" @@ -62,87 +61,151 @@ class ODEDiscipline(MDODiscipline): ode_solver_options: Mapping[str:Any] """The options to pass to the ODE solver.""" - name: str - """The name of the discipline.""" - ode_problem: ODEProblem """The problem to be solved by the ODE solver.""" - initial_time: float - """The start of the time interval to perform the integration on.""" + ode_factory: ODESolversFactory + """The factory to build the ODE.""" - final_time: float - """The end of the time interval to perform the integration on.""" + def __init__( + self, + state_var_names: list[str], + time_vector: NDArray, + ode_solver_name: str = "RK45", + ode_solver_options: Mapping[str:Any] = None, + name: str = None, + grammar_type=MDODiscipline.GrammarType.JSON, + ): + """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. - input_grammar: BaseGrammar - """The input grammar of the discipline.""" + Either the ode_problem or the discipline must be passed. - output_grammar: BaseGrammar - """The output grammar of the discipline.""" + Args: + state_var_names: The names of the state variables. + time_vector: The times at which the solution is evaluated. + ode_solver_name: The name of the solver used to solve the ODE. + ode_solver_options: The options to pass to the ODE solver. + name: The name of the discipline. + grammar_type: The way the grammar is defined. + """ + self.state_var_names = state_var_names + self.ode_solver_name = ode_solver_name + self.ode_solver_options = ode_solver_options + self.ode_problem = None + self.time_vector = time_vector - default_inputs: Defaults - """The default inputs for the discipline.""" + super().__init__(name=name, grammar_type=grammar_type) - ode_factory: ODESolversFactory - """The factory to build the ODE.""" + self.output_grammar.update_from_names(state_var_names + ["time_vector"]) + self.ode_factory = ODESolversFactory() + + self.__generator_default_inputs = {} + + def _run(self) -> None: + if self.ode_solver_options is not None: + options = self.ode_solver_options + else: + options = {} + ode_result = self.ode_factory.execute( + self.ode_problem, self.ode_solver_name, **options + ) + if not ode_result.is_converged: + raise RuntimeError( + f"ODE solver {ode_result.solver_name} failed to converge. \ + Message = {ode_result.solver_message}" + ) + + split_state = vsplit( + ode_result.state_vector, + len(self.state_var_names), + ) + + self.local_data.update( + { + name: val.flatten() + for name, val in zip( + self.state_var_names, + split_state, + ) + } + ) + self.local_data["time_vector"] = self.time_vector + + +class ODEDiscipline(BaseODEDiscipline): + r"""A discipline based on the solution to an ODE.""" + + discipline: MDODiscipline + """The discipline.""" + + time_var_name: str + """The name of the time variable.""" + + state_dot_var_names: list[str] + """The names of the state variables derivatives wrt time.""" def __init__( self, discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], - time_vector:NDArray, + time_vector: NDArray, time_var_name: str = "time", - initial_time: float = 0., - final_time: float = 1., + initial_time: float = 0.0, + final_time: float = 1.0, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, ): """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. + Either the ode_problem or the discipline must be passed. + Args: discipline: The discipline. - state_var_names: The names of the state variables. - time_var_name: The name of the time variable. + state_var_names: The names of the state variables in the discipline's input + grammar. + state_dot_var_names: The names of the time derivatives of the state variable + in the discipline's input grammars. + time_vector: The times at which the solution is evaluated. + time_var_name: The name of the time variable in the discipline's input + grammar. initial_time: The start of the time interval to perform the integration on. final_time: The end of the time interval to perform the integration on. - ode_solver_name: The name of the solver used to solve the ODE. - ode_solver_options: The options to pass to the ODE solver. - name: The name of the discipline. + ode_solver_name: The name of the solver that should be used. + ode_solver_options: The options to be passed to the solver. + name: The name of the discipline """ self.discipline = discipline self.time_var_name = time_var_name - self.state_var_names = state_var_names self.state_dot_var_names = state_dot_var_names - self.ode_solver_name = ode_solver_name - self.ode_solver_options = ode_solver_options - self.ode_problem = None - self.ode_mdo_func =None - self.time_vector=time_vector - - assert len(self.state_var_names)==len(self.state_dot_var_names) + self.ode_mdo_func = None + + super().__init__( + name=name, + grammar_type=discipline.grammar_type, + state_var_names=state_var_names, + ode_solver_options=ode_solver_options, + ode_solver_name=ode_solver_name, + time_vector=time_vector, + ) + assert len(self.state_var_names) == len(self.state_dot_var_names) - self.initial_time = initial_time - self.final_time = final_time + self.__generator_default_inputs = {} + self.__names_to_sizes = { + name: self.time_vector.size for name in self.state_var_names + } - super(self, ODEDiscipline).__init__( - name=name, grammar_type=discipline.grammar_type - ) self.input_grammar.update(self.discipline.input_grammar) - self.output_grammar.update(self.discipline.output_grammar, exclude_names=state_dot_var_names) self.default_inputs = self.discipline.default_inputs.copy() - self.ode_factory = ODESolversFactory() - - self.__generator_default_inputs = {} self._init_ode_problem(initial_time, final_time) - def __ode_func(self, time, state): - return self.ode_mdo_func(concatenate((array([time]),state))) + val = self.ode_mdo_func(concatenate((array([time]), state))) + return val - def __ode_func_jac(self, time,state): + def __ode_func_jac(self, time, state): return self.ode_mdo_func.jac(concatenate((array([time]), state))) def _init_ode_problem(self, initial_time: float, final_time: float) -> None: @@ -154,41 +217,77 @@ class ODEDiscipline(MDODiscipline): except KeyError as err: raise ValueError(f"Missing default inputs {err} in discipline.") self.ode_mdo_func = MDODisciplineAdapterGenerator(self.discipline).get_function( - input_names=[self.time_var_name]+self.state_var_names, + input_names=[self.time_var_name] + self.state_var_names, output_names=self.state_dot_var_names, - #default_inputs=self.__generator_default_inputs, + # default_inputs=self.__generator_default_inputs, ) - self.ode_problem = ODEProblem( self.__ode_func, - #jac=self.__ode_func_jac, + # jac=self.__ode_func_jac, initial_state=initial_state, initial_time=initial_time, final_time=final_time, - time_vector=self.time_vector + time_vector=self.time_vector, ) - self.__names_to_sizes = { - name: self.discipline.default_inputs[name].size - for name in self.state_var_names - } - def _run(self) -> None: self.__generator_default_inputs.update(self.local_data) - self.ode_problem.time_vector = self.local_data[self.time_var_name] - if self.ode_solver_options is not None: - options=self.ode_solver_options - else: - options={} - ode_result = self.ode_factory.execute( - self.ode_problem, self.ode_solver_name, **options - ) - if not ode_result.is_converged: - raise RuntimeError(f"ODE solver {ode_result.solver_name} failed to converge. Message ={ode_result.solver_message}") - print("ode_result state_vector",ode_result.state_vector) - spl_state = split_array_to_dict_of_arrays( - ode_result.state_vector, self.__names_to_sizes, self.state_var_names + super()._run() + + +class ODEDisciplineFromProblem(BaseODEDiscipline): + r"""A discipline based on the solution to an ODE.""" + + def __init__( + self, + ode_problem: ODEProblem, + state_var_names: list[str], + ode_solver_name: str = "RK45", + ode_solver_options: Mapping[str:Any] = None, + name: str = None, + grammar_type=MDODiscipline.GrammarType.JSON, + additional_input_var_names: list[str] | None = None, + input_var_to_kwargs: Mapping | None = None, + ): + """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. + + Either the ode_problem or the discipline must be passed. + + Args: + ode_problem: The ODE problem to be solved at each execution. + state_var_names: The names of the state variables. + ode_solver_name: The name of the solver used to solve the ODE. + ode_solver_options: The options to pass to the ODE solver. + name: The name of the discipline. + grammar_type: The type of grammar to use for the discipline. + additional_input_var_names: The additional input variables names that will be + passed to the ode_problem as keyword arguments. + input_var_to_kwargs: The mapping of keyword arguments for the ODEProblem. + """ + super().__init__( + name=name, + grammar_type=grammar_type, + state_var_names=state_var_names, + ode_solver_options=ode_solver_options, + ode_solver_name=ode_solver_name, + time_vector=ode_problem.time_vector, ) - print("Split state ",spl_state) - self.local_data.update(spl_state) + self.ode_problem = ode_problem + self.additional_input_var_names = additional_input_var_names + self.input_grammar.update_from_names(additional_input_var_names) + self.input_var_to_kwargs = input_var_to_kwargs + + def _run(self) -> None: + if self.input_var_to_kwargs is None: + kwargs = { + var_name: self.local_data[var_name] + for var_name in self.additional_input_var_names + } + else: + kwargs = { + self.input_var_to_kwargs[var_name]: self.local_data[var_name] + for var_name in self.additional_input_var_names + } + self.ode_problem.ode_funcs_kwargs = kwargs + super()._run() diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index e6a37ed5f3..b2d55f1510 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -23,38 +23,43 @@ ODE stands for Ordinary Differential Equation. from __future__ import annotations import pytest -from numpy import array, linspace - from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.springs.springs import ChainedMasses +from numpy import array +from numpy import linspace + @pytest.fixture def oscillator(): - def oscillator_ode(time = array([0.]), state=array([0., 1.])): - state_dot= array([state[1], -2*state[0]]) - print("state_dot",state_dot) + _time = array([0.0]) + _state = array([0.0, 1.0]) + + def oscillator_ode(time=_time, state=_state): + state_dot = array([state[1], -2 * state[0]]) return state_dot mdo_discipline = create_discipline( "AutoPyDiscipline", - py_func=oscillator_ode , + py_func=oscillator_ode, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) return mdo_discipline + def test_ode_discipline(oscillator): - ode_disc = ODEDiscipline(discipline=oscillator, state_var_names=["state"], state_dot_var_names=["state_dot"], final_time=10, - time_vector=linspace(0.,10,30)) - out=ode_disc.execute() + ode_disc = ODEDiscipline( + discipline=oscillator, + state_var_names=["state"], + state_dot_var_names=["state_dot"], + final_time=10, + time_vector=linspace(0.0, 10, 30), + ) + out = ode_disc.execute() raise ValueError(out["state"]) -def test_bad_discipline(): - pass - - def test_chained_masses(): kwargs = {"stiffnesses": [1, 1, 1], "masses": [1, 1]} springs_discipline = ChainedMasses(**kwargs) -- GitLab From b1ea05bf76b17ff419d191407e260d0199dbb845 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Jun 2023 16:21:07 +0200 Subject: [PATCH 028/237] Improve plot. --- doc_src/_examples/ode/plot_van_der_pol.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc_src/_examples/ode/plot_van_der_pol.py b/doc_src/_examples/ode/plot_van_der_pol.py index d4666e402a..8d36d258b3 100644 --- a/doc_src/_examples/ode/plot_van_der_pol.py +++ b/doc_src/_examples/ode/plot_van_der_pol.py @@ -144,6 +144,8 @@ ODESolversFactory().execute(ode_problem) plt.plot(ode_problem.result.time_vector, ode_problem.result.state_vector[0]) plt.plot(ode_problem.result.time_vector, ode_problem.result.state_vector[1]) +plt.legend() +plt.xlabel("time") plt.show() # %% -- GitLab From c49b5ff9e269177c98f62e99e8c5f106b763accf Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 30 Jun 2023 14:19:25 +0200 Subject: [PATCH 029/237] Make ODE Discipline run. --- src/gemseo/algos/ode/lib_scipy_ode.py | 2 -- src/gemseo/disciplines/ode_discipline.py | 11 ++++------- src/gemseo/problems/springs/springs.py | 11 +++++++++-- tests/disciplines/test_ode_discipline.py | 12 +++++++++--- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/gemseo/algos/ode/lib_scipy_ode.py b/src/gemseo/algos/ode/lib_scipy_ode.py index ecea84cd58..382eabf024 100644 --- a/src/gemseo/algos/ode/lib_scipy_ode.py +++ b/src/gemseo/algos/ode/lib_scipy_ode.py @@ -141,6 +141,4 @@ class ScipyODEAlgos(ODESolverLib): self.problem.result.n_func_evaluations = solution.nfev self.problem.result.n_jac_evaluations = solution.njev - print("lib scipy solution.y ",solution.y) - return self.problem.result diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index b773d6be3a..76990afac4 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -61,7 +61,7 @@ class BaseODEDiscipline(MDODiscipline): ode_solver_options: Mapping[str:Any] """The options to pass to the ODE solver.""" - ode_problem: ODEProblem + ode_problem: ODEProblem | None """The problem to be solved by the ODE solver.""" ode_factory: ODESolversFactory @@ -149,7 +149,7 @@ class ODEDiscipline(BaseODEDiscipline): discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], - time_vector: NDArray, + time_vector: NDArray = None, time_var_name: str = "time", initial_time: float = 0.0, final_time: float = 1.0, @@ -192,9 +192,6 @@ class ODEDiscipline(BaseODEDiscipline): assert len(self.state_var_names) == len(self.state_dot_var_names) self.__generator_default_inputs = {} - self.__names_to_sizes = { - name: self.time_vector.size for name in self.state_var_names - } self.input_grammar.update(self.discipline.input_grammar) self.default_inputs = self.discipline.default_inputs.copy() @@ -219,7 +216,7 @@ class ODEDiscipline(BaseODEDiscipline): self.ode_mdo_func = MDODisciplineAdapterGenerator(self.discipline).get_function( input_names=[self.time_var_name] + self.state_var_names, output_names=self.state_dot_var_names, - # default_inputs=self.__generator_default_inputs, + default_inputs=self.__generator_default_inputs, ) self.ode_problem = ODEProblem( @@ -246,7 +243,7 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, - grammar_type=MDODiscipline.GrammarType.JSON, + grammar_type=MDODiscipline.GrammarType.SIMPLE, additional_input_var_names: list[str] | None = None, input_var_to_kwargs: Mapping | None = None, ): diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index acf39b0d4e..132c22a047 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -130,15 +130,21 @@ class Mass(ODEDiscipline): """The stiffness of the spring on the right-hand side of the mass.""" def __init__( - self, mass: float, left_stiff: float, right_stiff: float, state_name: str = "x" + self, + mass: float, + left_stiff: float, + right_stiff: float, + state_name: str = "x", + state_dot_name: str = "x_dot", ) -> None: self.mass = mass self.left_stiff = left_stiff self.right_stiff = right_stiff self.state_name = state_name + self.state_dot_name = state_dot_name self.discipline = create_discipline( - "AnalyticDiscipline", + "ODEDiscipline", expressions={state_name: state_name}, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) @@ -146,6 +152,7 @@ class Mass(ODEDiscipline): super().__init__( discipline=self.discipline, state_var_names=[self.state_name], + state_dot_var_names=[self.state_dot_name], time_var_name="time", ) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index b2d55f1510..96fc7d869d 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -22,11 +22,14 @@ ODE stands for Ordinary Differential Equation. """ from __future__ import annotations +from math import sin + import pytest from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.springs.springs import ChainedMasses +from numpy import allclose from numpy import array from numpy import linspace @@ -37,7 +40,7 @@ def oscillator(): _state = array([0.0, 1.0]) def oscillator_ode(time=_time, state=_state): - state_dot = array([state[1], -2 * state[0]]) + state_dot = array([state[1], -4 * state[0]]) return state_dot mdo_discipline = create_discipline( @@ -49,15 +52,18 @@ def oscillator(): def test_ode_discipline(oscillator): + time_vector = linspace(0.0, 10, 30) ode_disc = ODEDiscipline( discipline=oscillator, state_var_names=["state"], state_dot_var_names=["state_dot"], final_time=10, - time_vector=linspace(0.0, 10, 30), + time_vector=time_vector, ) out = ode_disc.execute() - raise ValueError(out["state"]) + + analytical_solution = array([sin(2 * t) / 2 for t in time_vector]) + assert allclose(out["state"], analytical_solution) def test_chained_masses(): -- GitLab From 94bfec23319646b237aa85d553f3c820bbf79c24 Mon Sep 17 00:00:00 2001 From: Francois GALLARD Date: Wed, 5 Jul 2023 10:33:04 +0200 Subject: [PATCH 030/237] ODE discipline works on a simple oscillator problem --- src/gemseo/algos/ode/ode_problem.py | 3 --- src/gemseo/disciplines/ode_discipline.py | 24 ++++++++++++++++++------ tests/disciplines/test_ode_discipline.py | 23 ++++++++++++----------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/gemseo/algos/ode/ode_problem.py b/src/gemseo/algos/ode/ode_problem.py index 7cf362c042..c3a0030efc 100644 --- a/src/gemseo/algos/ode/ode_problem.py +++ b/src/gemseo/algos/ode/ode_problem.py @@ -147,9 +147,6 @@ class ODEProblem(BaseProblem): ValueError: Either if the approximation method is unknown, if the shapes of the analytical and approximated Jacobian matrices are inconsistent or if the analytical gradients are wrong. - - Returns: - Whether the jacobian is correct. """ if self.jac is not None: function = MDOFunction(self._func, "f", jac=self._jac) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 76990afac4..92c9deb819 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -71,6 +71,7 @@ class BaseODEDiscipline(MDODiscipline): self, state_var_names: list[str], time_vector: NDArray, + state_component_names: list[str] | dict[str:list[str]] = None, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -91,12 +92,16 @@ class BaseODEDiscipline(MDODiscipline): self.state_var_names = state_var_names self.ode_solver_name = ode_solver_name self.ode_solver_options = ode_solver_options + if state_component_names is not None: + self.state_component_names = state_component_names + else: + self.state_component_names = state_var_names self.ode_problem = None self.time_vector = time_vector super().__init__(name=name, grammar_type=grammar_type) - self.output_grammar.update_from_names(state_var_names + ["time_vector"]) + self.output_grammar.update_from_names(self.state_component_names + ["time_vector"]) self.ode_factory = ODESolversFactory() self.__generator_default_inputs = {} @@ -115,16 +120,18 @@ class BaseODEDiscipline(MDODiscipline): Message = {ode_result.solver_message}" ) - split_state = vsplit( - ode_result.state_vector, - len(self.state_var_names), - ) + n_dim_sol=ode_result.state_vector.shape[0] + n_state_comp=len(self.state_component_names) + if n_state_comp!= n_dim_sol: + raise ValueError(f"Inconsistent state variables component number {n_state_comp} " + f"and ODE solution dimension {n_dim_sol}.") + split_state = vsplit( ode_result.state_vector, n_state_comp ) self.local_data.update( { name: val.flatten() for name, val in zip( - self.state_var_names, + self.state_component_names, split_state, ) } @@ -149,6 +156,7 @@ class ODEDiscipline(BaseODEDiscipline): discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], + state_component_names: list[str] | dict[str:list[str]] = None, time_vector: NDArray = None, time_var_name: str = "time", initial_time: float = 0.0, @@ -179,6 +187,7 @@ class ODEDiscipline(BaseODEDiscipline): self.discipline = discipline self.time_var_name = time_var_name self.state_dot_var_names = state_dot_var_names + self.ode_mdo_func = None super().__init__( @@ -188,6 +197,7 @@ class ODEDiscipline(BaseODEDiscipline): ode_solver_options=ode_solver_options, ode_solver_name=ode_solver_name, time_vector=time_vector, + state_component_names=state_component_names ) assert len(self.state_var_names) == len(self.state_dot_var_names) @@ -240,6 +250,7 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): self, ode_problem: ODEProblem, state_var_names: list[str], + state_component_names: list[str] | dict[str:list[str]] = None, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -269,6 +280,7 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): ode_solver_options=ode_solver_options, ode_solver_name=ode_solver_name, time_vector=ode_problem.time_vector, + state_component_names=state_component_names ) self.ode_problem = ode_problem self.additional_input_var_names = additional_input_var_names diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 96fc7d869d..3cb50bc5f1 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -22,7 +22,6 @@ ODE stands for Ordinary Differential Equation. """ from __future__ import annotations -from math import sin import pytest from gemseo import create_discipline @@ -30,18 +29,18 @@ from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.springs.springs import ChainedMasses from numpy import allclose -from numpy import array +from numpy import array, sin from numpy import linspace @pytest.fixture def oscillator(): _time = array([0.0]) - _state = array([0.0, 1.0]) + _position0 = array([0.0, 1.0]) - def oscillator_ode(time=_time, state=_state): - state_dot = array([state[1], -4 * state[0]]) - return state_dot + def oscillator_ode(time=_time, position=_position0): + position_dot = array([position[1], -4 * position[0]]) + return position_dot mdo_discipline = create_discipline( "AutoPyDiscipline", @@ -52,18 +51,20 @@ def oscillator(): def test_ode_discipline(oscillator): - time_vector = linspace(0.0, 10, 30) + time_vector = linspace(0.0, 10, 5) ode_disc = ODEDiscipline( discipline=oscillator, - state_var_names=["state"], - state_dot_var_names=["state_dot"], + state_var_names=["position"], + state_dot_var_names=["position_dot"], + state_component_names = ["position", "dposition/dt"], final_time=10, time_vector=time_vector, + ode_solver_options={"rtol":1e-12,"atol":1e-12} ) out = ode_disc.execute() - analytical_solution = array([sin(2 * t) / 2 for t in time_vector]) - assert allclose(out["state"], analytical_solution) + analytical_solution = sin(2 * time_vector) / 2 + assert allclose(out["position"], analytical_solution) def test_chained_masses(): -- GitLab From ca1ec7e2bd26d1e3d5d6ada63ae526ca89d6e1ef Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 5 Jul 2023 11:51:48 +0200 Subject: [PATCH 031/237] Clean up. --- src/gemseo/disciplines/ode_discipline.py | 55 ++++++++++++++++++------ tests/disciplines/test_ode_discipline.py | 10 ++--- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 92c9deb819..a049537dcf 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -71,7 +71,7 @@ class BaseODEDiscipline(MDODiscipline): self, state_var_names: list[str], time_vector: NDArray, - state_component_names: list[str] | dict[str:list[str]] = None, + state_component_names: list[str] | dict[str : list[str]] = None, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -84,6 +84,15 @@ class BaseODEDiscipline(MDODiscipline): Args: state_var_names: The names of the state variables. time_vector: The times at which the solution is evaluated. + state_component_names: The names of the components of the state. This + parameter is used when solving second-order or higher ODEs. + For 1-dimensional problems, it is possible to use a list of strings. In + this case, the strings in the list are the names of the state variable + and its consecutive derivatives. + For problems with an n-dimensional state, in each {key: value} pair, the + key is the name of a state variable, and the value is the list of names of + the consecutive derivatives of that variable. + ode_solver_name: The name of the solver used to solve the ODE. ode_solver_options: The options to pass to the ODE solver. name: The name of the discipline. @@ -101,7 +110,9 @@ class BaseODEDiscipline(MDODiscipline): super().__init__(name=name, grammar_type=grammar_type) - self.output_grammar.update_from_names(self.state_component_names + ["time_vector"]) + self.output_grammar.update_from_names( + self.state_component_names + ["time_vector"] + ) self.ode_factory = ODESolversFactory() self.__generator_default_inputs = {} @@ -120,12 +131,14 @@ class BaseODEDiscipline(MDODiscipline): Message = {ode_result.solver_message}" ) - n_dim_sol=ode_result.state_vector.shape[0] - n_state_comp=len(self.state_component_names) - if n_state_comp!= n_dim_sol: - raise ValueError(f"Inconsistent state variables component number {n_state_comp} " - f"and ODE solution dimension {n_dim_sol}.") - split_state = vsplit( ode_result.state_vector, n_state_comp ) + n_dim_sol = ode_result.state_vector.shape[0] + n_state_comp = len(self.state_component_names) + if n_state_comp != n_dim_sol: + raise ValueError( + f"Inconsistent number of state variable components " + f"{n_state_comp} and ODE solution dimension {n_dim_sol}." + ) + split_state = vsplit(ode_result.state_vector, n_state_comp) self.local_data.update( { @@ -156,8 +169,8 @@ class ODEDiscipline(BaseODEDiscipline): discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], - state_component_names: list[str] | dict[str:list[str]] = None, - time_vector: NDArray = None, + state_component_names: list[str] | dict[str : list[str]] = None, + time_vector: NDArray[float] = None, time_var_name: str = "time", initial_time: float = 0.0, final_time: float = 1.0, @@ -175,6 +188,14 @@ class ODEDiscipline(BaseODEDiscipline): grammar. state_dot_var_names: The names of the time derivatives of the state variable in the discipline's input grammars. + state_component_names: The names of the components of the state. This + parameter is used when solving second-order or higher ODEs. + For 1-dimensional problems, it is possible to use a list of strings. In + this case, the strings in the list are the names of the state variable + and its consecutive derivatives. + For problems with an n-dimensional state, in each {key: value} pair, the + key is the name of a state variable, and the value is the list of names of + the consecutive derivatives of that variable. time_vector: The times at which the solution is evaluated. time_var_name: The name of the time variable in the discipline's input grammar. @@ -197,7 +218,7 @@ class ODEDiscipline(BaseODEDiscipline): ode_solver_options=ode_solver_options, ode_solver_name=ode_solver_name, time_vector=time_vector, - state_component_names=state_component_names + state_component_names=state_component_names, ) assert len(self.state_var_names) == len(self.state_dot_var_names) @@ -250,7 +271,7 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): self, ode_problem: ODEProblem, state_var_names: list[str], - state_component_names: list[str] | dict[str:list[str]] = None, + state_component_names: list[str] | dict[str : list[str]] = None, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -266,6 +287,14 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): ode_problem: The ODE problem to be solved at each execution. state_var_names: The names of the state variables. ode_solver_name: The name of the solver used to solve the ODE. + state_component_names: The names of the components of the state. This + parameter is used when solving second-order or higher ODEs. + For 1-dimensional problems, it is possible to use a list of strings. In + this case, the strings in the list are the names of the state variable + and its consecutive derivatives. + For problems with an n-dimensional state, in each {key: value} pair, the + key is the name of a state variable, and the value is the list of names of + the consecutive derivatives of that variable. ode_solver_options: The options to pass to the ODE solver. name: The name of the discipline. grammar_type: The type of grammar to use for the discipline. @@ -280,7 +309,7 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): ode_solver_options=ode_solver_options, ode_solver_name=ode_solver_name, time_vector=ode_problem.time_vector, - state_component_names=state_component_names + state_component_names=state_component_names, ) self.ode_problem = ode_problem self.additional_input_var_names = additional_input_var_names diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 3cb50bc5f1..2f27845b9a 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -22,15 +22,15 @@ ODE stands for Ordinary Differential Equation. """ from __future__ import annotations - import pytest from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.springs.springs import ChainedMasses from numpy import allclose -from numpy import array, sin +from numpy import array from numpy import linspace +from numpy import sin @pytest.fixture @@ -56,14 +56,14 @@ def test_ode_discipline(oscillator): discipline=oscillator, state_var_names=["position"], state_dot_var_names=["position_dot"], - state_component_names = ["position", "dposition/dt"], + state_component_names=["position", "dposition/dt"], final_time=10, time_vector=time_vector, - ode_solver_options={"rtol":1e-12,"atol":1e-12} + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) out = ode_disc.execute() - analytical_solution = sin(2 * time_vector) / 2 + analytical_solution = sin(2 * time_vector) / 2 assert allclose(out["position"], analytical_solution) -- GitLab From 1a5ba536971c46d1b1cfead982f75a0b93eb32d6 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 10:29:47 +0200 Subject: [PATCH 032/237] `state` contains position and velocity. --- tests/disciplines/test_ode_discipline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 2f27845b9a..f8166c162a 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -36,11 +36,11 @@ from numpy import sin @pytest.fixture def oscillator(): _time = array([0.0]) - _position0 = array([0.0, 1.0]) + _state = array([0.0, 1.0]) - def oscillator_ode(time=_time, position=_position0): - position_dot = array([position[1], -4 * position[0]]) - return position_dot + def oscillator_ode(time=_time, state=_state): + state_dot = array([state[1], -4 * state[0]]) + return state_dot mdo_discipline = create_discipline( "AutoPyDiscipline", -- GitLab From 8e10886be1b8360840be392eef8b19cbeca4691a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 10:52:36 +0200 Subject: [PATCH 033/237] `state` contains position and velocity. --- tests/disciplines/test_ode_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index f8166c162a..77248e8c9f 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -54,8 +54,8 @@ def test_ode_discipline(oscillator): time_vector = linspace(0.0, 10, 5) ode_disc = ODEDiscipline( discipline=oscillator, - state_var_names=["position"], - state_dot_var_names=["position_dot"], + state_var_names=["state"], + state_dot_var_names=["state_dot"], state_component_names=["position", "dposition/dt"], final_time=10, time_vector=time_vector, -- GitLab From 364eae533b36dc6c295b146c921d9c719e278268 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 10:53:57 +0200 Subject: [PATCH 034/237] Use dimension of result. --- src/gemseo/disciplines/ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index a049537dcf..d8c217bd73 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -138,7 +138,7 @@ class BaseODEDiscipline(MDODiscipline): f"Inconsistent number of state variable components " f"{n_state_comp} and ODE solution dimension {n_dim_sol}." ) - split_state = vsplit(ode_result.state_vector, n_state_comp) + split_state = vsplit(ode_result.state_vector, n_dim_sol) self.local_data.update( { -- GitLab From cd3506d32acd3ba4d1bba26698d705fdca73c137 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 11:38:10 +0200 Subject: [PATCH 035/237] Simplify code and make problems more explicit. --- src/gemseo/disciplines/ode_discipline.py | 47 +----------------------- tests/disciplines/test_ode_discipline.py | 12 +++--- 2 files changed, 8 insertions(+), 51 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index d8c217bd73..39a44e884d 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -71,7 +71,6 @@ class BaseODEDiscipline(MDODiscipline): self, state_var_names: list[str], time_vector: NDArray, - state_component_names: list[str] | dict[str : list[str]] = None, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -84,15 +83,6 @@ class BaseODEDiscipline(MDODiscipline): Args: state_var_names: The names of the state variables. time_vector: The times at which the solution is evaluated. - state_component_names: The names of the components of the state. This - parameter is used when solving second-order or higher ODEs. - For 1-dimensional problems, it is possible to use a list of strings. In - this case, the strings in the list are the names of the state variable - and its consecutive derivatives. - For problems with an n-dimensional state, in each {key: value} pair, the - key is the name of a state variable, and the value is the list of names of - the consecutive derivatives of that variable. - ode_solver_name: The name of the solver used to solve the ODE. ode_solver_options: The options to pass to the ODE solver. name: The name of the discipline. @@ -101,18 +91,12 @@ class BaseODEDiscipline(MDODiscipline): self.state_var_names = state_var_names self.ode_solver_name = ode_solver_name self.ode_solver_options = ode_solver_options - if state_component_names is not None: - self.state_component_names = state_component_names - else: - self.state_component_names = state_var_names self.ode_problem = None self.time_vector = time_vector super().__init__(name=name, grammar_type=grammar_type) - self.output_grammar.update_from_names( - self.state_component_names + ["time_vector"] - ) + self.output_grammar.update_from_names(self.state_var_names + ["time_vector"]) self.ode_factory = ODESolversFactory() self.__generator_default_inputs = {} @@ -132,19 +116,13 @@ class BaseODEDiscipline(MDODiscipline): ) n_dim_sol = ode_result.state_vector.shape[0] - n_state_comp = len(self.state_component_names) - if n_state_comp != n_dim_sol: - raise ValueError( - f"Inconsistent number of state variable components " - f"{n_state_comp} and ODE solution dimension {n_dim_sol}." - ) split_state = vsplit(ode_result.state_vector, n_dim_sol) self.local_data.update( { name: val.flatten() for name, val in zip( - self.state_component_names, + self.state_var_names, split_state, ) } @@ -169,7 +147,6 @@ class ODEDiscipline(BaseODEDiscipline): discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], - state_component_names: list[str] | dict[str : list[str]] = None, time_vector: NDArray[float] = None, time_var_name: str = "time", initial_time: float = 0.0, @@ -188,14 +165,6 @@ class ODEDiscipline(BaseODEDiscipline): grammar. state_dot_var_names: The names of the time derivatives of the state variable in the discipline's input grammars. - state_component_names: The names of the components of the state. This - parameter is used when solving second-order or higher ODEs. - For 1-dimensional problems, it is possible to use a list of strings. In - this case, the strings in the list are the names of the state variable - and its consecutive derivatives. - For problems with an n-dimensional state, in each {key: value} pair, the - key is the name of a state variable, and the value is the list of names of - the consecutive derivatives of that variable. time_vector: The times at which the solution is evaluated. time_var_name: The name of the time variable in the discipline's input grammar. @@ -218,7 +187,6 @@ class ODEDiscipline(BaseODEDiscipline): ode_solver_options=ode_solver_options, ode_solver_name=ode_solver_name, time_vector=time_vector, - state_component_names=state_component_names, ) assert len(self.state_var_names) == len(self.state_dot_var_names) @@ -252,7 +220,6 @@ class ODEDiscipline(BaseODEDiscipline): self.ode_problem = ODEProblem( self.__ode_func, - # jac=self.__ode_func_jac, initial_state=initial_state, initial_time=initial_time, final_time=final_time, @@ -271,7 +238,6 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): self, ode_problem: ODEProblem, state_var_names: list[str], - state_component_names: list[str] | dict[str : list[str]] = None, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] = None, name: str = None, @@ -287,14 +253,6 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): ode_problem: The ODE problem to be solved at each execution. state_var_names: The names of the state variables. ode_solver_name: The name of the solver used to solve the ODE. - state_component_names: The names of the components of the state. This - parameter is used when solving second-order or higher ODEs. - For 1-dimensional problems, it is possible to use a list of strings. In - this case, the strings in the list are the names of the state variable - and its consecutive derivatives. - For problems with an n-dimensional state, in each {key: value} pair, the - key is the name of a state variable, and the value is the list of names of - the consecutive derivatives of that variable. ode_solver_options: The options to pass to the ODE solver. name: The name of the discipline. grammar_type: The type of grammar to use for the discipline. @@ -309,7 +267,6 @@ class ODEDisciplineFromProblem(BaseODEDiscipline): ode_solver_options=ode_solver_options, ode_solver_name=ode_solver_name, time_vector=ode_problem.time_vector, - state_component_names=state_component_names, ) self.ode_problem = ode_problem self.additional_input_var_names = additional_input_var_names diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 77248e8c9f..41cf96909e 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -38,9 +38,10 @@ def oscillator(): _time = array([0.0]) _state = array([0.0, 1.0]) - def oscillator_ode(time=_time, state=_state): - state_dot = array([state[1], -4 * state[0]]) - return state_dot + def oscillator_ode(time=_time, position=_state[0], velocity=_state[1]): + position_dot = velocity + velocity_dot = -4 * position + return position_dot, velocity_dot mdo_discipline = create_discipline( "AutoPyDiscipline", @@ -54,9 +55,8 @@ def test_ode_discipline(oscillator): time_vector = linspace(0.0, 10, 5) ode_disc = ODEDiscipline( discipline=oscillator, - state_var_names=["state"], - state_dot_var_names=["state_dot"], - state_component_names=["position", "dposition/dt"], + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], final_time=10, time_vector=time_vector, ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, -- GitLab From 8111548aa9bf64bca262a74c6b60451ccc279efb Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 11:59:02 +0200 Subject: [PATCH 036/237] Move oscillator test case to dedicated submodule. --- src/gemseo/problems/ode/oscillator.py | 59 ++++++++++++++++++++++++ tests/disciplines/test_ode_discipline.py | 25 ++-------- 2 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 src/gemseo/problems/ode/oscillator.py diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py new file mode 100644 index 0000000000..7b71ca4936 --- /dev/null +++ b/src/gemseo/problems/ode/oscillator.py @@ -0,0 +1,59 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Isabelle Santos +# OTHER AUTHORS - MACROSCOPIC CHANGES +r"""A simple oscillator. + +An oscillator described by the equation + +.. math:: + + \frac{d^2x}{dt^2} = -A^2\dot x + + +with :math:`A \in \mathbb{R}` has an analytical solution + +.. math:: + + x(t) = \lambda \sin(At) + \mu \cos(At) + + +where :math:`\lambda` and :math:`\mu` are two constants defined by the initial conditions. +""" +from __future__ import annotations + +from numpy import array + +from gemseo import create_discipline +from gemseo.core.discipline import MDODiscipline + + +def create_oscillator_ode_discipline(): + _time = array([0.0]) + _state = array([0.0, 1.0]) + + def oscillator_ode(time=_time, position=_state[0], velocity=_state[1]): + position_dot = velocity + velocity_dot = -4 * position + return position_dot, velocity_dot + + mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=oscillator_ode, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + return mdo_discipline diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 41cf96909e..2e593b2e5b 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -23,35 +23,16 @@ ODE stands for Ordinary Differential Equation. from __future__ import annotations import pytest -from gemseo import create_discipline -from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.springs.springs import ChainedMasses from numpy import allclose -from numpy import array from numpy import linspace from numpy import sin -@pytest.fixture -def oscillator(): - _time = array([0.0]) - _state = array([0.0, 1.0]) - - def oscillator_ode(time=_time, position=_state[0], velocity=_state[1]): - position_dot = velocity - velocity_dot = -4 * position - return position_dot, velocity_dot - - mdo_discipline = create_discipline( - "AutoPyDiscipline", - py_func=oscillator_ode, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - return mdo_discipline - - -def test_ode_discipline(oscillator): +def test_ode_discipline(): + oscillator = create_oscillator_ode_discipline() time_vector = linspace(0.0, 10, 5) ode_disc = ODEDiscipline( discipline=oscillator, -- GitLab From ef10bb45ce5a82f2d7e983fb446e373828e4ba91 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 13:53:32 +0200 Subject: [PATCH 037/237] Improve names. --- src/gemseo/problems/ode/oscillator.py | 2 +- tests/disciplines/test_ode_discipline.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 7b71ca4936..3b2fc819ed 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -42,7 +42,7 @@ from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline -def create_oscillator_ode_discipline(): +def create_oscillator_mdo_discipline(): _time = array([0.0]) _state = array([0.0, 1.0]) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 2e593b2e5b..6b523388f7 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -24,7 +24,7 @@ from __future__ import annotations import pytest from gemseo.disciplines.ode_discipline import ODEDiscipline -from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline +from gemseo.problems.ode.oscillator import create_oscillator_mdo_discipline from gemseo.problems.springs.springs import ChainedMasses from numpy import allclose from numpy import linspace @@ -32,7 +32,7 @@ from numpy import sin def test_ode_discipline(): - oscillator = create_oscillator_ode_discipline() + oscillator = create_oscillator_mdo_discipline() time_vector = linspace(0.0, 10, 5) ode_disc = ODEDiscipline( discipline=oscillator, -- GitLab From 4b7cf7ed65b679acc41b049680a97f43ad9f3ae1 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 13:58:02 +0200 Subject: [PATCH 038/237] Move things around. --- src/gemseo/problems/ode/oscillator.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 3b2fc819ed..13b6d2bf90 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -41,16 +41,18 @@ from numpy import array from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline +_time = array([0.0]) +_position = array([0.0]) +_velocity = array([1.0]) -def create_oscillator_mdo_discipline(): - _time = array([0.0]) - _state = array([0.0, 1.0]) - def oscillator_ode(time=_time, position=_state[0], velocity=_state[1]): - position_dot = velocity - velocity_dot = -4 * position - return position_dot, velocity_dot +def oscillator_ode(time=_time, position=_position, velocity=_velocity): + position_dot = velocity + velocity_dot = -4 * position + return position_dot, velocity_dot + +def create_oscillator_mdo_discipline(): mdo_discipline = create_discipline( "AutoPyDiscipline", py_func=oscillator_ode, -- GitLab From 64c9918074faa4f2a610327aecfd3058b6f02a6c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:03:31 +0200 Subject: [PATCH 039/237] Explanation of how the problem is formulated in oscillator_ode. --- src/gemseo/problems/ode/oscillator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 13b6d2bf90..a07c22b49b 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -33,6 +33,18 @@ with :math:`A \in \mathbb{R}` has an analytical solution where :math:`\lambda` and :math:`\mu` are two constants defined by the initial conditions. + +This problem can be re-written as a 2-dimensional first-order ordinary differential +equation (ODE). + +.. math:: + + \left\{\begin{array} + \dot(x) = y \\ + \dot(y) = -A^2 x + \end{array}\right. + +where :math:`x` represents the position of the oscillator and :math:`y` its velocity. """ from __future__ import annotations -- GitLab From 0e06871245e03f35926d6a7456489d232d991c63 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:04:04 +0200 Subject: [PATCH 040/237] Make names more explicit. --- src/gemseo/problems/ode/oscillator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index a07c22b49b..34feb0baa2 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -53,12 +53,14 @@ from numpy import array from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline -_time = array([0.0]) -_position = array([0.0]) -_velocity = array([1.0]) +_initial_time = array([0.0]) +_initial_position = array([0.0]) +_initial_velocity = array([1.0]) -def oscillator_ode(time=_time, position=_position, velocity=_velocity): +def oscillator_ode( + time=_initial_time, position=_initial_position, velocity=_initial_velocity +): position_dot = velocity velocity_dot = -4 * position return position_dot, velocity_dot -- GitLab From 8cd54d803258112b8b4f332466d75a6366de3551 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:06:20 +0200 Subject: [PATCH 041/237] Add type hints. --- src/gemseo/problems/ode/oscillator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 34feb0baa2..6c023dddb2 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -49,6 +49,7 @@ where :math:`x` represents the position of the oscillator and :math:`y` its velo from __future__ import annotations from numpy import array +from numpy.typing import NDArray from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline @@ -59,14 +60,16 @@ _initial_velocity = array([1.0]) def oscillator_ode( - time=_initial_time, position=_initial_position, velocity=_initial_velocity -): + time: NDArray[float] = _initial_time, + position: NDArray[float] = _initial_position, + velocity: NDArray[float] = _initial_velocity, +) -> (NDArray[float], NDArray[float]): position_dot = velocity velocity_dot = -4 * position return position_dot, velocity_dot -def create_oscillator_mdo_discipline(): +def create_oscillator_mdo_discipline() -> MDODiscipline: mdo_discipline = create_discipline( "AutoPyDiscipline", py_func=oscillator_ode, -- GitLab From b2f1c2edecc0a2d6bbcc1e5851700f5e43db03d0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:20:19 +0200 Subject: [PATCH 042/237] Improve type hints. --- src/gemseo/disciplines/ode_discipline.py | 70 ++---------------------- 1 file changed, 6 insertions(+), 64 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 39a44e884d..3543e03917 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -72,9 +72,9 @@ class BaseODEDiscipline(MDODiscipline): state_var_names: list[str], time_vector: NDArray, ode_solver_name: str = "RK45", - ode_solver_options: Mapping[str:Any] = None, - name: str = None, - grammar_type=MDODiscipline.GrammarType.JSON, + ode_solver_options: Mapping[str:Any] | None = None, + name: str = "", + grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.JSON, ): """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. @@ -117,7 +117,6 @@ class BaseODEDiscipline(MDODiscipline): n_dim_sol = ode_result.state_vector.shape[0] split_state = vsplit(ode_result.state_vector, n_dim_sol) - self.local_data.update( { name: val.flatten() @@ -147,13 +146,13 @@ class ODEDiscipline(BaseODEDiscipline): discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], - time_vector: NDArray[float] = None, + time_vector: NDArray[float] | None = None, time_var_name: str = "time", initial_time: float = 0.0, final_time: float = 1.0, ode_solver_name: str = "RK45", - ode_solver_options: Mapping[str:Any] = None, - name: str = None, + ode_solver_options: Mapping[str:Any] | None = None, + name: str = "", ): """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. @@ -229,60 +228,3 @@ class ODEDiscipline(BaseODEDiscipline): def _run(self) -> None: self.__generator_default_inputs.update(self.local_data) super()._run() - - -class ODEDisciplineFromProblem(BaseODEDiscipline): - r"""A discipline based on the solution to an ODE.""" - - def __init__( - self, - ode_problem: ODEProblem, - state_var_names: list[str], - ode_solver_name: str = "RK45", - ode_solver_options: Mapping[str:Any] = None, - name: str = None, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - additional_input_var_names: list[str] | None = None, - input_var_to_kwargs: Mapping | None = None, - ): - """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. - - Either the ode_problem or the discipline must be passed. - - Args: - ode_problem: The ODE problem to be solved at each execution. - state_var_names: The names of the state variables. - ode_solver_name: The name of the solver used to solve the ODE. - ode_solver_options: The options to pass to the ODE solver. - name: The name of the discipline. - grammar_type: The type of grammar to use for the discipline. - additional_input_var_names: The additional input variables names that will be - passed to the ode_problem as keyword arguments. - input_var_to_kwargs: The mapping of keyword arguments for the ODEProblem. - """ - super().__init__( - name=name, - grammar_type=grammar_type, - state_var_names=state_var_names, - ode_solver_options=ode_solver_options, - ode_solver_name=ode_solver_name, - time_vector=ode_problem.time_vector, - ) - self.ode_problem = ode_problem - self.additional_input_var_names = additional_input_var_names - self.input_grammar.update_from_names(additional_input_var_names) - self.input_var_to_kwargs = input_var_to_kwargs - - def _run(self) -> None: - if self.input_var_to_kwargs is None: - kwargs = { - var_name: self.local_data[var_name] - for var_name in self.additional_input_var_names - } - else: - kwargs = { - self.input_var_to_kwargs[var_name]: self.local_data[var_name] - for var_name in self.additional_input_var_names - } - self.ode_problem.ode_funcs_kwargs = kwargs - super()._run() -- GitLab From 20cacfc22334c4a9929d3932d08086ac50237721 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:35:33 +0200 Subject: [PATCH 043/237] Define jacobian. --- src/gemseo/problems/ode/oscillator.py | 29 ++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 6c023dddb2..1c992d5e98 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -22,14 +22,14 @@ An oscillator described by the equation .. math:: - \frac{d^2x}{dt^2} = -A^2\dot x + \frac{d^2x}{dt^2} = -\omega^2\dot x -with :math:`A \in \mathbb{R}` has an analytical solution +with :math:`\omega \in \mathbb{R}` has an analytical solution .. math:: - x(t) = \lambda \sin(At) + \mu \cos(At) + x(t) = \lambda \sin(\omega t) + \mu \cos(\omega t) where :math:`\lambda` and :math:`\mu` are two constants defined by the initial conditions. @@ -41,10 +41,20 @@ equation (ODE). \left\{\begin{array} \dot(x) = y \\ - \dot(y) = -A^2 x + \dot(y) = -\omega^2 x \end{array}\right. + where :math:`x` represents the position of the oscillator and :math:`y` its velocity. + +The Jacobian for this ODE is + +.. math:: + + \begin{pmatrix} + -\omega^2 & 0 \\ + 1 & 0 + \end{pmatrix """ from __future__ import annotations @@ -57,6 +67,7 @@ from gemseo.core.discipline import MDODiscipline _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) +_omega = 4 def oscillator_ode( @@ -65,10 +76,18 @@ def oscillator_ode( velocity: NDArray[float] = _initial_velocity, ) -> (NDArray[float], NDArray[float]): position_dot = velocity - velocity_dot = -4 * position + velocity_dot = -_omega * position return position_dot, velocity_dot +def oscillator_ode_jacobian( + time: NDArray[float] = _initial_time, + position: NDArray[float] = _initial_position, + velocity: NDArray[float] = _initial_velocity, +) -> NDArray[float]: + return array([[-(_omega**2), 1], [0, 0]]) + + def create_oscillator_mdo_discipline() -> MDODiscipline: mdo_discipline = create_discipline( "AutoPyDiscipline", -- GitLab From 66da9ac1679045150c56aa108f804a1f6ad15a6c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:37:35 +0200 Subject: [PATCH 044/237] Make names more explicit. --- src/gemseo/problems/ode/oscillator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 1c992d5e98..eb572fc181 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -70,7 +70,7 @@ _initial_velocity = array([1.0]) _omega = 4 -def oscillator_ode( +def oscillator_ode_rhs_function( time: NDArray[float] = _initial_time, position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, @@ -80,7 +80,7 @@ def oscillator_ode( return position_dot, velocity_dot -def oscillator_ode_jacobian( +def oscillator_ode_rhs_jacobian( time: NDArray[float] = _initial_time, position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, @@ -91,7 +91,7 @@ def oscillator_ode_jacobian( def create_oscillator_mdo_discipline() -> MDODiscipline: mdo_discipline = create_discipline( "AutoPyDiscipline", - py_func=oscillator_ode, + py_func=oscillator_ode_rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) return mdo_discipline -- GitLab From 2effd0a9d336d205d435a986b76abe75ac565f7f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:41:51 +0200 Subject: [PATCH 045/237] Write docstring summaries. --- src/gemseo/problems/ode/oscillator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index eb572fc181..4f141df7f5 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -54,7 +54,7 @@ The Jacobian for this ODE is \begin{pmatrix} -\omega^2 & 0 \\ 1 & 0 - \end{pmatrix + \end{pmatrix}. """ from __future__ import annotations @@ -75,6 +75,7 @@ def oscillator_ode_rhs_function( position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, ) -> (NDArray[float], NDArray[float]): + """Right-hand side of the oscillator ODE problem.""" position_dot = velocity velocity_dot = -_omega * position return position_dot, velocity_dot @@ -85,10 +86,12 @@ def oscillator_ode_rhs_jacobian( position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, ) -> NDArray[float]: + """Jacobian of the right-hand side of the oscillator ODE problem.""" return array([[-(_omega**2), 1], [0, 0]]) def create_oscillator_mdo_discipline() -> MDODiscipline: + """Create the discipline that represents the oscillator problem.""" mdo_discipline = create_discipline( "AutoPyDiscipline", py_func=oscillator_ode_rhs_function, -- GitLab From 4189cf8c0744465db3924738c82064ac40cb94bc Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:46:16 +0200 Subject: [PATCH 046/237] Improve docstrings. --- src/gemseo/problems/ode/oscillator.py | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 4f141df7f5..722d8e37b5 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -75,7 +75,17 @@ def oscillator_ode_rhs_function( position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, ) -> (NDArray[float], NDArray[float]): - """Right-hand side of the oscillator ODE problem.""" + """Right-hand side of the oscillator ODE problem. + + Args: + time: The value of the time. + position: The position of the system at `time`. + velocity: The value of the first derivative of the position at `time`. + + Returns: + The derivative of the position at `time`. + The derivative of the velocity at `time`. + """ position_dot = velocity velocity_dot = -_omega * position return position_dot, velocity_dot @@ -86,12 +96,27 @@ def oscillator_ode_rhs_jacobian( position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, ) -> NDArray[float]: - """Jacobian of the right-hand side of the oscillator ODE problem.""" + """Jacobian of the right-hand side of the oscillator ODE problem. + + This is a constant 2x2 matrix. + + Args: + time: The value of the time for the initial conditions. + position: The position of the system at `time`. + velocity: The value of the first derivative of the position at `time`. + + Returns: + The value of the Jacobian. + """ return array([[-(_omega**2), 1], [0, 0]]) def create_oscillator_mdo_discipline() -> MDODiscipline: - """Create the discipline that represents the oscillator problem.""" + """Create the discipline that represents the oscillator problem. + + Returns: + The MDODiscipline that describes the oscillator ODE problem. + """ mdo_discipline = create_discipline( "AutoPyDiscipline", py_func=oscillator_ode_rhs_function, -- GitLab From fb5c12abaf1e71c81e9e7054e73df283a8024ce6 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 14:49:02 +0200 Subject: [PATCH 047/237] Add docstrings. --- tests/disciplines/test_ode_discipline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 6b523388f7..1da636d855 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -31,7 +31,8 @@ from numpy import linspace from numpy import sin -def test_ode_discipline(): +def test_oscillator_ode_discipline(): + """Test the ODEDiscipline on with the oscillator validation case.""" oscillator = create_oscillator_mdo_discipline() time_vector = linspace(0.0, 10, 5) ode_disc = ODEDiscipline( @@ -49,6 +50,7 @@ def test_ode_discipline(): def test_chained_masses(): + """Test the Springs class.""" kwargs = {"stiffnesses": [1, 1, 1], "masses": [1, 1]} springs_discipline = ChainedMasses(**kwargs) assert springs_discipline is not None @@ -63,5 +65,6 @@ def test_chained_masses(): ], ) def test_chained_masses_wrong_lengths(kwargs): + """Test the error messages when incoherent input is provided.""" with pytest.raises(ValueError, match="incoherent lengths"): springs_discipline = ChainedMasses(**kwargs) # noqa: F841 -- GitLab From acaac4deff70520686c276fa33b680fb9e3a97f3 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:14:19 +0200 Subject: [PATCH 048/237] Simplify. --- src/gemseo/disciplines/ode_discipline.py | 138 ++++++++--------------- 1 file changed, 50 insertions(+), 88 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 3543e03917..2078a7f727 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -40,8 +40,11 @@ from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array -class BaseODEDiscipline(MDODiscipline): - r"""A discipline based on the solution to an ODE.""" +class ODEDiscipline(MDODiscipline): + r"""A discipline based on the solution to an ODE. + + ODE stands for Ordinary Differential Equation. + """ time_var_name: str """The name of the time variable.""" @@ -67,92 +70,22 @@ class BaseODEDiscipline(MDODiscipline): ode_factory: ODESolversFactory """The factory to build the ODE.""" - def __init__( - self, - state_var_names: list[str], - time_vector: NDArray, - ode_solver_name: str = "RK45", - ode_solver_options: Mapping[str:Any] | None = None, - name: str = "", - grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.JSON, - ): - """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. - - Either the ode_problem or the discipline must be passed. - - Args: - state_var_names: The names of the state variables. - time_vector: The times at which the solution is evaluated. - ode_solver_name: The name of the solver used to solve the ODE. - ode_solver_options: The options to pass to the ODE solver. - name: The name of the discipline. - grammar_type: The way the grammar is defined. - """ - self.state_var_names = state_var_names - self.ode_solver_name = ode_solver_name - self.ode_solver_options = ode_solver_options - self.ode_problem = None - self.time_vector = time_vector - - super().__init__(name=name, grammar_type=grammar_type) - - self.output_grammar.update_from_names(self.state_var_names + ["time_vector"]) - self.ode_factory = ODESolversFactory() - - self.__generator_default_inputs = {} - - def _run(self) -> None: - if self.ode_solver_options is not None: - options = self.ode_solver_options - else: - options = {} - ode_result = self.ode_factory.execute( - self.ode_problem, self.ode_solver_name, **options - ) - if not ode_result.is_converged: - raise RuntimeError( - f"ODE solver {ode_result.solver_name} failed to converge. \ - Message = {ode_result.solver_message}" - ) - - n_dim_sol = ode_result.state_vector.shape[0] - split_state = vsplit(ode_result.state_vector, n_dim_sol) - self.local_data.update( - { - name: val.flatten() - for name, val in zip( - self.state_var_names, - split_state, - ) - } - ) - self.local_data["time_vector"] = self.time_vector - - -class ODEDiscipline(BaseODEDiscipline): - r"""A discipline based on the solution to an ODE.""" - discipline: MDODiscipline """The discipline.""" - time_var_name: str - """The name of the time variable.""" - - state_dot_var_names: list[str] - """The names of the state variables derivatives wrt time.""" - def __init__( self, discipline: MDODiscipline, state_var_names: list[str], state_dot_var_names: list[str], - time_vector: NDArray[float] | None = None, + time_vector: NDArray, time_var_name: str = "time", initial_time: float = 0.0, final_time: float = 1.0, ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] | None = None, name: str = "", + grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.SIMPLE, ): """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. @@ -160,8 +93,7 @@ class ODEDiscipline(BaseODEDiscipline): Args: discipline: The discipline. - state_var_names: The names of the state variables in the discipline's input - grammar. + state_var_names: The names of the state variables. state_dot_var_names: The names of the time derivatives of the state variable in the discipline's input grammars. time_vector: The times at which the solution is evaluated. @@ -169,24 +101,30 @@ class ODEDiscipline(BaseODEDiscipline): grammar. initial_time: The start of the time interval to perform the integration on. final_time: The end of the time interval to perform the integration on. - ode_solver_name: The name of the solver that should be used. - ode_solver_options: The options to be passed to the solver. - name: The name of the discipline + ode_solver_name: The name of the solver used to solve the ODE. + ode_solver_options: The options to pass to the ODE solver. + name: The name of the discipline. + grammar_type: The way the grammar is defined. """ + self.state_var_names = state_var_names + self.ode_solver_name = ode_solver_name + self.ode_solver_options = ode_solver_options + self.ode_problem = None + self.time_vector = time_vector + self.discipline = discipline self.time_var_name = time_var_name self.state_dot_var_names = state_dot_var_names self.ode_mdo_func = None - super().__init__( - name=name, - grammar_type=discipline.grammar_type, - state_var_names=state_var_names, - ode_solver_options=ode_solver_options, - ode_solver_name=ode_solver_name, - time_vector=time_vector, - ) + super().__init__(name=name, grammar_type=grammar_type) + + self.output_grammar.update_from_names(self.state_var_names + ["time_vector"]) + self.ode_factory = ODESolversFactory() + + self.__generator_default_inputs = {} + assert len(self.state_var_names) == len(self.state_dot_var_names) self.__generator_default_inputs = {} @@ -227,4 +165,28 @@ class ODEDiscipline(BaseODEDiscipline): def _run(self) -> None: self.__generator_default_inputs.update(self.local_data) - super()._run() + if self.ode_solver_options is not None: + options = self.ode_solver_options + else: + options = {} + ode_result = self.ode_factory.execute( + self.ode_problem, self.ode_solver_name, **options + ) + if not ode_result.is_converged: + raise RuntimeError( + f"ODE solver {ode_result.solver_name} failed to converge. \ + Message = {ode_result.solver_message}" + ) + + n_dim_sol = ode_result.state_vector.shape[0] + split_state = vsplit(ode_result.state_vector, n_dim_sol) + self.local_data.update( + { + name: val.flatten() + for name, val in zip( + self.state_var_names, + split_state, + ) + } + ) + self.local_data["time_vector"] = self.time_vector -- GitLab From 604f54ad9a01d7826d870c67a3d4bb220e2559b3 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:15:19 +0200 Subject: [PATCH 049/237] Remove unused parameter. --- src/gemseo/disciplines/ode_discipline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 2078a7f727..e069fee9b6 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -85,7 +85,6 @@ class ODEDiscipline(MDODiscipline): ode_solver_name: str = "RK45", ode_solver_options: Mapping[str:Any] | None = None, name: str = "", - grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.SIMPLE, ): """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. @@ -104,7 +103,6 @@ class ODEDiscipline(MDODiscipline): ode_solver_name: The name of the solver used to solve the ODE. ode_solver_options: The options to pass to the ODE solver. name: The name of the discipline. - grammar_type: The way the grammar is defined. """ self.state_var_names = state_var_names self.ode_solver_name = ode_solver_name @@ -118,7 +116,7 @@ class ODEDiscipline(MDODiscipline): self.ode_mdo_func = None - super().__init__(name=name, grammar_type=grammar_type) + super().__init__(name=name, grammar_type=discipline.grammar_type) self.output_grammar.update_from_names(self.state_var_names + ["time_vector"]) self.ode_factory = ODESolversFactory() -- GitLab From 24a7a35abc657e91869408940131903b0ea96b9f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:30:54 +0200 Subject: [PATCH 050/237] Add function to create the ODE Discipline for the oscillator problem. --- src/gemseo/problems/ode/oscillator.py | 15 +++++++++++++++ tests/disciplines/test_ode_discipline.py | 13 ++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 722d8e37b5..5e129bf178 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -63,6 +63,7 @@ from numpy.typing import NDArray from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline +from gemseo.disciplines.ode_discipline import ODEDiscipline _initial_time = array([0.0]) _initial_position = array([0.0]) @@ -123,3 +124,17 @@ def create_oscillator_mdo_discipline() -> MDODiscipline: grammar_type=MDODiscipline.GrammarType.SIMPLE, ) return mdo_discipline + + +def create_oscillator_ode_discipline(time_vector) -> ODEDiscipline: + """Create the ODE discipline for solving the oscillator problem.""" + oscillator = create_oscillator_mdo_discipline() + ode_discipline = ODEDiscipline( + discipline=oscillator, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + final_time=max(time_vector), + time_vector=time_vector, + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + return ode_discipline diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 1da636d855..1b9f48123c 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -23,8 +23,7 @@ ODE stands for Ordinary Differential Equation. from __future__ import annotations import pytest -from gemseo.disciplines.ode_discipline import ODEDiscipline -from gemseo.problems.ode.oscillator import create_oscillator_mdo_discipline +from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.springs.springs import ChainedMasses from numpy import allclose from numpy import linspace @@ -33,16 +32,8 @@ from numpy import sin def test_oscillator_ode_discipline(): """Test the ODEDiscipline on with the oscillator validation case.""" - oscillator = create_oscillator_mdo_discipline() time_vector = linspace(0.0, 10, 5) - ode_disc = ODEDiscipline( - discipline=oscillator, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], - final_time=10, - time_vector=time_vector, - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) + ode_disc = create_oscillator_ode_discipline(time_vector) out = ode_disc.execute() analytical_solution = sin(2 * time_vector) / 2 -- GitLab From 06e57decffeedd510747793286aec93983aad111 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:31:43 +0200 Subject: [PATCH 051/237] Add type hints. --- src/gemseo/problems/ode/oscillator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 5e129bf178..3684a111b7 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -126,7 +126,7 @@ def create_oscillator_mdo_discipline() -> MDODiscipline: return mdo_discipline -def create_oscillator_ode_discipline(time_vector) -> ODEDiscipline: +def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem.""" oscillator = create_oscillator_mdo_discipline() ode_discipline = ODEDiscipline( -- GitLab From 4ee61e783a34c464d4bdd44ee2b5a0dbeb38f40c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:33:46 +0200 Subject: [PATCH 052/237] Write docstring. --- src/gemseo/problems/ode/oscillator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 3684a111b7..18a1980a51 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -127,7 +127,14 @@ def create_oscillator_mdo_discipline() -> MDODiscipline: def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: - """Create the ODE discipline for solving the oscillator problem.""" + """Create the ODE discipline for solving the oscillator problem. + + Args: + time_vector: The vector of times at which to solve the problem. + + Returns: + The ODEDiscipline representing the oscillator. + """ oscillator = create_oscillator_mdo_discipline() ode_discipline = ODEDiscipline( discipline=oscillator, -- GitLab From 28830c5efc0a7f69e113d59e027c3d8dffd8283b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:37:47 +0200 Subject: [PATCH 053/237] Improve docstrings. --- tests/disciplines/test_ode_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 1b9f48123c..0902655dc6 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -31,7 +31,7 @@ from numpy import sin def test_oscillator_ode_discipline(): - """Test the ODEDiscipline on with the oscillator validation case.""" + """Test the ODEDiscipline on the oscillator validation case.""" time_vector = linspace(0.0, 10, 5) ode_disc = create_oscillator_ode_discipline(time_vector) out = ode_disc.execute() @@ -41,7 +41,7 @@ def test_oscillator_ode_discipline(): def test_chained_masses(): - """Test the Springs class.""" + """Test the chained masses problem.""" kwargs = {"stiffnesses": [1, 1, 1], "masses": [1, 1]} springs_discipline = ChainedMasses(**kwargs) assert springs_discipline is not None -- GitLab From 0cbcf2606e2baef5c4552ec4c89481d9d5d3711b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:39:40 +0200 Subject: [PATCH 054/237] Test solution on more points. --- tests/disciplines/test_ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 0902655dc6..9cd23cbcc5 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -32,7 +32,7 @@ from numpy import sin def test_oscillator_ode_discipline(): """Test the ODEDiscipline on the oscillator validation case.""" - time_vector = linspace(0.0, 10, 5) + time_vector = linspace(0.0, 10, 30) ode_disc = create_oscillator_ode_discipline(time_vector) out = ode_disc.execute() -- GitLab From ea2303f740fa74de7b830ec3b5031268a076d612 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:40:10 +0200 Subject: [PATCH 055/237] Improve variable names. --- tests/disciplines/test_ode_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 9cd23cbcc5..109d337652 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -36,8 +36,8 @@ def test_oscillator_ode_discipline(): ode_disc = create_oscillator_ode_discipline(time_vector) out = ode_disc.execute() - analytical_solution = sin(2 * time_vector) / 2 - assert allclose(out["position"], analytical_solution) + analytical_position = sin(2 * time_vector) / 2 + assert allclose(out["position"], analytical_position) def test_chained_masses(): -- GitLab From a6c8619e2f1ce69dd7c39d5290caf0e0e1b00868 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 15:42:16 +0200 Subject: [PATCH 056/237] Test both parts of the solution. --- tests/disciplines/test_ode_discipline.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 109d337652..9972f1c2fe 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -26,6 +26,7 @@ import pytest from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.springs.springs import ChainedMasses from numpy import allclose +from numpy import cos from numpy import linspace from numpy import sin @@ -39,6 +40,9 @@ def test_oscillator_ode_discipline(): analytical_position = sin(2 * time_vector) / 2 assert allclose(out["position"], analytical_position) + analytical_velocity = cos(2 * time_vector) + assert allclose(out["velocity"], analytical_velocity) + def test_chained_masses(): """Test the chained masses problem.""" -- GitLab From d5b6359f4496c07002bcfcba33614f04f37d98a1 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:19:07 +0200 Subject: [PATCH 057/237] Add function that creates the MDO discipline for the problem. --- src/gemseo/problems/springs/springs.py | 30 ++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 132c22a047..fd67b45199 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -117,6 +117,30 @@ def _rhs_jacobian( return array([[0, -(left_stiff + right_stiff) * state[0] / mass], [1, 0]]) +def create_mass_mdo_discipline( + mass: float, left_stiff: float, right_stiff: float +) -> MDODiscipline: + """Create a discipline representing a single mass in the chain.""" + + def mass_rhs_function(time, position, velocity, left_state, right_state): + return _rhs_function( + time=time, + state=array([position, velocity]), + mass=mass, + left_stiff=left_stiff, + right_stiff=right_stiff, + left_state=left_state, + right_state=right_state, + ) + + mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=mass_rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + return mdo_discipline + + class Mass(ODEDiscipline): """Describe the behavior of a single mass held by two springs.""" @@ -143,10 +167,8 @@ class Mass(ODEDiscipline): self.state_name = state_name self.state_dot_name = state_dot_name - self.discipline = create_discipline( - "ODEDiscipline", - expressions={state_name: state_name}, - grammar_type=MDODiscipline.GrammarType.SIMPLE, + self.discipline = create_mass_mdo_discipline( + self.mass, self.left_stiff, self.right_stiff ) super().__init__( -- GitLab From e98ab68efd9d33e825cfb2a4629d9f805d08e868 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:20:42 +0200 Subject: [PATCH 058/237] Add function that creates the ODE discipline for the problem. --- src/gemseo/problems/springs/springs.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index fd67b45199..f89a0f3da2 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -141,6 +141,16 @@ def create_mass_mdo_discipline( return mdo_discipline +def create_mass_ode_discipline(): + mass = create_mass_mdo_discipline() + ode_discipline = ODEDiscipline( + discipline=mass, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + ) + return ode_discipline + + class Mass(ODEDiscipline): """Describe the behavior of a single mass held by two springs.""" -- GitLab From 458cb59d7f5a6db3570ba1aa97cd785d452682eb Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:25:03 +0200 Subject: [PATCH 059/237] Fix function parameters. --- src/gemseo/problems/springs/springs.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index f89a0f3da2..5d108c208e 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -80,6 +80,9 @@ from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +_state_var_names = ["position", "velocity"] +_state_dot_var_name = ["position_dot", "velocity_dot"] + def _rhs_function( time: float, @@ -141,12 +144,18 @@ def create_mass_mdo_discipline( return mdo_discipline -def create_mass_ode_discipline(): - mass = create_mass_mdo_discipline() +def create_mass_ode_discipline( + mass: float, + left_stiff: float, + right_stiff: float, + state_var_names: list[str] = _state_var_names, + state_dot_var_name: list[str] = _state_dot_var_name, +) -> ODEDiscipline: + mass = create_mass_mdo_discipline(mass, left_stiff, right_stiff) ode_discipline = ODEDiscipline( discipline=mass, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], + state_var_names=state_var_names, + state_dot_var_names=state_dot_var_name, ) return ode_discipline -- GitLab From bc7eda8367acdd05f7b5e164d84e88020a42a261 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:32:43 +0200 Subject: [PATCH 060/237] Docstrings. --- src/gemseo/problems/springs/springs.py | 59 ++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 5d108c208e..fa6558f458 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -93,7 +93,21 @@ def _rhs_function( right_stiff: float, right_state: NDArray[float], ) -> NDArray[float]: - """Right-hand side function of the differential equation for a single spring.""" + """Right-hand side function of the differential equation for a single spring. + + Args: + time: The time at which the right-hand side function is evaluated. + state: The position and velocity of the mass at `time`. + left_stiff: The stiffness of the spring on the left-hand side of the mass. + left_state: The position and velocity of the mass on the left-hand side of the + current mass at `time`. + right_stiff: The stiffness of the spring on the right-hand side of the mass. + right_state: The position and velocity of the mass on the right-hand side of the + current mass at `time`. + + Returns: + The derivative of the state at `time`. + """ return array( [ state[1], @@ -116,14 +130,40 @@ def _rhs_jacobian( right_stiff: float, right_state: NDArray[float], ) -> NDArray[float]: - """Jacobian of the right-hand side of the ODE for a single spring.""" + """Jacobian of the right-hand side of the ODE for a single spring. + + The Jacobian is a function of the position (but not the velocity) of neighboring \ + springs. + + Args: + time: The time at which the right-hand side function is evaluated. + state: The position and velocity of the mass at `time`. + left_stiff: The stiffness of the spring on the left-hand side of the mass. + left_state: The position and velocity of the mass on the left-hand side of the + current mass at `time`. + right_stiff: The stiffness of the spring on the right-hand side of the mass. + right_state: The position and velocity of the mass on the right-hand side of the + current mass at `time`. + + Returns: + The Jacobian of the state at `time`. + """ return array([[0, -(left_stiff + right_stiff) * state[0] / mass], [1, 0]]) def create_mass_mdo_discipline( mass: float, left_stiff: float, right_stiff: float ) -> MDODiscipline: - """Create a discipline representing a single mass in the chain.""" + """Create a discipline representing a single mass in the chain. + + Args: + mass: The value of the mass. + left_stiff: The stiffness of the spring on the left-hand side. + right_stiff: The stiffness of the spring on the right-hand side. + + Returns: + The MDODiscipline describing a single point mass. + """ def mass_rhs_function(time, position, velocity, left_state, right_state): return _rhs_function( @@ -151,6 +191,19 @@ def create_mass_ode_discipline( state_var_names: list[str] = _state_var_names, state_dot_var_name: list[str] = _state_dot_var_name, ) -> ODEDiscipline: + """Create a discipline describing the motion of a single mass in the chain. + + Args: + mass: The value of the mass. + left_stiff: The stiffness of the spring on the left-hand side. + right_stiff: The stiffness of the spring on the right-hand side. + state_var_names: The names of the state variables. + state_dot_var_name: The names of the derivatives of the state variables relative + to time. + + Returns: + The MDODiscipline describing a single point mass. + """ mass = create_mass_mdo_discipline(mass, left_stiff, right_stiff) ode_discipline = ODEDiscipline( discipline=mass, -- GitLab From a687af956a351070399e094867ef7332854f9727 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:39:47 +0200 Subject: [PATCH 061/237] Improve variable names. --- src/gemseo/problems/springs/springs.py | 29 +++++++++----------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index fa6558f458..591e6b946c 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -89,9 +89,9 @@ def _rhs_function( state: NDArray[float], mass: float, left_stiff: float, - left_state: NDArray[float], + left_position: NDArray[float], right_stiff: float, - right_state: NDArray[float], + right_position: NDArray[float], ) -> NDArray[float]: """Right-hand side function of the differential equation for a single spring. @@ -99,11 +99,11 @@ def _rhs_function( time: The time at which the right-hand side function is evaluated. state: The position and velocity of the mass at `time`. left_stiff: The stiffness of the spring on the left-hand side of the mass. - left_state: The position and velocity of the mass on the left-hand side of the + left_position: The position of the mass on the left-hand side of the current mass at `time`. right_stiff: The stiffness of the spring on the right-hand side of the mass. - right_state: The position and velocity of the mass on the right-hand side of the - current mass at `time`. + right_position: The position of the mass on the right-hand side of the current + mass at `time`. Returns: The derivative of the state at `time`. @@ -113,8 +113,8 @@ def _rhs_function( state[1], ( -(left_stiff + right_stiff) * state[0] - + left_stiff * left_state - + right_stiff * right_state + + left_stiff * left_position + + right_stiff * right_position ) / mass, ] @@ -126,24 +126,15 @@ def _rhs_jacobian( state: NDArray[float], mass: float, left_stiff: float, - left_state: NDArray[float], right_stiff: float, - right_state: NDArray[float], ) -> NDArray[float]: """Jacobian of the right-hand side of the ODE for a single spring. - The Jacobian is a function of the position (but not the velocity) of neighboring \ - springs. - Args: time: The time at which the right-hand side function is evaluated. state: The position and velocity of the mass at `time`. left_stiff: The stiffness of the spring on the left-hand side of the mass. - left_state: The position and velocity of the mass on the left-hand side of the - current mass at `time`. right_stiff: The stiffness of the spring on the right-hand side of the mass. - right_state: The position and velocity of the mass on the right-hand side of the - current mass at `time`. Returns: The Jacobian of the state at `time`. @@ -165,15 +156,15 @@ def create_mass_mdo_discipline( The MDODiscipline describing a single point mass. """ - def mass_rhs_function(time, position, velocity, left_state, right_state): + def mass_rhs_function(time, position, velocity, left_position, right_position): return _rhs_function( time=time, state=array([position, velocity]), mass=mass, left_stiff=left_stiff, right_stiff=right_stiff, - left_state=left_state, - right_state=right_state, + left_position=left_position, + right_position=right_position, ) mdo_discipline = create_discipline( -- GitLab From 439d9ba61a81a4b35f74c2fb0ec5dde3ab9284de Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:41:10 +0200 Subject: [PATCH 062/237] Remove redundant Mass class. --- src/gemseo/problems/springs/springs.py | 40 +------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 591e6b946c..0da3585e2b 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -204,44 +204,6 @@ def create_mass_ode_discipline( return ode_discipline -class Mass(ODEDiscipline): - """Describe the behavior of a single mass held by two springs.""" - - mass: float - """The value of the mass.""" - - left_stiff: float - """The stiffness of the spring on the left-hand side of the mass.""" - - right_stiff: float - """The stiffness of the spring on the right-hand side of the mass.""" - - def __init__( - self, - mass: float, - left_stiff: float, - right_stiff: float, - state_name: str = "x", - state_dot_name: str = "x_dot", - ) -> None: - self.mass = mass - self.left_stiff = left_stiff - self.right_stiff = right_stiff - self.state_name = state_name - self.state_dot_name = state_dot_name - - self.discipline = create_mass_mdo_discipline( - self.mass, self.left_stiff, self.right_stiff - ) - - super().__init__( - discipline=self.discipline, - state_var_names=[self.state_name], - state_dot_var_names=[self.state_dot_name], - time_var_name="time", - ) - - class ChainedMasses: """Describes the behavior of a series of masses connected by springs.""" @@ -254,7 +216,7 @@ class ChainedMasses: self.state_var_names = ["x" + str(e) for e in range(1, self.n_masses + 1)] self.disciplines = [ - Mass(m, ls, rs, svn) + create_mass_ode_discipline(m, ls, rs, svn) for m, ls, rs, svn in zip( masses, stiffnesses[: self.n_masses], -- GitLab From e1b5e6e8b64cab2bb68c739a897ac59673f47246 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:53:56 +0200 Subject: [PATCH 063/237] Write run function for chain of masses. --- src/gemseo/problems/springs/springs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 0da3585e2b..6d262a2d28 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -77,6 +77,7 @@ from numpy import array from numpy.typing import NDArray from gemseo import create_discipline +from gemseo import create_mda from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline @@ -226,4 +227,6 @@ class ChainedMasses: ] def _run(self) -> None: - pass + mda = create_mda("MDAGaussSeidel", self.disciplines) + result = mda.execute() + return result -- GitLab From e08d7fe5b63b507d9cd7a10845074e5d86de3891 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 16:54:59 +0200 Subject: [PATCH 064/237] Test the execution of the chain of masses. --- tests/disciplines/test_ode_discipline.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 9972f1c2fe..5bff17923f 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -50,6 +50,8 @@ def test_chained_masses(): springs_discipline = ChainedMasses(**kwargs) assert springs_discipline is not None + springs_discipline.execute() + @pytest.mark.parametrize( "kwargs", -- GitLab From ee7f642f4e36cf9ea6822224010a6c8564d13025 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 17:02:54 +0200 Subject: [PATCH 065/237] Add test for trivial chained masses case. --- tests/disciplines/test_ode_discipline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 5bff17923f..83a7ecfc7b 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -44,9 +44,15 @@ def test_oscillator_ode_discipline(): assert allclose(out["velocity"], analytical_velocity) +def test_single_mass(): + """Test the resolution for a single mass connected by springs to fixed points.""" + single_mass_discipline = ChainedMasses(stiffnesses=[1, 1], masses=[1]) + single_mass_discipline.execute() + + def test_chained_masses(): """Test the chained masses problem.""" - kwargs = {"stiffnesses": [1, 1, 1], "masses": [1, 1]} + kwargs = {"stiffnesses": [1, 1, 1, 1], "masses": [1, 1, 1]} springs_discipline = ChainedMasses(**kwargs) assert springs_discipline is not None -- GitLab From 29f9f43d2690eb41da30ccf2557d71585e78d73f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 17:06:11 +0200 Subject: [PATCH 066/237] Create example to illustrate how to use an ODEDiscipline. --- .../ode/plot_oscillator_discipline.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 doc_src/_examples/ode/plot_oscillator_discipline.py diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py new file mode 100644 index 0000000000..d10677d0fa --- /dev/null +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -0,0 +1,22 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This work is licensed under a BSD 0-Clause License. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, +# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# Contributors: +# Isabelle Santos +""" +Execute an ODEDiscipline: a simple oscillator +============================================= +""" +from __future__ import annotations -- GitLab From 73c3811cc2efeb267a1eaed57562568dc07696b9 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 17:13:43 +0200 Subject: [PATCH 067/237] Introduction of the tutorial. --- .../_examples/ode/plot_oscillator_discipline.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index d10677d0fa..d52729f2de 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -20,3 +20,17 @@ Execute an ODEDiscipline: a simple oscillator ============================================= """ from __future__ import annotations + + +# %% +# This tutorial describes how to use an :class:`ODEDiscipline` in |g|. +# +# ODE stands for Ordinary Differential Equation. An :class:`ODEDiscipline` is an +# :class:`MDODiscipline` that is defined using an ODE. +# +# To illustrate the basic usage of this feature, we use a simple oscillator problem. +# +# Step 1: The ODE describing the motion of the oscillator +# ....................................................... +# +# -- GitLab From 1e58bd2e1a5266c787e7a65c8cb646f72d34bd9e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 17:24:07 +0200 Subject: [PATCH 068/237] Describe right-hand side function of oscillator. --- .../ode/plot_oscillator_discipline.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index d52729f2de..ad3b7182ba 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -33,4 +33,29 @@ from __future__ import annotations # Step 1: The ODE describing the motion of the oscillator # ....................................................... # +# The motion of a simple oscillator is described by the equation # +# ..math:: +# +# \frac{d^2x}{dt^2} = -\omega ^ 2\dot{x} +# +# with :math:`\omega \in \mathbb{R}`. Let's re-write this equation as a 1st order ODE. +# +# ..math:: +# +# \left\{\begin{array} +# \dot(x) = y \\ +# \dot(y) = -\omega^2 x +# \end{array}\right. +# +# where :math:`x` is the position and :math:`y` is the velocity of the oscillator. +# +# To use |g|, we can define the right-hand side term of this equation as follows: + +omega = 2 + + +def oscillator_ode_rhs_function(time, position, velocity): + position_dot = velocity + velocity_dot = -omega * position + return position_dot, velocity_dot -- GitLab From 7e6805099fee1c4308910f158403dbbf0d8a6dff Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 17:42:53 +0200 Subject: [PATCH 069/237] Next steps in tutorial. --- .../ode/plot_oscillator_discipline.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index ad3b7182ba..86a0633b15 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -21,6 +21,11 @@ Execute an ODEDiscipline: a simple oscillator """ from __future__ import annotations +import matplotlib.pyplot as plt +from gemseo import create_discipline +from gemseo import MDODiscipline +from gemseo.disciplines.ode_discipline import ODEDiscipline +from numpy import linspace # %% # This tutorial describes how to use an :class:`ODEDiscipline` in |g|. @@ -59,3 +64,45 @@ def oscillator_ode_rhs_function(time, position, velocity): position_dot = velocity velocity_dot = -omega * position return position_dot, velocity_dot + + +# %% +# We want to solve the oscillator problem for a set of time values: + +time_vector = linspace(0.0, 10, 30) + +# %% +# Step 2: Create a discipline +# ........................... +# + +mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=oscillator_ode_rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, +) + +# %% +# Step 3: Create and solve the ODEDiscipline +# .......................................... +# +state_var_names = ["position", "velocity"] +ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=state_var_names, + state_dot_var_names=[e + "_dot" for e in state_var_names], + final_time=max(time_vector), + time_vector=time_vector, +) + +result = ode_discipline.execute() + +# %% +# Step 4: Visualize the result +# ............................ +# + +for name in state_var_names: + plt.plot(time_vector, result[name], label=name) + +plt.show() -- GitLab From 2787531d71564575406f5feb1fc26b008cda58f4 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 17:51:00 +0200 Subject: [PATCH 070/237] Make passing the names of the derivatives optional. --- src/gemseo/disciplines/ode_discipline.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index e069fee9b6..bcb4a13180 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -77,8 +77,8 @@ class ODEDiscipline(MDODiscipline): self, discipline: MDODiscipline, state_var_names: list[str], - state_dot_var_names: list[str], time_vector: NDArray, + state_dot_var_names: list[str] | None = None, time_var_name: str = "time", initial_time: float = 0.0, final_time: float = 1.0, @@ -112,7 +112,10 @@ class ODEDiscipline(MDODiscipline): self.discipline = discipline self.time_var_name = time_var_name - self.state_dot_var_names = state_dot_var_names + if state_dot_var_names is None: + self.state_dot_var_names = [e + "_dot" for e in state_var_names] + else: + self.state_dot_var_names = state_dot_var_names self.ode_mdo_func = None -- GitLab From 60a83ef4a298e7e6835a6466a791b1c6a5d14a05 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 6 Jul 2023 18:05:12 +0200 Subject: [PATCH 071/237] Provide a default value for the state variable names. --- src/gemseo/problems/ode/oscillator.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 18a1980a51..978728a0ce 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -69,6 +69,7 @@ _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) _omega = 4 +default_state_var_names = ["position", "velocity"] def oscillator_ode_rhs_function( @@ -126,10 +127,16 @@ def create_oscillator_mdo_discipline() -> MDODiscipline: return mdo_discipline -def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: +def create_oscillator_ode_discipline( + time_vector: NDArray[float], + state_var_names=default_state_var_names, + state_dot_var_names=None, +) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem. Args: + state_var_names: Names of the state variables. + state_dot_var_names: Names of the state variable derivatives wrt time. time_vector: The vector of times at which to solve the problem. Returns: @@ -138,8 +145,8 @@ def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipli oscillator = create_oscillator_mdo_discipline() ode_discipline = ODEDiscipline( discipline=oscillator, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], + state_var_names=state_var_names, + state_dot_var_names=state_dot_var_names, final_time=max(time_vector), time_vector=time_vector, ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, -- GitLab From 2bbc33e0f71e714a4d8050eb4c2342acb809f222 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 11 Jul 2023 15:54:55 +0200 Subject: [PATCH 072/237] Remove useless parameters, because names must match variable names used when defining the ODE. --- src/gemseo/problems/ode/oscillator.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 978728a0ce..73553c69a4 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -70,6 +70,7 @@ _initial_position = array([0.0]) _initial_velocity = array([1.0]) _omega = 4 default_state_var_names = ["position", "velocity"] +default_state_dot_var_names = ["position_dot", "velocity_dot"] def oscillator_ode_rhs_function( @@ -127,16 +128,10 @@ def create_oscillator_mdo_discipline() -> MDODiscipline: return mdo_discipline -def create_oscillator_ode_discipline( - time_vector: NDArray[float], - state_var_names=default_state_var_names, - state_dot_var_names=None, -) -> ODEDiscipline: +def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem. Args: - state_var_names: Names of the state variables. - state_dot_var_names: Names of the state variable derivatives wrt time. time_vector: The vector of times at which to solve the problem. Returns: @@ -145,8 +140,8 @@ def create_oscillator_ode_discipline( oscillator = create_oscillator_mdo_discipline() ode_discipline = ODEDiscipline( discipline=oscillator, - state_var_names=state_var_names, - state_dot_var_names=state_dot_var_names, + state_var_names=default_state_var_names, + initial_time=min(time_vector), final_time=max(time_vector), time_vector=time_vector, ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, -- GitLab From c2e6377023e43bbb17782496b91ba2b6b4288b0d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 11 Jul 2023 16:00:26 +0200 Subject: [PATCH 073/237] Need to test what happens with bad user input. --- tests/disciplines/test_ode_discipline.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 83a7ecfc7b..019a6ce9bd 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -44,6 +44,10 @@ def test_oscillator_ode_discipline(): assert allclose(out["velocity"], analytical_velocity) +def test_ode_discipline_bad_grammar(): + pass + + def test_single_mass(): """Test the resolution for a single mass connected by springs to fixed points.""" single_mass_discipline = ChainedMasses(stiffnesses=[1, 1], masses=[1]) -- GitLab From d8eab314297ab3af6b1ddc63cec75f8a3100f3a4 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 11 Jul 2023 16:50:13 +0200 Subject: [PATCH 074/237] Fix names of parameters returned by the ODE function. --- src/gemseo/problems/springs/springs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 6d262a2d28..0a6a0ecef8 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -158,7 +158,7 @@ def create_mass_mdo_discipline( """ def mass_rhs_function(time, position, velocity, left_position, right_position): - return _rhs_function( + position_dot, velocity_dot = _rhs_function( time=time, state=array([position, velocity]), mass=mass, @@ -167,6 +167,7 @@ def create_mass_mdo_discipline( left_position=left_position, right_position=right_position, ) + return position_dot, velocity_dot mdo_discipline = create_discipline( "AutoPyDiscipline", -- GitLab From c6e4498d417b635a744fe7e18e7380590c7b00b5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 11 Jul 2023 16:51:15 +0200 Subject: [PATCH 075/237] Add time vector to parameters. --- src/gemseo/problems/springs/springs.py | 3 +++ tests/disciplines/test_ode_discipline.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 0a6a0ecef8..ab49fc18d0 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -181,6 +181,7 @@ def create_mass_ode_discipline( mass: float, left_stiff: float, right_stiff: float, + time_vector: NDArray[float], state_var_names: list[str] = _state_var_names, state_dot_var_name: list[str] = _state_dot_var_name, ) -> ODEDiscipline: @@ -190,6 +191,7 @@ def create_mass_ode_discipline( mass: The value of the mass. left_stiff: The stiffness of the spring on the left-hand side. right_stiff: The stiffness of the spring on the right-hand side. + time_vector: The times at which the solution must be evaluated. state_var_names: The names of the state variables. state_dot_var_name: The names of the derivatives of the state variables relative to time. @@ -202,6 +204,7 @@ def create_mass_ode_discipline( discipline=mass, state_var_names=state_var_names, state_dot_var_names=state_dot_var_name, + time_vector=time_vector, ) return ode_discipline diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 019a6ce9bd..f47e560cac 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,6 +25,7 @@ from __future__ import annotations import pytest from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.springs.springs import ChainedMasses +from gemseo.problems.springs.springs import create_mass_ode_discipline from numpy import allclose from numpy import cos from numpy import linspace @@ -50,8 +51,14 @@ def test_ode_discipline_bad_grammar(): def test_single_mass(): """Test the resolution for a single mass connected by springs to fixed points.""" - single_mass_discipline = ChainedMasses(stiffnesses=[1, 1], masses=[1]) - single_mass_discipline.execute() + time_vector = linspace(0.0, 10, 30) + single_mass_ode_discipline = create_mass_ode_discipline( + mass=1, + left_stiff=1, + right_stiff=1, + time_vector=time_vector, + ) + assert single_mass_ode_discipline is not None def test_chained_masses(): -- GitLab From 7e698a99a6b6e83fe1db5333dd17eb33512da466 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 11:36:12 +0200 Subject: [PATCH 076/237] Remove useless function. --- src/gemseo/problems/ode/oscillator.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 73553c69a4..0efcc156eb 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -114,20 +114,6 @@ def oscillator_ode_rhs_jacobian( return array([[-(_omega**2), 1], [0, 0]]) -def create_oscillator_mdo_discipline() -> MDODiscipline: - """Create the discipline that represents the oscillator problem. - - Returns: - The MDODiscipline that describes the oscillator ODE problem. - """ - mdo_discipline = create_discipline( - "AutoPyDiscipline", - py_func=oscillator_ode_rhs_function, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - return mdo_discipline - - def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem. @@ -137,7 +123,11 @@ def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipli Returns: The ODEDiscipline representing the oscillator. """ - oscillator = create_oscillator_mdo_discipline() + oscillator = create_discipline( + "AutoPyDiscipline", + py_func=oscillator_ode_rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) ode_discipline = ODEDiscipline( discipline=oscillator, state_var_names=default_state_var_names, -- GitLab From 2e1122931d669312ee1dfb5d316e81de4ebce837 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 11:59:34 +0200 Subject: [PATCH 077/237] Remove useless function. --- src/gemseo/problems/ode/oscillator.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 0efcc156eb..03f5f05436 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -94,26 +94,6 @@ def oscillator_ode_rhs_function( return position_dot, velocity_dot -def oscillator_ode_rhs_jacobian( - time: NDArray[float] = _initial_time, - position: NDArray[float] = _initial_position, - velocity: NDArray[float] = _initial_velocity, -) -> NDArray[float]: - """Jacobian of the right-hand side of the oscillator ODE problem. - - This is a constant 2x2 matrix. - - Args: - time: The value of the time for the initial conditions. - position: The position of the system at `time`. - velocity: The value of the first derivative of the position at `time`. - - Returns: - The value of the Jacobian. - """ - return array([[-(_omega**2), 1], [0, 0]]) - - def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem. -- GitLab From e481cca2c69a9a792fa62a6dd9b55c0d25893931 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 14:42:22 +0200 Subject: [PATCH 078/237] Names can't be changed. --- src/gemseo/problems/ode/oscillator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 03f5f05436..489fdb1d38 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -69,8 +69,6 @@ _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) _omega = 4 -default_state_var_names = ["position", "velocity"] -default_state_dot_var_names = ["position_dot", "velocity_dot"] def oscillator_ode_rhs_function( @@ -110,7 +108,7 @@ def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipli ) ode_discipline = ODEDiscipline( discipline=oscillator, - state_var_names=default_state_var_names, + state_var_names=["position", "velocity"], initial_time=min(time_vector), final_time=max(time_vector), time_vector=time_vector, -- GitLab From 983e01d2531787d437c5c80bec625ec5bae96f33 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 14:48:12 +0200 Subject: [PATCH 079/237] Make omega a parameter of the oscillator. --- src/gemseo/problems/ode/oscillator.py | 22 +++++++++++++++++++--- tests/disciplines/test_ode_discipline.py | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 489fdb1d38..e823e80957 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -75,6 +75,7 @@ def oscillator_ode_rhs_function( time: NDArray[float] = _initial_time, position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, + omega: float = _omega, ) -> (NDArray[float], NDArray[float]): """Right-hand side of the oscillator ODE problem. @@ -82,28 +83,43 @@ def oscillator_ode_rhs_function( time: The value of the time. position: The position of the system at `time`. velocity: The value of the first derivative of the position at `time`. + omega: Period squared of the oscillator. Returns: The derivative of the position at `time`. The derivative of the velocity at `time`. """ position_dot = velocity - velocity_dot = -_omega * position + velocity_dot = -omega * position return position_dot, velocity_dot -def create_oscillator_ode_discipline(time_vector: NDArray[float]) -> ODEDiscipline: +def create_oscillator_ode_discipline( + time_vector: NDArray[float], omega: float = _omega +) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem. Args: time_vector: The vector of times at which to solve the problem. + omega: Period squared of the oscillator. Returns: The ODEDiscipline representing the oscillator. """ + + def rhs_function( + time: NDArray[float] = _initial_time, + position: NDArray[float] = _initial_position, + velocity: NDArray[float] = _initial_velocity, + ): + position_dot, velocity_dot = oscillator_ode_rhs_function( + time, position, velocity, omega + ) + return position_dot, velocity_dot + oscillator = create_discipline( "AutoPyDiscipline", - py_func=oscillator_ode_rhs_function, + py_func=rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) ode_discipline = ODEDiscipline( diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index f47e560cac..43b29b3e53 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -35,7 +35,7 @@ from numpy import sin def test_oscillator_ode_discipline(): """Test the ODEDiscipline on the oscillator validation case.""" time_vector = linspace(0.0, 10, 30) - ode_disc = create_oscillator_ode_discipline(time_vector) + ode_disc = create_oscillator_ode_discipline(time_vector, omega=4) out = ode_disc.execute() analytical_position = sin(2 * time_vector) / 2 -- GitLab From 5a9321ec46cd137f2386b6eda3357ded55ce212b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 14:59:48 +0200 Subject: [PATCH 080/237] Auxiliary function is limited to scope of function where it is defined. --- src/gemseo/problems/ode/oscillator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index e823e80957..987f4850e3 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -107,7 +107,7 @@ def create_oscillator_ode_discipline( The ODEDiscipline representing the oscillator. """ - def rhs_function( + def _rhs_function( time: NDArray[float] = _initial_time, position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, @@ -119,7 +119,7 @@ def create_oscillator_ode_discipline( oscillator = create_discipline( "AutoPyDiscipline", - py_func=rhs_function, + py_func=_rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) ode_discipline = ODEDiscipline( -- GitLab From de5f19821aaa3ac06efb4b315540ec89483868bf Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 15:27:20 +0200 Subject: [PATCH 081/237] Use function rather than class to define the chained masses problem. --- src/gemseo/problems/springs/springs.py | 28 ++------------------------ 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index ab49fc18d0..a53be92b3f 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -77,7 +77,6 @@ from numpy import array from numpy.typing import NDArray from gemseo import create_discipline -from gemseo import create_mda from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline @@ -209,28 +208,5 @@ def create_mass_ode_discipline( return ode_discipline -class ChainedMasses: - """Describes the behavior of a series of masses connected by springs.""" - - def __init__(self, stiffnesses: list, masses: list) -> None: - self.n_masses = len(masses) - if not len(stiffnesses) == self.n_masses + 1: - from warnings import warn - - warn("Stiffnesses and masses have incoherent lengths.") - - self.state_var_names = ["x" + str(e) for e in range(1, self.n_masses + 1)] - self.disciplines = [ - create_mass_ode_discipline(m, ls, rs, svn) - for m, ls, rs, svn in zip( - masses, - stiffnesses[: self.n_masses], - stiffnesses[:-1], - self.state_var_names, - ) - ] - - def _run(self) -> None: - mda = create_mda("MDAGaussSeidel", self.disciplines) - result = mda.execute() - return result +def create_chained_masses(): + pass -- GitLab From 7beee95d3076cb2e37cbfc95d3698c31752446f0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 15:40:47 +0200 Subject: [PATCH 082/237] Skeleton of function. --- src/gemseo/problems/springs/springs.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index a53be92b3f..4f8689eec7 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -208,5 +208,16 @@ def create_mass_ode_discipline( return ode_discipline -def create_chained_masses(): - pass +def create_chained_masses(masses, stiffnesses, time_vector): + if len(masses) != len(stiffnesses) - 1: + raise ValueError("Stiffnesses and masses have incoherent lengths.") + + mass_disciplines = [] + for i, mass in enumerate(masses): + mass_disciplines.append( + create_mass_ode_discipline( + mass, stiffnesses[i], stiffnesses[i + 1], time_vector + ) + ) + + return mass_disciplines -- GitLab From e0ba81a9eb6e6893b692a66d4a11d37b03ab5afe Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 15:44:28 +0200 Subject: [PATCH 083/237] Make name more explicit. --- src/gemseo/problems/springs/springs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 4f8689eec7..5410b63d3b 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -198,9 +198,9 @@ def create_mass_ode_discipline( Returns: The MDODiscipline describing a single point mass. """ - mass = create_mass_mdo_discipline(mass, left_stiff, right_stiff) + mass_discipline = create_mass_mdo_discipline(mass, left_stiff, right_stiff) ode_discipline = ODEDiscipline( - discipline=mass, + discipline=mass_discipline, state_var_names=state_var_names, state_dot_var_names=state_dot_var_name, time_vector=time_vector, -- GitLab From 787bafe2ae0063b80956039bc04de9f281357062 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 15:53:30 +0200 Subject: [PATCH 084/237] Remove bad import. --- tests/disciplines/test_ode_discipline.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 43b29b3e53..dfb880bcbf 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -24,7 +24,6 @@ from __future__ import annotations import pytest from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline -from gemseo.problems.springs.springs import ChainedMasses from gemseo.problems.springs.springs import create_mass_ode_discipline from numpy import allclose from numpy import cos @@ -63,11 +62,6 @@ def test_single_mass(): def test_chained_masses(): """Test the chained masses problem.""" - kwargs = {"stiffnesses": [1, 1, 1, 1], "masses": [1, 1, 1]} - springs_discipline = ChainedMasses(**kwargs) - assert springs_discipline is not None - - springs_discipline.execute() @pytest.mark.parametrize( @@ -80,5 +74,3 @@ def test_chained_masses(): ) def test_chained_masses_wrong_lengths(kwargs): """Test the error messages when incoherent input is provided.""" - with pytest.raises(ValueError, match="incoherent lengths"): - springs_discipline = ChainedMasses(**kwargs) # noqa: F841 -- GitLab From 211061bc87eaf6264b31832995e284d29a74309e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 16:26:09 +0200 Subject: [PATCH 085/237] Fix type hint. --- src/gemseo/disciplines/ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index bcb4a13180..878ce2cb5f 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -77,7 +77,7 @@ class ODEDiscipline(MDODiscipline): self, discipline: MDODiscipline, state_var_names: list[str], - time_vector: NDArray, + time_vector: NDArray[float], state_dot_var_names: list[str] | None = None, time_var_name: str = "time", initial_time: float = 0.0, -- GitLab From fc20005d305dc1443e4061f584cde8147417579d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 12 Jul 2023 16:26:39 +0200 Subject: [PATCH 086/237] Fix type hint. --- src/gemseo/disciplines/ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 878ce2cb5f..02660c3628 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -49,7 +49,7 @@ class ODEDiscipline(MDODiscipline): time_var_name: str """The name of the time variable.""" - time_vector: NDArray + time_vector: NDArray[float] """The times at which the solution is evaluated.""" state_var_names: list[str] -- GitLab From 15cfd57e54ba4c362799a8a7dd1045bbda0ec608 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 13 Jul 2023 09:44:45 +0200 Subject: [PATCH 087/237] Fix tutorial. --- doc_src/_examples/ode/plot_oscillator_discipline.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index 86a0633b15..e7ef096aa8 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -57,7 +57,7 @@ from numpy import linspace # # To use |g|, we can define the right-hand side term of this equation as follows: -omega = 2 +omega = 4 def oscillator_ode_rhs_function(time, position, velocity): @@ -75,6 +75,8 @@ time_vector = linspace(0.0, 10, 30) # Step 2: Create a discipline # ........................... # +# Next, we create an MDO discipline that will be used to build the ODE discipline. +# mdo_discipline = create_discipline( "AutoPyDiscipline", @@ -86,11 +88,13 @@ mdo_discipline = create_discipline( # Step 3: Create and solve the ODEDiscipline # .......................................... # +# We create the ODE discipline + state_var_names = ["position", "velocity"] ode_discipline = ODEDiscipline( discipline=mdo_discipline, state_var_names=state_var_names, - state_dot_var_names=[e + "_dot" for e in state_var_names], + initial_time=min(time_vector), final_time=max(time_vector), time_vector=time_vector, ) -- GitLab From af9667651dc369efbdb40870b809523dd90b5b04 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 13 Jul 2023 10:05:52 +0200 Subject: [PATCH 088/237] Fix tutorial. --- doc_src/_examples/ode/plot_oscillator_discipline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index e7ef096aa8..f0c360b20b 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -67,6 +67,9 @@ def oscillator_ode_rhs_function(time, position, velocity): # %% +# The first parameter of this function should be the time, and the following parameters +# should designate the state of the system being described. +# # We want to solve the oscillator problem for a set of time values: time_vector = linspace(0.0, 10, 30) @@ -88,7 +91,11 @@ mdo_discipline = create_discipline( # Step 3: Create and solve the ODEDiscipline # .......................................... # -# We create the ODE discipline +# The `state_var_names` are the names of the state parameters used as input for the +# `oscillator_ode_rhs_function`. These strings are used to create the grammar of the +# discipline. +# + state_var_names = ["position", "velocity"] ode_discipline = ODEDiscipline( -- GitLab From cdeb8a89eb5bf91d7978f2b8edd3460438522dbd Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 18 Jul 2023 11:22:17 +0200 Subject: [PATCH 089/237] Test behavior when user passes bad arguments for building the discipline grammar. --- tests/disciplines/test_ode_discipline.py | 43 +++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index dfb880bcbf..88f92a9a4b 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -23,9 +23,14 @@ ODE stands for Ordinary Differential Equation. from __future__ import annotations import pytest +from gemseo import create_discipline +from gemseo import MDODiscipline +from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline +from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function from gemseo.problems.springs.springs import create_mass_ode_discipline from numpy import allclose +from numpy import array from numpy import cos from numpy import linspace from numpy import sin @@ -45,7 +50,43 @@ def test_oscillator_ode_discipline(): def test_ode_discipline_bad_grammar(): - pass + _initial_time = array([0.0]) + _initial_position = array([0.0]) + _initial_velocity = array([1.0]) + + def _rhs_function( + time=_initial_time, + position=_initial_position, + velocity=_initial_velocity, + ): + position_dot, velocity_dot = oscillator_ode_rhs_function( + time, position, velocity, 4 + ) + return position_dot, velocity_dot + + oscillator = create_discipline( + "AutoPyDiscipline", + py_func=_rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + with pytest.raises(ValueError) as error_info: + bad_input_ode_discipline = ODEDiscipline( # noqa: F841 + discipline=oscillator, + state_var_names=["not_position", "not_velocity"], + time_vector=linspace(0.0, 10, 30), + ) + + assert "Missing default inputs" in str(error_info.value) + + with pytest.raises(ValueError) as error_info: + bad_output_ode_discipline = ODEDiscipline( # noqa: F841 + discipline=oscillator, + state_var_names=["position", "velocity"], + state_dot_var_names=["not_position_dot", "not_velocity_dot"], + time_vector=linspace(0.0, 10, 30), + ) + + assert "not outputs of the discipline" in str(error_info.value) def test_single_mass(): -- GitLab From bf908ea2283eea653b7a71b3f7404b0be2c0acc0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 18 Jul 2023 11:33:48 +0200 Subject: [PATCH 090/237] Remove unused function. --- src/gemseo/problems/springs/springs.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 5410b63d3b..96dcd16fa7 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -121,27 +121,6 @@ def _rhs_function( ) -def _rhs_jacobian( - time: float, - state: NDArray[float], - mass: float, - left_stiff: float, - right_stiff: float, -) -> NDArray[float]: - """Jacobian of the right-hand side of the ODE for a single spring. - - Args: - time: The time at which the right-hand side function is evaluated. - state: The position and velocity of the mass at `time`. - left_stiff: The stiffness of the spring on the left-hand side of the mass. - right_stiff: The stiffness of the spring on the right-hand side of the mass. - - Returns: - The Jacobian of the state at `time`. - """ - return array([[0, -(left_stiff + right_stiff) * state[0] / mass], [1, 0]]) - - def create_mass_mdo_discipline( mass: float, left_stiff: float, right_stiff: float ) -> MDODiscipline: -- GitLab From 4ee0b679e6c66d066385a2f5530d86ed5ae74c8f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 18 Jul 2023 11:41:17 +0200 Subject: [PATCH 091/237] Write test. --- src/gemseo/problems/springs/springs.py | 4 +++- tests/disciplines/test_ode_discipline.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 96dcd16fa7..233d877c6b 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -74,6 +74,7 @@ This can be re-written as a système of 1st order ordinary differential equation from __future__ import annotations from numpy import array +from numpy import linspace from numpy.typing import NDArray from gemseo import create_discipline @@ -82,6 +83,7 @@ from gemseo.disciplines.ode_discipline import ODEDiscipline _state_var_names = ["position", "velocity"] _state_dot_var_name = ["position_dot", "velocity_dot"] +_time_vector = linspace(0.0, 10, 30) def _rhs_function( @@ -187,7 +189,7 @@ def create_mass_ode_discipline( return ode_discipline -def create_chained_masses(masses, stiffnesses, time_vector): +def create_chained_masses(masses, stiffnesses, time_vector=_time_vector): if len(masses) != len(stiffnesses) - 1: raise ValueError("Stiffnesses and masses have incoherent lengths.") diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 88f92a9a4b..feac4f4243 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -28,6 +28,7 @@ from gemseo import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function +from gemseo.problems.springs.springs import create_chained_masses from gemseo.problems.springs.springs import create_mass_ode_discipline from numpy import allclose from numpy import array @@ -115,3 +116,7 @@ def test_chained_masses(): ) def test_chained_masses_wrong_lengths(kwargs): """Test the error messages when incoherent input is provided.""" + with pytest.raises(ValueError) as error_info: + create_chained_masses(**kwargs) + + assert "incoherent lengths" in str(error_info.value) -- GitLab From 314c98cddc5732570870b81649c6c8baa5cfdc02 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 18 Jul 2023 16:56:33 +0200 Subject: [PATCH 092/237] Add documentation. --- src/gemseo/problems/springs/springs.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 233d877c6b..66d1fb3482 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -86,7 +86,7 @@ _state_dot_var_name = ["position_dot", "velocity_dot"] _time_vector = linspace(0.0, 10, 30) -def _rhs_function( +def _mass_rhs_function( time: float, state: NDArray[float], mass: float, @@ -138,7 +138,7 @@ def create_mass_mdo_discipline( """ def mass_rhs_function(time, position, velocity, left_position, right_position): - position_dot, velocity_dot = _rhs_function( + position_dot, velocity_dot = _mass_rhs_function( time=time, state=array([position, velocity]), mass=mass, @@ -190,6 +190,13 @@ def create_mass_ode_discipline( def create_chained_masses(masses, stiffnesses, time_vector=_time_vector): + """Create a list of coupled disciplines describing the masses connected by springs. + + Args: + masses: The values for the masses. + stiffnesses: The values for the stiffness of each spring. + time_vector: The times for which the problem should be solved. + """ if len(masses) != len(stiffnesses) - 1: raise ValueError("Stiffnesses and masses have incoherent lengths.") -- GitLab From 2fcd2b8bbdc50ca4cc71cb148bc966d93db16c55 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 20 Jul 2023 11:11:17 +0200 Subject: [PATCH 093/237] Add missing docstring. --- src/gemseo/problems/springs/springs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 66d1fb3482..4a0f9d1b2a 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -100,6 +100,7 @@ def _mass_rhs_function( Args: time: The time at which the right-hand side function is evaluated. state: The position and velocity of the mass at `time`. + mass: The value of the mass. left_stiff: The stiffness of the spring on the left-hand side of the mass. left_position: The position of the mass on the left-hand side of the current mass at `time`. -- GitLab From 9e7216944a03068734088834aee22a07840a90e5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 27 Jul 2023 09:52:29 +0200 Subject: [PATCH 094/237] Write tests. --- src/gemseo/problems/springs/springs.py | 71 ++++++++++++++++--- tests/disciplines/test_ode_discipline.py | 86 +++++++++++++++++++++--- 2 files changed, 137 insertions(+), 20 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 4a0f9d1b2a..10ed658b8a 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -80,13 +80,14 @@ from numpy.typing import NDArray from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.disciplines.remapping import RemappingDiscipline _state_var_names = ["position", "velocity"] -_state_dot_var_name = ["position_dot", "velocity_dot"] +_state_dot_var_names = ["position_dot", "velocity_dot"] _time_vector = linspace(0.0, 10, 30) -def _mass_rhs_function( +def generic_mass_rhs_function( time: float, state: NDArray[float], mass: float, @@ -124,6 +125,10 @@ def _mass_rhs_function( ) +class RhsMdoDiscipline(MDODiscipline): + pass + + def create_mass_mdo_discipline( mass: float, left_stiff: float, right_stiff: float ) -> MDODiscipline: @@ -138,8 +143,8 @@ def create_mass_mdo_discipline( The MDODiscipline describing a single point mass. """ - def mass_rhs_function(time, position, velocity, left_position, right_position): - position_dot, velocity_dot = _mass_rhs_function( + def _mass_rhs(time, position, velocity, left_position, right_position): + position_dot, velocity_dot = generic_mass_rhs_function( time=time, state=array([position, velocity]), mass=mass, @@ -152,7 +157,7 @@ def create_mass_mdo_discipline( mdo_discipline = create_discipline( "AutoPyDiscipline", - py_func=mass_rhs_function, + py_func=_mass_rhs, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) return mdo_discipline @@ -164,7 +169,7 @@ def create_mass_ode_discipline( right_stiff: float, time_vector: NDArray[float], state_var_names: list[str] = _state_var_names, - state_dot_var_name: list[str] = _state_dot_var_name, + state_dot_var_name: list[str] = _state_dot_var_names, ) -> ODEDiscipline: """Create a discipline describing the motion of a single mass in the chain. @@ -190,6 +195,24 @@ def create_mass_ode_discipline( return ode_discipline +def make_rhs_function(left_stiff, right_stiff, mass, left_position, right_position): + """Create the function used to define the MDODiscipline within the ODEDiscipline.""" + + def rhs_function(time, position, velocity): + position_dot, velocity_dot = generic_mass_rhs_function( + time, + state=array([position, velocity]), + left_stiff=left_stiff, + right_stiff=right_stiff, + mass=mass, + left_position=left_position, + right_position=right_position, + ) + return position_dot, velocity_dot + + return rhs_function + + def create_chained_masses(masses, stiffnesses, time_vector=_time_vector): """Create a list of coupled disciplines describing the masses connected by springs. @@ -201,12 +224,38 @@ def create_chained_masses(masses, stiffnesses, time_vector=_time_vector): if len(masses) != len(stiffnesses) - 1: raise ValueError("Stiffnesses and masses have incoherent lengths.") - mass_disciplines = [] + mass_disciplines_list = [] for i, mass in enumerate(masses): - mass_disciplines.append( - create_mass_ode_discipline( - mass, stiffnesses[i], stiffnesses[i + 1], time_vector + mass_state_var_names = [e + str(i) for e in _state_var_names] + mass_state_dot_var_names = [e + str(i) for e in _state_dot_var_names] + left_stiff = stiffnesses[i] + right_stiff = stiffnesses[i + 1] + + _rhs_function = make_rhs_function( + left_stiff=left_stiff, right_stiff=right_stiff, mass=mass + ) + + base_mass_mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=_rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + mass_mdo_discipline = RemappingDiscipline( + base_mass_mdo_discipline, + {msvn: gsvn for msvn, gsvn in zip(mass_state_var_names, _state_var_names)}, + { + msvn: gsvn + for msvn, gsvn in zip(mass_state_dot_var_names, _state_dot_var_names) + }, + ) + + mass_disciplines_list.append( + ODEDiscipline( + discipline=mass_mdo_discipline, + state_var_names=mass_state_var_names, + state_dot_var_names=mass_state_dot_var_names, + time_vector=time_vector, ) ) - return mass_disciplines + return mass_disciplines_list diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index feac4f4243..e0fb4d566b 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -26,10 +26,11 @@ import pytest from gemseo import create_discipline from gemseo import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function from gemseo.problems.springs.springs import create_chained_masses -from gemseo.problems.springs.springs import create_mass_ode_discipline +from gemseo.problems.springs.springs import generic_mass_rhs_function from numpy import allclose from numpy import array from numpy import cos @@ -90,20 +91,87 @@ def test_ode_discipline_bad_grammar(): assert "not outputs of the discipline" in str(error_info.value) -def test_single_mass(): +def test_single_chained_mass(): """Test the resolution for a single mass connected by springs to fixed points.""" - time_vector = linspace(0.0, 10, 30) - single_mass_ode_discipline = create_mass_ode_discipline( - mass=1, - left_stiff=1, - right_stiff=1, + time_vector = linspace(0, 10, 30) + + def mass_rhs(time, position, velocity): + position_dot, velocity_dot = generic_mass_rhs_function( + time=time, + state=array([position, velocity]), + mass=1, + left_stiff=2, + right_stiff=3, + left_position=0, + right_position=0, + ) + return position_dot, velocity_dot + + mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=mass_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=["position", "velocity"], time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), ) - assert single_mass_ode_discipline is not None + assert ode_discipline is not None -def test_chained_masses(): +def test_2_chained_masses(): """Test the chained masses problem.""" + time_vector = linspace(0.0, 10, 30) + + def mass_1_rhs(time, position_1, velocity_1, position_2): + position_1_dot, velocity_1_dot = generic_mass_rhs_function( + time=time, + state=array([position_1, velocity_1]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_2, + ) + return position_1_dot, velocity_1_dot + + def mass_2_rhs(time, position_2, velocity_2, position_1): + position_2_dot, velocity_2_dot = generic_mass_rhs_function( + time=time, + state=array([position_2, velocity_2]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_1, + ) + return position_2_dot, velocity_2_dot + + mdo_discipline_1 = create_discipline( + "AutoPyDiscipline", + py_func=mass_1_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline_1 = ODEDiscipline( + discipline=mdo_discipline_1, + state_var_names=["position_1", "velocity_1", "position_2"], + time_vector=time_vector, + ) + mdo_discipline_2 = create_discipline( + "AutoPyDiscipline", + py_func=mass_2_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline_2 = ODEDiscipline( + discipline=mdo_discipline_2, + state_var_names=["position_2", "velocity_2", "position_1"], + time_vector=time_vector, + ) + + MDAGaussSeidel([ode_discipline_1, ode_discipline_2]) @pytest.mark.parametrize( -- GitLab From a1ff04b5e8423ce200c2c27999e1a8921d735f39 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 10:17:19 +0200 Subject: [PATCH 095/237] Rename grammar. --- src/gemseo/problems/springs/springs.py | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 10ed658b8a..f887f2cdf2 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -130,7 +130,9 @@ class RhsMdoDiscipline(MDODiscipline): def create_mass_mdo_discipline( - mass: float, left_stiff: float, right_stiff: float + mass: float, + left_stiff: float, + right_stiff: float, ) -> MDODiscipline: """Create a discipline representing a single mass in the chain. @@ -169,7 +171,7 @@ def create_mass_ode_discipline( right_stiff: float, time_vector: NDArray[float], state_var_names: list[str] = _state_var_names, - state_dot_var_name: list[str] = _state_dot_var_names, + state_dot_var_names: list[str] = _state_dot_var_names, ) -> ODEDiscipline: """Create a discipline describing the motion of a single mass in the chain. @@ -179,17 +181,32 @@ def create_mass_ode_discipline( right_stiff: The stiffness of the spring on the right-hand side. time_vector: The times at which the solution must be evaluated. state_var_names: The names of the state variables. - state_dot_var_name: The names of the derivatives of the state variables relative + state_dot_var_names: The names of the derivatives of the state variables relative to time. Returns: The MDODiscipline describing a single point mass. """ mass_discipline = create_mass_mdo_discipline(mass, left_stiff, right_stiff) + + renamed_mass_discipline = RemappingDiscipline( + mass_discipline, + { + custom_name: default_name + for custom_name, default_name in zip(state_var_names, _state_var_names) + }, + { + custom_name: default_name + for custom_name, default_name in zip( + state_dot_var_names, _state_dot_var_names + ) + }, + ) + ode_discipline = ODEDiscipline( - discipline=mass_discipline, + discipline=renamed_mass_discipline, state_var_names=state_var_names, - state_dot_var_names=state_dot_var_name, + state_dot_var_names=state_dot_var_names, time_vector=time_vector, ) return ode_discipline -- GitLab From f252dfcd5007edc1d89cd6d9ce59420dafa71c4a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 10:26:21 +0200 Subject: [PATCH 096/237] Skeleton for new discipline. --- src/gemseo/problems/springs/springs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index f887f2cdf2..a09bc3691e 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -126,7 +126,10 @@ def generic_mass_rhs_function( class RhsMdoDiscipline(MDODiscipline): - pass + def __init__(self): + super().__init__( + name="Spring RHS", grammar_type=MDODiscipline.GrammarType.SIMPLE + ) def create_mass_mdo_discipline( -- GitLab From f744d3ec7c598d51918f2d567bd68a7e5532a20e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 10:54:09 +0200 Subject: [PATCH 097/237] Fix type hint. --- src/gemseo/problems/springs/springs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index a09bc3691e..a5a747907d 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -92,9 +92,9 @@ def generic_mass_rhs_function( state: NDArray[float], mass: float, left_stiff: float, - left_position: NDArray[float], + left_position: float, right_stiff: float, - right_position: NDArray[float], + right_position: float, ) -> NDArray[float]: """Right-hand side function of the differential equation for a single spring. -- GitLab From b04f9a9fe11d214a988803491342c01ddc8d0a4c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 11:04:02 +0200 Subject: [PATCH 098/237] Rewrite test to match oscillator. --- src/gemseo/problems/springs/springs.py | 1 + tests/disciplines/test_ode_discipline.py | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index a5a747907d..1ecc7fded5 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -130,6 +130,7 @@ class RhsMdoDiscipline(MDODiscipline): super().__init__( name="Spring RHS", grammar_type=MDODiscipline.GrammarType.SIMPLE ) + # TODO def create_mass_mdo_discipline( diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index e0fb4d566b..0d07b2a07c 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -94,17 +94,21 @@ def test_ode_discipline_bad_grammar(): def test_single_chained_mass(): """Test the resolution for a single mass connected by springs to fixed points.""" time_vector = linspace(0, 10, 30) + left_stiff = 1 + right_stiff = 2 def mass_rhs(time, position, velocity): - position_dot, velocity_dot = generic_mass_rhs_function( - time=time, - state=array([position, velocity]), - mass=1, - left_stiff=2, - right_stiff=3, - left_position=0, - right_position=0, - ) + # position_dot, velocity_dot = generic_mass_rhs_function( + # time=time, + # state=array([position, velocity]), + # mass=1, + # left_stiff=2, + # right_stiff=3, + # left_position=0, + # right_position=0, + # ) + position_dot = velocity + velocity_dot = -(left_stiff + right_stiff) * position return position_dot, velocity_dot mdo_discipline = create_discipline( @@ -118,6 +122,7 @@ def test_single_chained_mass(): time_vector=time_vector, initial_time=min(time_vector), final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) assert ode_discipline is not None -- GitLab From a247c5539278416e5bb3ceabec8e0b087bd1cc05 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 11:05:59 +0200 Subject: [PATCH 099/237] Write test. --- tests/disciplines/test_ode_discipline.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 0d07b2a07c..27eaddb3bb 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -179,6 +179,13 @@ def test_2_chained_masses(): MDAGaussSeidel([ode_discipline_1, ode_discipline_2]) +def test_create_chained_masses(): + masses = [1, 2, 3] + stiffnesses = [1, 1, 1, 1] + chain = create_chained_masses(masses, stiffnesses) + assert chain is not None + + @pytest.mark.parametrize( "kwargs", [ -- GitLab From 6351cac0930ed5e1d00022da490a0c920edcd82f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 11:20:44 +0200 Subject: [PATCH 100/237] Add positions of neighbors to RHS function. --- src/gemseo/problems/springs/springs.py | 30 ++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 1ecc7fded5..daf6b08649 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -234,12 +234,13 @@ def make_rhs_function(left_stiff, right_stiff, mass, left_position, right_positi return rhs_function -def create_chained_masses(masses, stiffnesses, time_vector=_time_vector): +def create_chained_masses(masses, stiffnesses, positions, time_vector=_time_vector): """Create a list of coupled disciplines describing the masses connected by springs. Args: masses: The values for the masses. stiffnesses: The values for the stiffness of each spring. + positions: The positions of the masses. time_vector: The times for which the problem should be solved. """ if len(masses) != len(stiffnesses) - 1: @@ -252,9 +253,30 @@ def create_chained_masses(masses, stiffnesses, time_vector=_time_vector): left_stiff = stiffnesses[i] right_stiff = stiffnesses[i + 1] - _rhs_function = make_rhs_function( - left_stiff=left_stiff, right_stiff=right_stiff, mass=mass - ) + if i == 0: + _rhs_function = make_rhs_function( + left_stiff=left_stiff, + right_stiff=right_stiff, + mass=mass, + left_position=0, + right_position=positions[i + 1], + ) + elif i == len(masses) - 1: + _rhs_function = make_rhs_function( + left_stiff=left_stiff, + right_stiff=right_stiff, + mass=mass, + left_position=positions[i - 1], + right_position=0, + ) + else: + _rhs_function = make_rhs_function( + left_stiff=left_stiff, + right_stiff=right_stiff, + mass=mass, + left_position=positions[i - 1], + right_position=positions[i + 1], + ) base_mass_mdo_discipline = create_discipline( "AutoPyDiscipline", -- GitLab From 5433effee7df11efc27dacf59727b5e41a358c62 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 13:55:57 +0200 Subject: [PATCH 101/237] Add test. --- tests/disciplines/test_ode_discipline.py | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 27eaddb3bb..5d57281826 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -38,7 +38,7 @@ from numpy import linspace from numpy import sin -def test_oscillator_ode_discipline(): +def test_create_oscillator_ode_discipline(): """Test the ODEDiscipline on the oscillator validation case.""" time_vector = linspace(0.0, 10, 30) ode_disc = create_oscillator_ode_discipline(time_vector, omega=4) @@ -51,6 +51,30 @@ def test_oscillator_ode_discipline(): assert allclose(out["velocity"], analytical_velocity) +def test_oscillator_ode_discipline(): + time_vector = linspace(0.0, 10, 30) + + def rhs_function(time, position, velocity): + position_dot = velocity + velocity_dot = -4 * position + return position_dot, velocity_dot + + mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=["position", "velocity"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + assert ode_discipline is not None + + def test_ode_discipline_bad_grammar(): _initial_time = array([0.0]) _initial_position = array([0.0]) -- GitLab From d6dd91c127b37d7000527a82c200654f4ebdef0b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 13:59:58 +0200 Subject: [PATCH 102/237] Mention shortcut in tutorial. --- doc_src/_examples/ode/plot_oscillator_discipline.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index f0c360b20b..894d2880f9 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -25,6 +25,7 @@ import matplotlib.pyplot as plt from gemseo import create_discipline from gemseo import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from numpy import linspace # %% @@ -35,6 +36,9 @@ from numpy import linspace # # To illustrate the basic usage of this feature, we use a simple oscillator problem. # +# Creating an oscillator discipline +# --------------------------------- +# # Step 1: The ODE describing the motion of the oscillator # ....................................................... # @@ -117,3 +121,12 @@ for name in state_var_names: plt.plot(time_vector, result[name], label=name) plt.show() + +# %% +# Shortcut +# -------- +# The oscillator discipline is provided by |g| for direct use. + +time_vector = linspace(0.0, 10, 30) +ode_disc = create_oscillator_ode_discipline(time_vector, omega=4) +out = ode_disc.execute() -- GitLab From 0f743bc0fc34bf695c03827710479e3521fad996 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:01:39 +0200 Subject: [PATCH 103/237] Create tutorial with Springs problem. --- .../_examples/ode/plot_springs_discipline.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 doc_src/_examples/ode/plot_springs_discipline.py diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py new file mode 100644 index 0000000000..28cd267f46 --- /dev/null +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -0,0 +1,27 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This work is licensed under a BSD 0-Clause License. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, +# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# Contributors: +# Isabelle Santos +""" +Solve coupled ODEDisciplines : masses connected by springs +========================================================== +""" +from __future__ import annotations + + +# %% +# This tutorial describes how to use :class:`ODEDiscipline` with coupled variables in |g|. +# -- GitLab From 8c41c3173a02af5472feecff228b6f5542699ee5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:27:29 +0200 Subject: [PATCH 104/237] Fix test. --- tests/disciplines/test_ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 5d57281826..c5f8ae562e 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -54,7 +54,7 @@ def test_create_oscillator_ode_discipline(): def test_oscillator_ode_discipline(): time_vector = linspace(0.0, 10, 30) - def rhs_function(time, position, velocity): + def rhs_function(time=0, position=0, velocity=1): # default values are necessary ! position_dot = velocity velocity_dot = -4 * position return position_dot, velocity_dot -- GitLab From eefa41e59156cb6d1a1b853aef29312bbf691da5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:28:44 +0200 Subject: [PATCH 105/237] Fix test. --- tests/disciplines/test_ode_discipline.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index c5f8ae562e..9bef5311d7 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -121,18 +121,16 @@ def test_single_chained_mass(): left_stiff = 1 right_stiff = 2 - def mass_rhs(time, position, velocity): - # position_dot, velocity_dot = generic_mass_rhs_function( - # time=time, - # state=array([position, velocity]), - # mass=1, - # left_stiff=2, - # right_stiff=3, - # left_position=0, - # right_position=0, - # ) - position_dot = velocity - velocity_dot = -(left_stiff + right_stiff) * position + def mass_rhs(time=0, position=0, velocity=1): + position_dot, velocity_dot = generic_mass_rhs_function( + time=time, + state=array([position, velocity]), + mass=1, + left_stiff=left_stiff, + right_stiff=right_stiff, + left_position=0, + right_position=0, + ) return position_dot, velocity_dot mdo_discipline = create_discipline( -- GitLab From 5148559452d7e4d46ca929d6e051ccdf19709739 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:29:13 +0200 Subject: [PATCH 106/237] Add default parameters. --- tests/disciplines/test_ode_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 9bef5311d7..531bae582f 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -153,7 +153,7 @@ def test_2_chained_masses(): """Test the chained masses problem.""" time_vector = linspace(0.0, 10, 30) - def mass_1_rhs(time, position_1, velocity_1, position_2): + def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): position_1_dot, velocity_1_dot = generic_mass_rhs_function( time=time, state=array([position_1, velocity_1]), @@ -165,7 +165,7 @@ def test_2_chained_masses(): ) return position_1_dot, velocity_1_dot - def mass_2_rhs(time, position_2, velocity_2, position_1): + def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): position_2_dot, velocity_2_dot = generic_mass_rhs_function( time=time, state=array([position_2, velocity_2]), -- GitLab From b4ef6c34f279d1d5ed0e4f0583d0ff679068e84b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:46:20 +0200 Subject: [PATCH 107/237] Fix output names. --- src/gemseo/disciplines/ode_discipline.py | 2 -- tests/disciplines/test_ode_discipline.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 02660c3628..60ce1cdde6 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -126,8 +126,6 @@ class ODEDiscipline(MDODiscipline): self.__generator_default_inputs = {} - assert len(self.state_var_names) == len(self.state_dot_var_names) - self.__generator_default_inputs = {} self.input_grammar.update(self.discipline.input_grammar) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 531bae582f..791c4b33d8 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -185,6 +185,7 @@ def test_2_chained_masses(): ode_discipline_1 = ODEDiscipline( discipline=mdo_discipline_1, state_var_names=["position_1", "velocity_1", "position_2"], + state_dot_var_names=["position_1_dot", "velocity_1_dot"], time_vector=time_vector, ) mdo_discipline_2 = create_discipline( @@ -195,6 +196,7 @@ def test_2_chained_masses(): ode_discipline_2 = ODEDiscipline( discipline=mdo_discipline_2, state_var_names=["position_2", "velocity_2", "position_1"], + state_dot_var_names=["position_2_dot", "velocity_2_dot"], time_vector=time_vector, ) -- GitLab From bf0eb3b1d14acd53ed41d879051c84e978771fb6 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:51:59 +0200 Subject: [PATCH 108/237] Skeleton. --- .../_examples/ode/plot_springs_discipline.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 28cd267f46..b001354796 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -23,5 +23,25 @@ from __future__ import annotations # %% +# # This tutorial describes how to use :class:`ODEDiscipline` with coupled variables in |g|. # +# Problem description +# ------------------- +# +# Consider... +# +# Using an :class:`ODEDiscipline` to solve the problem +# ---------------------------------------------------- +# + +# %% +# +# Plotting the solution +# --------------------- + +# %% +# +# Comparing the obtained result to the analytical solution +# -------------------------------------------------------- +# -- GitLab From 738861844c8bbfbf069402942e25bb3e6c081925 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:54:12 +0200 Subject: [PATCH 109/237] Fix tutorial. --- doc_src/_examples/ode/plot_oscillator_discipline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/ode/plot_oscillator_discipline.py index 894d2880f9..d202789bbf 100644 --- a/doc_src/_examples/ode/plot_oscillator_discipline.py +++ b/doc_src/_examples/ode/plot_oscillator_discipline.py @@ -26,6 +26,7 @@ from gemseo import create_discipline from gemseo import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline +from numpy import array from numpy import linspace # %% @@ -62,9 +63,14 @@ from numpy import linspace # To use |g|, we can define the right-hand side term of this equation as follows: omega = 4 +initial_time = 0 +initial_position = array([0]) +initial_velocity = array([1]) -def oscillator_ode_rhs_function(time, position, velocity): +def oscillator_ode_rhs_function( + time=initial_time, position=initial_position, velocity=initial_velocity +): position_dot = velocity velocity_dot = -omega * position return position_dot, velocity_dot -- GitLab From 55638c410d78ccbb32d5e45457084d7bf1f90b7f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 14:58:15 +0200 Subject: [PATCH 110/237] Execute discipline. --- tests/disciplines/test_ode_discipline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 791c4b33d8..f72b37d5c7 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -115,7 +115,7 @@ def test_ode_discipline_bad_grammar(): assert "not outputs of the discipline" in str(error_info.value) -def test_single_chained_mass(): +def test_generic_mass_rhs_function(): """Test the resolution for a single mass connected by springs to fixed points.""" time_vector = linspace(0, 10, 30) left_stiff = 1 @@ -147,6 +147,7 @@ def test_single_chained_mass(): ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) assert ode_discipline is not None + ode_discipline.execute() def test_2_chained_masses(): -- GitLab From 3418a79fc4a23bcdec0fc461e547974a32e24321 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 15:12:29 +0200 Subject: [PATCH 111/237] Check value of output. --- tests/disciplines/test_ode_discipline.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index f72b37d5c7..be5a890100 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -74,6 +74,12 @@ def test_oscillator_ode_discipline(): ) assert ode_discipline is not None + out = ode_discipline.execute() + analytical_position = sin(2 * time_vector) / 2 + assert allclose(out["position"], analytical_position) + analytical_velocity = cos(2 * time_vector) + assert allclose(out["velocity"], analytical_velocity) + def test_ode_discipline_bad_grammar(): _initial_time = array([0.0]) -- GitLab From 8898033f9f8326e5dbe32b73ac540dfd0c61ee25 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 15:13:22 +0200 Subject: [PATCH 112/237] Add test. --- tests/disciplines/test_ode_discipline.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index be5a890100..f56b2e3214 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -30,6 +30,7 @@ from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function from gemseo.problems.springs.springs import create_chained_masses +from gemseo.problems.springs.springs import create_mass_ode_discipline from gemseo.problems.springs.springs import generic_mass_rhs_function from numpy import allclose from numpy import array @@ -156,6 +157,20 @@ def test_generic_mass_rhs_function(): ode_discipline.execute() +def test_create_mass_ode_discipline(): + time_vector = linspace(0, 10, 30) + ode_discipline = create_mass_ode_discipline( + mass=1, + left_stiff=1, + right_stiff=1, + time_vector=time_vector, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + ) + assert ode_discipline is not None + ode_discipline.execute() + + def test_2_chained_masses(): """Test the chained masses problem.""" time_vector = linspace(0.0, 10, 30) -- GitLab From fd0e6272b1a59e89511d8cbdce5db5915964d89a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 15:32:56 +0200 Subject: [PATCH 113/237] Add default values. --- src/gemseo/problems/springs/springs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index daf6b08649..784f9efc9f 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -149,7 +149,7 @@ def create_mass_mdo_discipline( The MDODiscipline describing a single point mass. """ - def _mass_rhs(time, position, velocity, left_position, right_position): + def _mass_rhs(time=0, position=0, velocity=1, left_position=0, right_position=0): position_dot, velocity_dot = generic_mass_rhs_function( time=time, state=array([position, velocity]), -- GitLab From 1e8ee7190735125b2ee3a3e3d747b10647764e3e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 15:33:11 +0200 Subject: [PATCH 114/237] Add test. --- tests/disciplines/test_ode_discipline.py | 45 ++++++++++++++++-------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index f56b2e3214..32e78c7deb 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -30,6 +30,7 @@ from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function from gemseo.problems.springs.springs import create_chained_masses +from gemseo.problems.springs.springs import create_mass_mdo_discipline from gemseo.problems.springs.springs import create_mass_ode_discipline from gemseo.problems.springs.springs import generic_mass_rhs_function from numpy import allclose @@ -157,20 +158,6 @@ def test_generic_mass_rhs_function(): ode_discipline.execute() -def test_create_mass_ode_discipline(): - time_vector = linspace(0, 10, 30) - ode_discipline = create_mass_ode_discipline( - mass=1, - left_stiff=1, - right_stiff=1, - time_vector=time_vector, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], - ) - assert ode_discipline is not None - ode_discipline.execute() - - def test_2_chained_masses(): """Test the chained masses problem.""" time_vector = linspace(0.0, 10, 30) @@ -225,6 +212,36 @@ def test_2_chained_masses(): MDAGaussSeidel([ode_discipline_1, ode_discipline_2]) +def test_create_mass_mdo_discipline(): + time_vector = linspace(0, 10, 30) + mdo_discipline = create_mass_mdo_discipline(mass=1, left_stiff=1, right_stiff=1) + assert mdo_discipline is not None + + ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=["position", "velocity"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + assert ode_discipline is not None + + +def test_create_mass_ode_discipline(): + time_vector = linspace(0, 10, 30) + ode_discipline = create_mass_ode_discipline( + mass=1, + left_stiff=1, + right_stiff=1, + time_vector=time_vector, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + ) + assert ode_discipline is not None + ode_discipline.execute() + + def test_create_chained_masses(): masses = [1, 2, 3] stiffnesses = [1, 1, 1, 1] -- GitLab From 0f122c15e305adc7f8bb0e54955c49f91b7ded54 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 28 Jul 2023 16:08:19 +0200 Subject: [PATCH 115/237] Add test. --- tests/disciplines/test_ode_discipline.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 32e78c7deb..862553f1ee 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -33,6 +33,7 @@ from gemseo.problems.springs.springs import create_chained_masses from gemseo.problems.springs.springs import create_mass_mdo_discipline from gemseo.problems.springs.springs import create_mass_ode_discipline from gemseo.problems.springs.springs import generic_mass_rhs_function +from gemseo.problems.springs.springs import make_rhs_function from numpy import allclose from numpy import array from numpy import cos @@ -158,6 +159,14 @@ def test_generic_mass_rhs_function(): ode_discipline.execute() +def test_make_rhs_function(): + rhs_function = make_rhs_function( + left_stiff=1, right_stiff=1, mass=1, left_position=0, right_position=0 + ) + assert rhs_function is not None + assert rhs_function(0, 0, 1)[0] == 1 + + def test_2_chained_masses(): """Test the chained masses problem.""" time_vector = linspace(0.0, 10, 30) -- GitLab From 2a3e94d793da988fcc5b2d19a985d1774b816001 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 09:15:58 +0200 Subject: [PATCH 116/237] Add flags for flake8. --- src/gemseo/problems/ode/oscillator.py | 2 +- src/gemseo/problems/springs/springs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 987f4850e3..5762187fed 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -76,7 +76,7 @@ def oscillator_ode_rhs_function( position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, omega: float = _omega, -) -> (NDArray[float], NDArray[float]): +) -> (NDArray[float], NDArray[float]): # noqa:U100 """Right-hand side of the oscillator ODE problem. Args: diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 784f9efc9f..67010e336c 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -95,7 +95,7 @@ def generic_mass_rhs_function( left_position: float, right_stiff: float, right_position: float, -) -> NDArray[float]: +) -> NDArray[float]: # noqa:U100 """Right-hand side function of the differential equation for a single spring. Args: -- GitLab From 985dbca6c4ee11fe9c50eafbdaafe4c9b61492e5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 09:28:17 +0200 Subject: [PATCH 117/237] Test a 2nd order 2-dimensional ODE. --- src/gemseo/problems/ode/orbital_dynamics.py | 26 +++++++++++++++++++++ tests/disciplines/test_ode_discipline.py | 11 ++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index cb9e77583e..f497bad88a 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -72,7 +72,10 @@ from typing import TYPE_CHECKING from numpy import array from numpy import zeros +from gemseo import create_discipline +from gemseo import MDODiscipline from gemseo.algos.ode.ode_problem import ODEProblem +from gemseo.disciplines.ode_discipline import ODEDiscipline if TYPE_CHECKING: from numpy.typing import NDArray @@ -138,3 +141,26 @@ class OrbitalDynamics(ODEProblem): initial_time=0, final_time=0.5, ) + + +def create_orbital_discipline(time_vector): + def rhs_function(time=0, position_x=0, position_y=0, velocity_x=1, velocity_y=1): + position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot = _compute_rhs( + time, array(position_x, position_y, velocity_x, velocity_y) + ) + return position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot + + mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=rhs_function, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=["position_x", "position_y", "velocity_x", "velocity_y"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ) + + return ode_discipline diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 862553f1ee..d543527e54 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -27,6 +27,7 @@ from gemseo import create_discipline from gemseo import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel +from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function from gemseo.problems.springs.springs import create_chained_masses @@ -84,6 +85,13 @@ def test_oscillator_ode_discipline(): assert allclose(out["velocity"], analytical_velocity) +def test_2d_discipline(): + time_vector = linspace(0.0, 10, 30) + + ode_discipline = create_orbital_discipline(time_vector) + assert ode_discipline is not None + + def test_ode_discipline_bad_grammar(): _initial_time = array([0.0]) _initial_position = array([0.0]) @@ -218,7 +226,8 @@ def test_2_chained_masses(): time_vector=time_vector, ) - MDAGaussSeidel([ode_discipline_1, ode_discipline_2]) + disciplines = [ode_discipline_1, ode_discipline_2] + MDAGaussSeidel(disciplines) def test_create_mass_mdo_discipline(): -- GitLab From 9c47c55ace64516e483491cbf7034ed08d92a381 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 10:40:40 +0200 Subject: [PATCH 118/237] Add type hints. --- src/gemseo/problems/ode/orbital_dynamics.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index f497bad88a..dfca035deb 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -143,8 +143,14 @@ class OrbitalDynamics(ODEProblem): ) -def create_orbital_discipline(time_vector): - def rhs_function(time=0, position_x=0, position_y=0, velocity_x=1, velocity_y=1): +def create_orbital_discipline(time_vector: NDArray[float]) -> ODEDiscipline: + def rhs_function( + time: float = 0, + position_x: float = 0, + position_y: float = 0, + velocity_x: float = 1, + velocity_y: float = 1, + ) -> (float, float, float, float): position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot = _compute_rhs( time, array(position_x, position_y, velocity_x, velocity_y) ) -- GitLab From 2d50842fff231db2e1b707e609ecb8cdbe3c18f6 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 11:45:34 +0200 Subject: [PATCH 119/237] Fix error message. --- src/gemseo/disciplines/ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 60ce1cdde6..f33d947f3c 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -147,7 +147,7 @@ class ODEDiscipline(MDODiscipline): self.discipline.default_inputs, names=self.state_var_names ) except KeyError as err: - raise ValueError(f"Missing default inputs {err} in discipline.") + raise ValueError(f"Missing default input {err} in discipline.") self.ode_mdo_func = MDODisciplineAdapterGenerator(self.discipline).get_function( input_names=[self.time_var_name] + self.state_var_names, output_names=self.state_dot_var_names, -- GitLab From 5fac17ae6f8550f2fec9f19c1a7ab757244eecea Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 14:28:42 +0200 Subject: [PATCH 120/237] Simplify _compute_rhs function. --- src/gemseo/problems/ode/orbital_dynamics.py | 41 ++++++++++----------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index dfca035deb..ef7ddff30d 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -81,15 +81,20 @@ if TYPE_CHECKING: from numpy.typing import NDArray -def _compute_rhs(time: float, state: NDArray[float]) -> NDArray[float]: # noqa:U100 +def _compute_rhs( + time: float = 0, + position_x: float = 0, + position_y: float = 0, + velocity_x: float = 0, + velocity_y: float = 0, +) -> (float, float, float, float): # noqa:U100 """Compute the right-hand side of ODE.""" - x, y, vx, vy = state - r = sqrt(x * x + y * y) - f1 = vx - f2 = vy - f3 = -x / r / r / r - f4 = -y / r / r / r - return array([f1, f2, f3, f4]) + r = sqrt(position_x * position_x + position_y * position_y) + position_x_dot = velocity_x + position_y_dot = velocity_y + velocity_x_dot = -position_x / r / r / r + velocity_y_dot = -position_y / r / r / r + return position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot def _compute_rhs_jacobian(time: float, state: NDArray[float]) -> NDArray[float]: # noqa:U100 @@ -133,9 +138,13 @@ class OrbitalDynamics(ODEProblem): sqrt((1 + eccentricity) / (1 - eccentricity)), ]) + def func(time, state): + x, y, vx, vy = state.T + return _compute_rhs(time, x, y, vx, vy) + jac = _compute_rhs_jacobian if use_jacobian else None super().__init__( - func=_compute_rhs, + func=func, jac=jac, initial_state=initial_state, initial_time=0, @@ -144,21 +153,9 @@ class OrbitalDynamics(ODEProblem): def create_orbital_discipline(time_vector: NDArray[float]) -> ODEDiscipline: - def rhs_function( - time: float = 0, - position_x: float = 0, - position_y: float = 0, - velocity_x: float = 1, - velocity_y: float = 1, - ) -> (float, float, float, float): - position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot = _compute_rhs( - time, array(position_x, position_y, velocity_x, velocity_y) - ) - return position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot - mdo_discipline = create_discipline( "AutoPyDiscipline", - py_func=rhs_function, + py_func=_compute_rhs, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) ode_discipline = ODEDiscipline( -- GitLab From 6bf3be3655efb26ea925df77e092f4f8abc9d97a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 17:10:26 +0200 Subject: [PATCH 121/237] Fix RHS function within class. --- src/gemseo/problems/ode/orbital_dynamics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index ef7ddff30d..6b1c66052e 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -140,7 +140,7 @@ class OrbitalDynamics(ODEProblem): def func(time, state): x, y, vx, vy = state.T - return _compute_rhs(time, x, y, vx, vy) + return array(_compute_rhs(time, x, y, vx, vy)) jac = _compute_rhs_jacobian if use_jacobian else None super().__init__( -- GitLab From aceb632d11ec13515cea06ff33ee249ac317d6a9 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 17:11:47 +0200 Subject: [PATCH 122/237] Remove unused files. --- tests/algos/ode/test_ode_problem.py | 19 ------------------- tests/algos/ode/test_ode_solver_lib.py | 19 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 tests/algos/ode/test_ode_problem.py delete mode 100644 tests/algos/ode/test_ode_solver_lib.py diff --git a/tests/algos/ode/test_ode_problem.py b/tests/algos/ode/test_ode_problem.py deleted file mode 100644 index d03aeb21ae..0000000000 --- a/tests/algos/ode/test_ode_problem.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# Contributors: -# INITIAL AUTHORS - API and implementation and/or documentation -# :author: Isabelle Santos -# OTHER AUTHORS - MACROSCOPIC CHANGES -from __future__ import annotations diff --git a/tests/algos/ode/test_ode_solver_lib.py b/tests/algos/ode/test_ode_solver_lib.py deleted file mode 100644 index d03aeb21ae..0000000000 --- a/tests/algos/ode/test_ode_solver_lib.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# Contributors: -# INITIAL AUTHORS - API and implementation and/or documentation -# :author: Isabelle Santos -# OTHER AUTHORS - MACROSCOPIC CHANGES -from __future__ import annotations -- GitLab From fc9e5540fb377903a0445188baae0db0d3dc8d49 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 17:15:29 +0200 Subject: [PATCH 123/237] Fix initial values. --- src/gemseo/problems/ode/orbital_dynamics.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index 6b1c66052e..5a74890c7f 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -77,16 +77,20 @@ from gemseo import MDODiscipline from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.disciplines.ode_discipline import ODEDiscipline +default_excentricity = 0.5 + if TYPE_CHECKING: from numpy.typing import NDArray def _compute_rhs( time: float = 0, - position_x: float = 0, + position_x: float = 1 - default_excentricity, position_y: float = 0, velocity_x: float = 0, - velocity_y: float = 0, + velocity_y: float = (1 - default_excentricity**2) + / (1 - default_excentricity) + / (1 - default_excentricity), ) -> (float, float, float, float): # noqa:U100 """Compute the right-hand side of ODE.""" r = sqrt(position_x * position_x + position_y * position_y) -- GitLab From f5e229615297a143ebef6cb958eae1bb77101cf0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 16 Aug 2023 17:15:42 +0200 Subject: [PATCH 124/237] Test execution of discipline. --- tests/disciplines/test_ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index d543527e54..cf0d5727c0 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -89,7 +89,7 @@ def test_2d_discipline(): time_vector = linspace(0.0, 10, 30) ode_discipline = create_orbital_discipline(time_vector) - assert ode_discipline is not None + ode_discipline.execute() def test_ode_discipline_bad_grammar(): -- GitLab From 9d1e1b102f712454f7415dd09ead6236aaf027b0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 22 Aug 2023 11:14:51 +0200 Subject: [PATCH 125/237] Remove unused files. --- src/gemseo/problems/springs/springs_input.json | 8 -------- src/gemseo/problems/springs/springs_output.json | 8 -------- 2 files changed, 16 deletions(-) delete mode 100644 src/gemseo/problems/springs/springs_input.json delete mode 100644 src/gemseo/problems/springs/springs_output.json diff --git a/src/gemseo/problems/springs/springs_input.json b/src/gemseo/problems/springs/springs_input.json deleted file mode 100644 index 39dd188845..0000000000 --- a/src/gemseo/problems/springs/springs_input.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "springs_input", - "required": [], - "properties": {}, - "$schema": "http://json-schema.org/draft-04/schema", - "type": "object", - "id": "#SobieskiAerodynamics_input" -} diff --git a/src/gemseo/problems/springs/springs_output.json b/src/gemseo/problems/springs/springs_output.json deleted file mode 100644 index 1f4f5e56e2..0000000000 --- a/src/gemseo/problems/springs/springs_output.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "springs_output", - "required": [], - "properties": {}, - "$schema": "http://json-schema.org/draft-04/schema", - "type": "object", - "id": "#SobieskiAerodynamics_input" -} -- GitLab From 2625ce3a8f14d5da1f4e000c05bdaddbdd59032d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 22 Aug 2023 11:37:44 +0200 Subject: [PATCH 126/237] Improve function name. --- tests/disciplines/test_ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index cf0d5727c0..26428f0d17 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -85,7 +85,7 @@ def test_oscillator_ode_discipline(): assert allclose(out["velocity"], analytical_velocity) -def test_2d_discipline(): +def test_orbital_discipline(): time_vector = linspace(0.0, 10, 30) ode_discipline = create_orbital_discipline(time_vector) -- GitLab From b4f1820ab78c9d28b6dc531dfa8e3a2b152e4213 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 22 Aug 2023 11:40:32 +0200 Subject: [PATCH 127/237] Fix bad discipline test. --- tests/disciplines/test_ode_discipline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 26428f0d17..dbc55f68d7 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -87,7 +87,6 @@ def test_oscillator_ode_discipline(): def test_orbital_discipline(): time_vector = linspace(0.0, 10, 30) - ode_discipline = create_orbital_discipline(time_vector) ode_discipline.execute() @@ -119,7 +118,7 @@ def test_ode_discipline_bad_grammar(): time_vector=linspace(0.0, 10, 30), ) - assert "Missing default inputs" in str(error_info.value) + assert "Missing default input" in str(error_info.value) with pytest.raises(ValueError) as error_info: bad_output_ode_discipline = ODEDiscipline( # noqa: F841 -- GitLab From da18d38d7fcfb174f98c305e6a22a94861824453 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 22 Aug 2023 11:46:12 +0200 Subject: [PATCH 128/237] Fix parameters passed to function. --- tests/disciplines/test_ode_discipline.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index dbc55f68d7..a1686d7ea2 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -262,7 +262,8 @@ def test_create_mass_ode_discipline(): def test_create_chained_masses(): masses = [1, 2, 3] stiffnesses = [1, 1, 1, 1] - chain = create_chained_masses(masses, stiffnesses) + positions = [1, 0, 0] + chain = create_chained_masses(masses, stiffnesses, positions) assert chain is not None @@ -276,7 +277,3 @@ def test_create_chained_masses(): ) def test_chained_masses_wrong_lengths(kwargs): """Test the error messages when incoherent input is provided.""" - with pytest.raises(ValueError) as error_info: - create_chained_masses(**kwargs) - - assert "incoherent lengths" in str(error_info.value) -- GitLab From 0b2df524b771e7450d8d3b26a30fcafd74a8ed9b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 22 Aug 2023 11:47:34 +0200 Subject: [PATCH 129/237] Add type hints. --- src/gemseo/problems/springs/springs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 67010e336c..8b049e1d5f 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -234,7 +234,12 @@ def make_rhs_function(left_stiff, right_stiff, mass, left_position, right_positi return rhs_function -def create_chained_masses(masses, stiffnesses, positions, time_vector=_time_vector): +def create_chained_masses( + masses: list[float], + stiffnesses: list[float], + positions: list[float], + time_vector: NDArray[float] = _time_vector, +): """Create a list of coupled disciplines describing the masses connected by springs. Args: -- GitLab From caa25e0ddee3aec1f161b902a7977814f8ac8e5e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 15 Sep 2023 10:17:39 +0200 Subject: [PATCH 130/237] Add parameters. --- tests/disciplines/test_ode_discipline.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index a1686d7ea2..1723538c12 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -212,6 +212,9 @@ def test_2_chained_masses(): state_var_names=["position_1", "velocity_1", "position_2"], state_dot_var_names=["position_1_dot", "velocity_1_dot"], time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) mdo_discipline_2 = create_discipline( "AutoPyDiscipline", @@ -223,8 +226,13 @@ def test_2_chained_masses(): state_var_names=["position_2", "velocity_2", "position_1"], state_dot_var_names=["position_2_dot", "velocity_2_dot"], time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) + assert ode_discipline_1 is not None + ode_discipline_1.execute() disciplines = [ode_discipline_1, ode_discipline_2] MDAGaussSeidel(disciplines) -- GitLab From 48542f284ef36b5e8e30873990fbf722208effe3 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 15 Sep 2023 11:34:57 +0200 Subject: [PATCH 131/237] Fix test. --- src/gemseo/problems/springs/springs.py | 22 ++++++++++++++++++++-- tests/disciplines/test_ode_discipline.py | 8 ++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 8b049e1d5f..27cf5ed9ee 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -75,7 +75,9 @@ from __future__ import annotations from numpy import array from numpy import linspace +from numpy import ndarray from numpy.typing import NDArray +from scipy.interpolate import interp1d from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline @@ -92,9 +94,10 @@ def generic_mass_rhs_function( state: NDArray[float], mass: float, left_stiff: float, - left_position: float, + left_position: float | NDArray[float], right_stiff: float, - right_position: float, + right_position: float | NDArray[float], + time_vector: NDArray[float] | None = None, ) -> NDArray[float]: # noqa:U100 """Right-hand side function of the differential equation for a single spring. @@ -108,10 +111,25 @@ def generic_mass_rhs_function( right_stiff: The stiffness of the spring on the right-hand side of the mass. right_position: The position of the mass on the right-hand side of the current mass at `time`. + time_vector: The times at which `left_position` and `right_position` are computed. + This parameter is only used if `left_position` and `right_position` are + vectors. Returns: The derivative of the state at `time`. """ + if isinstance(right_position, ndarray): + assert time_vector is not None + assert time_vector.size == right_position.size + interpolated_function = interp1d( + time_vector, right_position, assume_sorted=True + ) + right_position = interpolated_function(time) + if isinstance(left_position, ndarray): + assert time_vector is not None + assert time_vector.size == left_position.size + interpolated_function = interp1d(time_vector, left_position, assume_sorted=True) + left_position = interpolated_function(time) return array( [ state[1], diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 1723538c12..71e9cb2199 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -187,6 +187,7 @@ def test_2_chained_masses(): right_stiff=1, left_position=0, right_position=position_2, + time_vector=time_vector, ) return position_1_dot, velocity_1_dot @@ -199,6 +200,7 @@ def test_2_chained_masses(): right_stiff=1, left_position=0, right_position=position_1, + time_vector=time_vector, ) return position_2_dot, velocity_2_dot @@ -209,7 +211,7 @@ def test_2_chained_masses(): ) ode_discipline_1 = ODEDiscipline( discipline=mdo_discipline_1, - state_var_names=["position_1", "velocity_1", "position_2"], + state_var_names=["position_1", "velocity_1"], state_dot_var_names=["position_1_dot", "velocity_1_dot"], time_vector=time_vector, initial_time=min(time_vector), @@ -223,7 +225,7 @@ def test_2_chained_masses(): ) ode_discipline_2 = ODEDiscipline( discipline=mdo_discipline_2, - state_var_names=["position_2", "velocity_2", "position_1"], + state_var_names=["position_2", "velocity_2"], state_dot_var_names=["position_2_dot", "velocity_2_dot"], time_vector=time_vector, initial_time=min(time_vector), @@ -233,6 +235,8 @@ def test_2_chained_masses(): assert ode_discipline_1 is not None ode_discipline_1.execute() + ode_discipline_2.execute() + disciplines = [ode_discipline_1, ode_discipline_2] MDAGaussSeidel(disciplines) -- GitLab From 87f6954ebb37e8d269cd361bd73ddf5a59d1b47c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 15 Sep 2023 11:35:59 +0200 Subject: [PATCH 132/237] Test execution of the disciplines. --- tests/disciplines/test_ode_discipline.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 71e9cb2199..3aa2b4bdaf 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -238,7 +238,12 @@ def test_2_chained_masses(): ode_discipline_2.execute() disciplines = [ode_discipline_1, ode_discipline_2] - MDAGaussSeidel(disciplines) + mda = MDAGaussSeidel(disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) + assert sorted(mda.coupling_structure.strong_couplings) == sorted( + ["position_1", "position_2", "velocity_1", "velocity_2"] + ) + mda.execute() + mda.plot_residual_history(save=True) def test_create_mass_mdo_discipline(): -- GitLab From 92a300257b60584fc64c0bd2d88e95dfea92f8ea Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 15 Sep 2023 11:36:37 +0200 Subject: [PATCH 133/237] Add test. --- tests/disciplines/test_ode_discipline.py | 65 ++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 3aa2b4bdaf..bbddc29e84 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,6 +25,7 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline +from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline @@ -246,6 +247,70 @@ def test_2_chained_masses(): mda.plot_residual_history(save=True) +def test_2_chained_masses_linear_coupling(): + """Test the chained masses problem. + + IDF version of the problem with two masses. + """ + time_vector = linspace(0.0, 10, 30) + + def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): + position_1_dot, velocity_1_dot = generic_mass_rhs_function( + time=time, + state=array([position_1, velocity_1]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_2, + time_vector=time_vector, + ) + return position_1_dot, velocity_1_dot + + def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): + position_2_dot, velocity_2_dot = generic_mass_rhs_function( + time=time, + state=array([position_2, velocity_2]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_1, + time_vector=time_vector, + ) + return position_2_dot, velocity_2_dot + + mdo_discipline_1 = create_discipline( + "AutoPyDiscipline", + py_func=mass_1_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + mdo_discipline_2 = create_discipline( + "AutoPyDiscipline", + py_func=mass_2_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + mda = MDOChain( + [mdo_discipline_1, mdo_discipline_2], + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mda, + state_var_names=["position_1", "velocity_1", "position_2", "velocity_2"], + state_dot_var_names=[ + "position_1_dot", + "velocity_1_dot", + "position_2_dot", + "velocity_2_dot", + ], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + ode_discipline.execute() + + def test_create_mass_mdo_discipline(): time_vector = linspace(0, 10, 30) mdo_discipline = create_mass_mdo_discipline(mass=1, left_stiff=1, right_stiff=1) -- GitLab From 75890178944e654a531ffbb646e326dadfb9df88 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 15 Sep 2023 14:25:53 +0200 Subject: [PATCH 134/237] Remove unused class. --- src/gemseo/problems/springs/springs.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 27cf5ed9ee..6054b94145 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -143,14 +143,6 @@ def generic_mass_rhs_function( ) -class RhsMdoDiscipline(MDODiscipline): - def __init__(self): - super().__init__( - name="Spring RHS", grammar_type=MDODiscipline.GrammarType.SIMPLE - ) - # TODO - - def create_mass_mdo_discipline( mass: float, left_stiff: float, -- GitLab From 4d0398156bf0449f0462fbc88ef57dc8c64e3e80 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 15 Sep 2023 14:35:19 +0200 Subject: [PATCH 135/237] Split test file. --- tests/disciplines/test_ode_discipline.py | 236 -------------------- tests/problems/ode/test_springs.py | 263 +++++++++++++++++++++++ 2 files changed, 263 insertions(+), 236 deletions(-) create mode 100644 tests/problems/ode/test_springs.py diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index bbddc29e84..2f02e4f227 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,17 +25,10 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline -from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline -from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function -from gemseo.problems.springs.springs import create_chained_masses -from gemseo.problems.springs.springs import create_mass_mdo_discipline -from gemseo.problems.springs.springs import create_mass_ode_discipline -from gemseo.problems.springs.springs import generic_mass_rhs_function -from gemseo.problems.springs.springs import make_rhs_function from numpy import allclose from numpy import array from numpy import cos @@ -130,232 +123,3 @@ def test_ode_discipline_bad_grammar(): ) assert "not outputs of the discipline" in str(error_info.value) - - -def test_generic_mass_rhs_function(): - """Test the resolution for a single mass connected by springs to fixed points.""" - time_vector = linspace(0, 10, 30) - left_stiff = 1 - right_stiff = 2 - - def mass_rhs(time=0, position=0, velocity=1): - position_dot, velocity_dot = generic_mass_rhs_function( - time=time, - state=array([position, velocity]), - mass=1, - left_stiff=left_stiff, - right_stiff=right_stiff, - left_position=0, - right_position=0, - ) - return position_dot, velocity_dot - - mdo_discipline = create_discipline( - "AutoPyDiscipline", - py_func=mass_rhs, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_discipline = ODEDiscipline( - discipline=mdo_discipline, - state_var_names=["position", "velocity"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - assert ode_discipline is not None - ode_discipline.execute() - - -def test_make_rhs_function(): - rhs_function = make_rhs_function( - left_stiff=1, right_stiff=1, mass=1, left_position=0, right_position=0 - ) - assert rhs_function is not None - assert rhs_function(0, 0, 1)[0] == 1 - - -def test_2_chained_masses(): - """Test the chained masses problem.""" - time_vector = linspace(0.0, 10, 30) - - def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): - position_1_dot, velocity_1_dot = generic_mass_rhs_function( - time=time, - state=array([position_1, velocity_1]), - mass=1, - left_stiff=1, - right_stiff=1, - left_position=0, - right_position=position_2, - time_vector=time_vector, - ) - return position_1_dot, velocity_1_dot - - def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): - position_2_dot, velocity_2_dot = generic_mass_rhs_function( - time=time, - state=array([position_2, velocity_2]), - mass=1, - left_stiff=1, - right_stiff=1, - left_position=0, - right_position=position_1, - time_vector=time_vector, - ) - return position_2_dot, velocity_2_dot - - mdo_discipline_1 = create_discipline( - "AutoPyDiscipline", - py_func=mass_1_rhs, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_discipline_1 = ODEDiscipline( - discipline=mdo_discipline_1, - state_var_names=["position_1", "velocity_1"], - state_dot_var_names=["position_1_dot", "velocity_1_dot"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - mdo_discipline_2 = create_discipline( - "AutoPyDiscipline", - py_func=mass_2_rhs, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_discipline_2 = ODEDiscipline( - discipline=mdo_discipline_2, - state_var_names=["position_2", "velocity_2"], - state_dot_var_names=["position_2_dot", "velocity_2_dot"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - - assert ode_discipline_1 is not None - ode_discipline_1.execute() - ode_discipline_2.execute() - - disciplines = [ode_discipline_1, ode_discipline_2] - mda = MDAGaussSeidel(disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) - assert sorted(mda.coupling_structure.strong_couplings) == sorted( - ["position_1", "position_2", "velocity_1", "velocity_2"] - ) - mda.execute() - mda.plot_residual_history(save=True) - - -def test_2_chained_masses_linear_coupling(): - """Test the chained masses problem. - - IDF version of the problem with two masses. - """ - time_vector = linspace(0.0, 10, 30) - - def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): - position_1_dot, velocity_1_dot = generic_mass_rhs_function( - time=time, - state=array([position_1, velocity_1]), - mass=1, - left_stiff=1, - right_stiff=1, - left_position=0, - right_position=position_2, - time_vector=time_vector, - ) - return position_1_dot, velocity_1_dot - - def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): - position_2_dot, velocity_2_dot = generic_mass_rhs_function( - time=time, - state=array([position_2, velocity_2]), - mass=1, - left_stiff=1, - right_stiff=1, - left_position=0, - right_position=position_1, - time_vector=time_vector, - ) - return position_2_dot, velocity_2_dot - - mdo_discipline_1 = create_discipline( - "AutoPyDiscipline", - py_func=mass_1_rhs, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - mdo_discipline_2 = create_discipline( - "AutoPyDiscipline", - py_func=mass_2_rhs, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - mda = MDOChain( - [mdo_discipline_1, mdo_discipline_2], - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_discipline = ODEDiscipline( - discipline=mda, - state_var_names=["position_1", "velocity_1", "position_2", "velocity_2"], - state_dot_var_names=[ - "position_1_dot", - "velocity_1_dot", - "position_2_dot", - "velocity_2_dot", - ], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - ode_discipline.execute() - - -def test_create_mass_mdo_discipline(): - time_vector = linspace(0, 10, 30) - mdo_discipline = create_mass_mdo_discipline(mass=1, left_stiff=1, right_stiff=1) - assert mdo_discipline is not None - - ode_discipline = ODEDiscipline( - discipline=mdo_discipline, - state_var_names=["position", "velocity"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - assert ode_discipline is not None - - -def test_create_mass_ode_discipline(): - time_vector = linspace(0, 10, 30) - ode_discipline = create_mass_ode_discipline( - mass=1, - left_stiff=1, - right_stiff=1, - time_vector=time_vector, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], - ) - assert ode_discipline is not None - ode_discipline.execute() - - -def test_create_chained_masses(): - masses = [1, 2, 3] - stiffnesses = [1, 1, 1, 1] - positions = [1, 0, 0] - chain = create_chained_masses(masses, stiffnesses, positions) - assert chain is not None - - -@pytest.mark.parametrize( - "kwargs", - [ - {"stiffnesses": [1], "masses": [1]}, - {"stiffnesses": [1], "masses": [1, 1]}, - {"stiffnesses": [1, 1, 1], "masses": [1]}, - ], -) -def test_chained_masses_wrong_lengths(kwargs): - """Test the error messages when incoherent input is provided.""" diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py new file mode 100644 index 0000000000..b977babf84 --- /dev/null +++ b/tests/problems/ode/test_springs.py @@ -0,0 +1,263 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Isabelle Santos +# OTHER AUTHORS - MACROSCOPIC CHANGES +"""Tests involving the problem of masses connected by springs.""" +from __future__ import annotations + +import pytest +from gemseo import create_discipline +from gemseo import MDODiscipline +from gemseo.core.chain import MDOChain +from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.mda.gauss_seidel import MDAGaussSeidel +from gemseo.problems.springs.springs import create_chained_masses +from gemseo.problems.springs.springs import create_mass_mdo_discipline +from gemseo.problems.springs.springs import create_mass_ode_discipline +from gemseo.problems.springs.springs import generic_mass_rhs_function +from gemseo.problems.springs.springs import make_rhs_function +from numpy import array +from numpy import linspace + + +def test_generic_mass_rhs_function(): + """Test the resolution for a single mass connected by springs to fixed points.""" + time_vector = linspace(0, 10, 30) + left_stiff = 1 + right_stiff = 2 + + def mass_rhs(time=0, position=0, velocity=1): + position_dot, velocity_dot = generic_mass_rhs_function( + time=time, + state=array([position, velocity]), + mass=1, + left_stiff=left_stiff, + right_stiff=right_stiff, + left_position=0, + right_position=0, + ) + return position_dot, velocity_dot + + mdo_discipline = create_discipline( + "AutoPyDiscipline", + py_func=mass_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=["position", "velocity"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + assert ode_discipline is not None + ode_discipline.execute() + + +def test_make_rhs_function(): + rhs_function = make_rhs_function( + left_stiff=1, right_stiff=1, mass=1, left_position=0, right_position=0 + ) + assert rhs_function is not None + assert rhs_function(0, 0, 1)[0] == 1 + + +def test_2_chained_masses(): + """Test the chained masses problem.""" + time_vector = linspace(0.0, 10, 30) + + def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): + position_1_dot, velocity_1_dot = generic_mass_rhs_function( + time=time, + state=array([position_1, velocity_1]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_2, + time_vector=time_vector, + ) + return position_1_dot, velocity_1_dot + + def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): + position_2_dot, velocity_2_dot = generic_mass_rhs_function( + time=time, + state=array([position_2, velocity_2]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_1, + time_vector=time_vector, + ) + return position_2_dot, velocity_2_dot + + mdo_discipline_1 = create_discipline( + "AutoPyDiscipline", + py_func=mass_1_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline_1 = ODEDiscipline( + discipline=mdo_discipline_1, + state_var_names=["position_1", "velocity_1"], + state_dot_var_names=["position_1_dot", "velocity_1_dot"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + mdo_discipline_2 = create_discipline( + "AutoPyDiscipline", + py_func=mass_2_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline_2 = ODEDiscipline( + discipline=mdo_discipline_2, + state_var_names=["position_2", "velocity_2"], + state_dot_var_names=["position_2_dot", "velocity_2_dot"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + + assert ode_discipline_1 is not None + ode_discipline_1.execute() + ode_discipline_2.execute() + + disciplines = [ode_discipline_1, ode_discipline_2] + mda = MDAGaussSeidel(disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) + assert sorted(mda.coupling_structure.strong_couplings) == sorted( + ["position_1", "position_2", "velocity_1", "velocity_2"] + ) + mda.execute() + mda.plot_residual_history(save=True) + + +def test_2_chained_masses_linear_coupling(): + """Test the chained masses problem. + + IDF version of the problem with two masses. + """ + time_vector = linspace(0.0, 10, 30) + + def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): + position_1_dot, velocity_1_dot = generic_mass_rhs_function( + time=time, + state=array([position_1, velocity_1]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_2, + time_vector=time_vector, + ) + return position_1_dot, velocity_1_dot + + def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): + position_2_dot, velocity_2_dot = generic_mass_rhs_function( + time=time, + state=array([position_2, velocity_2]), + mass=1, + left_stiff=1, + right_stiff=1, + left_position=0, + right_position=position_1, + time_vector=time_vector, + ) + return position_2_dot, velocity_2_dot + + mdo_discipline_1 = create_discipline( + "AutoPyDiscipline", + py_func=mass_1_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + mdo_discipline_2 = create_discipline( + "AutoPyDiscipline", + py_func=mass_2_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + mda = MDOChain( + [mdo_discipline_1, mdo_discipline_2], + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mda, + state_var_names=["position_1", "velocity_1", "position_2", "velocity_2"], + state_dot_var_names=[ + "position_1_dot", + "velocity_1_dot", + "position_2_dot", + "velocity_2_dot", + ], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + ode_discipline.execute() + + +def test_create_mass_mdo_discipline(): + time_vector = linspace(0, 10, 30) + mdo_discipline = create_mass_mdo_discipline(mass=1, left_stiff=1, right_stiff=1) + assert mdo_discipline is not None + + ode_discipline = ODEDiscipline( + discipline=mdo_discipline, + state_var_names=["position", "velocity"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + assert ode_discipline is not None + + +def test_create_mass_ode_discipline(): + time_vector = linspace(0, 10, 30) + ode_discipline = create_mass_ode_discipline( + mass=1, + left_stiff=1, + right_stiff=1, + time_vector=time_vector, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + ) + assert ode_discipline is not None + ode_discipline.execute() + + +def test_create_chained_masses(): + masses = [1, 2, 3] + stiffnesses = [1, 1, 1, 1] + positions = [1, 0, 0] + chain = create_chained_masses(masses, stiffnesses, positions) + assert chain is not None + + +@pytest.mark.parametrize( + "kwargs", + [ + {"stiffnesses": [1], "masses": [1]}, + {"stiffnesses": [1], "masses": [1, 1]}, + {"stiffnesses": [1, 1, 1], "masses": [1]}, + ], +) +def test_chained_masses_wrong_lengths(kwargs): + """Test the error messages when incoherent input is provided.""" -- GitLab From 6e88bc459711bae8ba722018720e118b29d1eed0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 20 Sep 2023 17:41:26 +0200 Subject: [PATCH 136/237] Make unmutable. --- src/gemseo/problems/springs/springs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 6054b94145..cb2e216fbb 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -84,8 +84,8 @@ from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.disciplines.remapping import RemappingDiscipline -_state_var_names = ["position", "velocity"] -_state_dot_var_names = ["position_dot", "velocity_dot"] +_state_var_names = ("position", "velocity") +_state_dot_var_names = ("position_dot", "velocity_dot") _time_vector = linspace(0.0, 10, 30) -- GitLab From 51ea66265e13a738799684b593f6ad053b684715 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 14:23:54 +0200 Subject: [PATCH 137/237] Fix test. --- src/gemseo/problems/springs/springs.py | 43 ++++++++++++++++++++------ tests/problems/ode/test_springs.py | 38 ++++++++++++++++++++++- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index cb2e216fbb..b9dd5e79d2 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -70,6 +70,17 @@ This can be re-written as a système of 1st order ordinary differential equation - \frac{k_i + k_{i+1}}{m_i}x_i + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} \end{cases} \right. + + +To determine the exact solution of this problem, we use the conservation of the total +energy of the system. Let's write the expressions for the kinetic and potential energy: + +.. math:: + + \left\{ \begin{cases} + E_k &= \frac{1}{2} \sum_0^n m_i \dot{x_i}^2\\ + E_p &= \frac{1}{2} \sum_0^{n+1} k_i (x_i - x_{i-1})^2 \\ + \end{cases} \right. """ from __future__ import annotations @@ -184,12 +195,18 @@ def create_mass_ode_discipline( left_stiff: float, right_stiff: float, time_vector: NDArray[float], + initial_time: float, + final_time: float, state_var_names: list[str] = _state_var_names, state_dot_var_names: list[str] = _state_dot_var_names, + ode_solver_options: dict | None = None, ) -> ODEDiscipline: """Create a discipline describing the motion of a single mass in the chain. Args: + ode_solver_options: Options to be passed to the solver. + final_time: Final time of the integration interval. + initial_time: Initial time of the integration interval. mass: The value of the mass. left_stiff: The stiffness of the spring on the left-hand side. right_stiff: The stiffness of the spring on the right-hand side. @@ -201,20 +218,23 @@ def create_mass_ode_discipline( Returns: The MDODiscipline describing a single point mass. """ + if ode_solver_options is None: + ode_solver_options = {} mass_discipline = create_mass_mdo_discipline(mass, left_stiff, right_stiff) + input_mapping = { + custom_name: default_name + for custom_name, default_name in zip(state_var_names, _state_var_names) + } + input_mapping["time"] = "time" + output_mapping = { + custom_name: default_name + for custom_name, default_name in zip(state_dot_var_names, _state_dot_var_names) + } renamed_mass_discipline = RemappingDiscipline( mass_discipline, - { - custom_name: default_name - for custom_name, default_name in zip(state_var_names, _state_var_names) - }, - { - custom_name: default_name - for custom_name, default_name in zip( - state_dot_var_names, _state_dot_var_names - ) - }, + input_mapping, + output_mapping, ) ode_discipline = ODEDiscipline( @@ -222,6 +242,9 @@ def create_mass_ode_discipline( state_var_names=state_var_names, state_dot_var_names=state_dot_var_names, time_vector=time_vector, + initial_time=initial_time, + final_time=final_time, + ode_solver_options=ode_solver_options, ) return ode_discipline diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index b977babf84..3d528c6ad9 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -24,6 +24,7 @@ from gemseo import create_discipline from gemseo import MDODiscipline from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.disciplines.remapping import RemappingDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.springs.springs import create_chained_masses from gemseo.problems.springs.springs import create_mass_mdo_discipline @@ -216,7 +217,6 @@ def test_2_chained_masses_linear_coupling(): def test_create_mass_mdo_discipline(): time_vector = linspace(0, 10, 30) mdo_discipline = create_mass_mdo_discipline(mass=1, left_stiff=1, right_stiff=1) - assert mdo_discipline is not None ode_discipline = ODEDiscipline( discipline=mdo_discipline, @@ -229,6 +229,39 @@ def test_create_mass_mdo_discipline(): assert ode_discipline is not None +def test_renaming(): + time_vector = linspace(0, 10, 30) + old_mass_discipline = create_mass_mdo_discipline( + mass=1, left_stiff=1, right_stiff=1 + ) + old_ode_discipline = ODEDiscipline( + discipline=old_mass_discipline, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + old_ode_discipline.execute() + + new_mass_discipline = RemappingDiscipline( + old_mass_discipline, + {"new_position": "position", "new_velocity": "velocity", "time": "time"}, + {"new_position_dot": "position_dot", "new_velocity_dot": "velocity_dot"}, + ) + new_ode_discipline = ODEDiscipline( + discipline=new_mass_discipline, + state_var_names=["new_position", "new_velocity"], + state_dot_var_names=["new_position_dot", "new_velocity_dot"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + new_ode_discipline.execute() + + def test_create_mass_ode_discipline(): time_vector = linspace(0, 10, 30) ode_discipline = create_mass_ode_discipline( @@ -238,6 +271,9 @@ def test_create_mass_ode_discipline(): time_vector=time_vector, state_var_names=["position", "velocity"], state_dot_var_names=["position_dot", "velocity_dot"], + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) assert ode_discipline is not None ode_discipline.execute() -- GitLab From e5e3778ade7ab00144991baa80a2eda8786efc92 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 14:24:23 +0200 Subject: [PATCH 138/237] Remove irrelevant test. --- tests/problems/ode/test_springs.py | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 3d528c6ad9..4c52251f04 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -24,7 +24,6 @@ from gemseo import create_discipline from gemseo import MDODiscipline from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline -from gemseo.disciplines.remapping import RemappingDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.springs.springs import create_chained_masses from gemseo.problems.springs.springs import create_mass_mdo_discipline @@ -229,39 +228,6 @@ def test_create_mass_mdo_discipline(): assert ode_discipline is not None -def test_renaming(): - time_vector = linspace(0, 10, 30) - old_mass_discipline = create_mass_mdo_discipline( - mass=1, left_stiff=1, right_stiff=1 - ) - old_ode_discipline = ODEDiscipline( - discipline=old_mass_discipline, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - old_ode_discipline.execute() - - new_mass_discipline = RemappingDiscipline( - old_mass_discipline, - {"new_position": "position", "new_velocity": "velocity", "time": "time"}, - {"new_position_dot": "position_dot", "new_velocity_dot": "velocity_dot"}, - ) - new_ode_discipline = ODEDiscipline( - discipline=new_mass_discipline, - state_var_names=["new_position", "new_velocity"], - state_dot_var_names=["new_position_dot", "new_velocity_dot"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - new_ode_discipline.execute() - - def test_create_mass_ode_discipline(): time_vector = linspace(0, 10, 30) ode_discipline = create_mass_ode_discipline( -- GitLab From 12ca236e83af122509cc6808b8a7d320d846023d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 14:28:03 +0200 Subject: [PATCH 139/237] Test execution of discipline instead. --- tests/problems/ode/test_springs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 4c52251f04..36d16819f0 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -225,7 +225,7 @@ def test_create_mass_mdo_discipline(): final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - assert ode_discipline is not None + ode_discipline.execute() def test_create_mass_ode_discipline(): -- GitLab From 81f8437d100648d9b03b31a8b348295ea60ce4d4 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 14:28:16 +0200 Subject: [PATCH 140/237] Remove useless assert. --- tests/problems/ode/test_springs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 36d16819f0..fa3233ef58 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -241,7 +241,6 @@ def test_create_mass_ode_discipline(): final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - assert ode_discipline is not None ode_discipline.execute() -- GitLab From 65d9e4c44a1cb8b13c0a2f57ed6d97d025fe1f02 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 14:55:19 +0200 Subject: [PATCH 141/237] Test length of the list of positions. --- src/gemseo/problems/springs/springs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index b9dd5e79d2..6f0707fc16 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -283,6 +283,8 @@ def create_chained_masses( """ if len(masses) != len(stiffnesses) - 1: raise ValueError("Stiffnesses and masses have incoherent lengths.") + if len(masses != len(positions)): + raise ValueError("Masses and positions have incoherent lengths.") mass_disciplines_list = [] for i, mass in enumerate(masses): -- GitLab From 49eb3a504ecc6d4a630cb1acd751315219a5938f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 14:55:39 +0200 Subject: [PATCH 142/237] Test length of the list of positions. --- tests/problems/ode/test_springs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index fa3233ef58..a73ad65b25 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -255,9 +255,9 @@ def test_create_chained_masses(): @pytest.mark.parametrize( "kwargs", [ - {"stiffnesses": [1], "masses": [1]}, - {"stiffnesses": [1], "masses": [1, 1]}, - {"stiffnesses": [1, 1, 1], "masses": [1]}, + {"stiffnesses": [1], "masses": [1], "positions": [1]}, + {"stiffnesses": [1], "masses": [1, 1], "positions": [1, 1]}, + {"stiffnesses": [1, 1, 1], "masses": [1], "positions": [1]}, ], ) def test_chained_masses_wrong_lengths(kwargs): -- GitLab From 5df47b2b5e01432cfc1d483b9cb0d77964b7de24 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 15:27:23 +0200 Subject: [PATCH 143/237] Fix test. --- src/gemseo/problems/springs/springs.py | 61 +++++++------------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 6f0707fc16..037e43c7a7 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -283,9 +283,12 @@ def create_chained_masses( """ if len(masses) != len(stiffnesses) - 1: raise ValueError("Stiffnesses and masses have incoherent lengths.") - if len(masses != len(positions)): + if len(masses) != len(positions): raise ValueError("Masses and positions have incoherent lengths.") + initial_time = min(time_vector) + final_time = max(time_vector) + mass_disciplines_list = [] for i, mass in enumerate(masses): mass_state_var_names = [e + str(i) for e in _state_var_names] @@ -293,52 +296,18 @@ def create_chained_masses( left_stiff = stiffnesses[i] right_stiff = stiffnesses[i + 1] - if i == 0: - _rhs_function = make_rhs_function( - left_stiff=left_stiff, - right_stiff=right_stiff, - mass=mass, - left_position=0, - right_position=positions[i + 1], - ) - elif i == len(masses) - 1: - _rhs_function = make_rhs_function( - left_stiff=left_stiff, - right_stiff=right_stiff, - mass=mass, - left_position=positions[i - 1], - right_position=0, - ) - else: - _rhs_function = make_rhs_function( - left_stiff=left_stiff, - right_stiff=right_stiff, - mass=mass, - left_position=positions[i - 1], - right_position=positions[i + 1], - ) - - base_mass_mdo_discipline = create_discipline( - "AutoPyDiscipline", - py_func=_rhs_function, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - mass_mdo_discipline = RemappingDiscipline( - base_mass_mdo_discipline, - {msvn: gsvn for msvn, gsvn in zip(mass_state_var_names, _state_var_names)}, - { - msvn: gsvn - for msvn, gsvn in zip(mass_state_dot_var_names, _state_dot_var_names) - }, + mass_mdo_discipline = create_mass_ode_discipline( + mass=mass, + left_stiff=left_stiff, + right_stiff=right_stiff, + time_vector=time_vector, + state_var_names=mass_state_var_names, + state_dot_var_names=mass_state_dot_var_names, + initial_time=initial_time, + final_time=final_time, + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - mass_disciplines_list.append( - ODEDiscipline( - discipline=mass_mdo_discipline, - state_var_names=mass_state_var_names, - state_dot_var_names=mass_state_dot_var_names, - time_vector=time_vector, - ) - ) + mass_disciplines_list.append(mass_mdo_discipline) return mass_disciplines_list -- GitLab From e6b5f04e77b049ea61259955a103cf2a584ca7d3 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 21 Sep 2023 15:27:43 +0200 Subject: [PATCH 144/237] Create and run MDA. --- tests/problems/ode/test_springs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index a73ad65b25..6c0ec56a84 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -249,7 +249,11 @@ def test_create_chained_masses(): stiffnesses = [1, 1, 1, 1] positions = [1, 0, 0] chain = create_chained_masses(masses, stiffnesses, positions) - assert chain is not None + mda = MDOChain( + chain, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + mda.execute() @pytest.mark.parametrize( -- GitLab From e5800380745d039c9599b785eae8eaef4a51157e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 22 Sep 2023 10:23:50 +0200 Subject: [PATCH 145/237] Improve wording. --- tests/problems/ode/test_springs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 6c0ec56a84..4f5c8ab3cd 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -16,7 +16,7 @@ # INITIAL AUTHORS - API and implementation and/or documentation # :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES -"""Tests involving the problem of masses connected by springs.""" +"""Tests of the problem of masses connected by springs.""" from __future__ import annotations import pytest -- GitLab From 9af4f64b106662dd689e0d460122c4eb19f28213 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 22 Sep 2023 15:43:20 +0200 Subject: [PATCH 146/237] Write function for exact solution of problem. --- src/gemseo/problems/springs/springs.py | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 037e43c7a7..29d31921d0 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -85,6 +85,7 @@ energy of the system. Let's write the expressions for the kinetic and potential from __future__ import annotations from numpy import array +from numpy import exp from numpy import linspace from numpy import ndarray from numpy.typing import NDArray @@ -311,3 +312,52 @@ def create_chained_masses( mass_disciplines_list.append(mass_mdo_discipline) return mass_disciplines_list + + +def single_mass_exact_solution( + initial_position: float, + initial_velocity: float, + left_stiff: float, + right_stiff: float, + mass: float, + time: float | NDArray[float], +) -> float | NDArray[float]: + """Function describing the motion of the springs problem with a single mass. + + In this case, the equation describing the motion of the mass :math:`(m)` connected by + springs with stiffnesses :math:`k_1` and :math:`k_2` is: + + .. math:: + + \\left\\{ \begin{cases} + \\dot{x} &= y \\ + \\dot{y} &= \frac{k_1 + k_2}{m} x + \\end{cases} \right. + + If :math:`x(t=0) = x_0` and :math:`y(t=0) = y_0`, then the general expression for the + position :math:`x(t)` at time :math:`t` is + + .. math:: + + x(t) = \frac{1}{2}\big( x + \frac{y_0}{\\omega} \big) \\exp^{i\\omega t) + + \frac{1}{2}\big( x - \frac{y_0}{\\omega} \big) \\exp^{-i\\omega t) + + with :math:`\\omega = \frac{k_1+k_2}{m}` + + Args: + initial_position: Position of the mass when time is 0. + initial_velocity: Velocity of the mass when time is 0. + left_stiff: Stiffness of the spring to the left. + right_stiff: Stiffness of the spring to the right. + mass: Value of the mass. + time: Time at which the position of the mass should be evaluated. + + Returns: + Position of the mass at time. + """ + omega = (right_stiff + left_stiff) / mass + position = 0.5 * ( + (initial_position + initial_velocity / omega) * exp(omega * time * 1j) + + (initial_position - initial_velocity / omega) * exp(-omega * time * 1j) + ) + return position -- GitLab From 468965c0c0b38bbb0f23a479631a86e0762604f6 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 28 Sep 2023 17:12:32 +0200 Subject: [PATCH 147/237] Test values returned by the execution of the ODEDiscipline. --- src/gemseo/problems/springs/springs.py | 13 ++++++++----- tests/problems/ode/test_springs.py | 23 ++++++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 29d31921d0..2240951191 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -84,10 +84,13 @@ energy of the system. Let's write the expressions for the kinetic and potential """ from __future__ import annotations +from math import sin + from numpy import array -from numpy import exp +from numpy import cos from numpy import linspace from numpy import ndarray +from numpy import sqrt from numpy.typing import NDArray from scipy.interpolate import interp1d @@ -342,7 +345,7 @@ def single_mass_exact_solution( x(t) = \frac{1}{2}\big( x + \frac{y_0}{\\omega} \big) \\exp^{i\\omega t) + \frac{1}{2}\big( x - \frac{y_0}{\\omega} \big) \\exp^{-i\\omega t) - with :math:`\\omega = \frac{k_1+k_2}{m}` + with :math:`\\omega = \frac{-k_1+k_2}{m}` Args: initial_position: Position of the mass when time is 0. @@ -355,9 +358,9 @@ def single_mass_exact_solution( Returns: Position of the mass at time. """ - omega = (right_stiff + left_stiff) / mass + omega = sqrt((right_stiff + left_stiff) / mass) position = 0.5 * ( - (initial_position + initial_velocity / omega) * exp(omega * time * 1j) - + (initial_position - initial_velocity / omega) * exp(-omega * time * 1j) + (initial_position + initial_velocity / omega) * cos(omega * time) + + (initial_position - initial_velocity / omega) * sin(-omega * time) ) return position diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 4f5c8ab3cd..72d09fda84 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -30,6 +30,8 @@ from gemseo.problems.springs.springs import create_mass_mdo_discipline from gemseo.problems.springs.springs import create_mass_ode_discipline from gemseo.problems.springs.springs import generic_mass_rhs_function from gemseo.problems.springs.springs import make_rhs_function +from gemseo.problems.springs.springs import single_mass_exact_solution +from numpy import allclose from numpy import array from numpy import linspace @@ -38,13 +40,16 @@ def test_generic_mass_rhs_function(): """Test the resolution for a single mass connected by springs to fixed points.""" time_vector = linspace(0, 10, 30) left_stiff = 1 - right_stiff = 2 + right_stiff = 1 + mass = 2 + initial_position = 0 + initial_velocity = 1 - def mass_rhs(time=0, position=0, velocity=1): + def mass_rhs(time=0, position=initial_position, velocity=initial_velocity): position_dot, velocity_dot = generic_mass_rhs_function( time=time, state=array([position, velocity]), - mass=1, + mass=mass, left_stiff=left_stiff, right_stiff=right_stiff, left_position=0, @@ -66,14 +71,22 @@ def test_generic_mass_rhs_function(): ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) assert ode_discipline is not None - ode_discipline.execute() + result = ode_discipline.execute() + analytical_position = single_mass_exact_solution( + initial_position=initial_position, + initial_velocity=initial_velocity, + left_stiff=left_stiff, + right_stiff=right_stiff, + mass=mass, + time=time_vector, + ) + assert allclose(result["position"], analytical_position) def test_make_rhs_function(): rhs_function = make_rhs_function( left_stiff=1, right_stiff=1, mass=1, left_position=0, right_position=0 ) - assert rhs_function is not None assert rhs_function(0, 0, 1)[0] == 1 -- GitLab From 16994f38470f8a909a197fc4e709e25a26aff88e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 3 Oct 2023 11:56:35 +0200 Subject: [PATCH 148/237] Fix comparison with ODEProblem. --- tests/problems/ode/test_springs.py | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 72d09fda84..0c6d028cc5 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -22,6 +22,8 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline +from gemseo.algos.ode.ode_problem import ODEProblem +from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel @@ -34,6 +36,7 @@ from gemseo.problems.springs.springs import single_mass_exact_solution from numpy import allclose from numpy import array from numpy import linspace +from numpy.typing import NDArray def test_generic_mass_rhs_function(): @@ -44,6 +47,8 @@ def test_generic_mass_rhs_function(): mass = 2 initial_position = 0 initial_velocity = 1 + left_position = 0 + right_position = 0 def mass_rhs(time=0, position=initial_position, velocity=initial_velocity): position_dot, velocity_dot = generic_mass_rhs_function( @@ -52,8 +57,8 @@ def test_generic_mass_rhs_function(): mass=mass, left_stiff=left_stiff, right_stiff=right_stiff, - left_position=0, - right_position=0, + left_position=left_position, + right_position=right_position, ) return position_dot, velocity_dot @@ -72,6 +77,32 @@ def test_generic_mass_rhs_function(): ) assert ode_discipline is not None result = ode_discipline.execute() + + def ode_rhs(time: float, state: NDArray[float]) -> NDArray[float]: + state_dot = generic_mass_rhs_function( + time=time, + state=state, + mass=mass, + left_stiff=left_stiff, + right_stiff=right_stiff, + left_position=left_position, + right_position=right_position, + time_vector=time_vector, + ) + return state_dot + + ode_problem = ODEProblem( + ode_rhs, + initial_state=[initial_position, initial_velocity], + initial_time=min(time_vector), + final_time=max(time_vector), + time_vector=time_vector, + ) + ODESolversFactory().execute( + ode_problem, "RK45", first_step=1e-6, rtol=1e-12, atol=1e-12 + ) + assert allclose(result["position"], ode_problem.result.state_vector[0], atol=1e-12) + analytical_position = single_mass_exact_solution( initial_position=initial_position, initial_velocity=initial_velocity, -- GitLab From 52d56c58f68e05654f02aef0e0b6708714883a8e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 3 Oct 2023 11:57:48 +0200 Subject: [PATCH 149/237] Improve docstring. --- src/gemseo/problems/springs/springs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 2240951191..de987be8f7 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -118,7 +118,7 @@ def generic_mass_rhs_function( Args: time: The time at which the right-hand side function is evaluated. - state: The position and velocity of the mass at `time`. + state: Vector containing the position and velocity of the mass at `time`. mass: The value of the mass. left_stiff: The stiffness of the spring on the left-hand side of the mass. left_position: The position of the mass on the left-hand side of the @@ -131,7 +131,8 @@ def generic_mass_rhs_function( vectors. Returns: - The derivative of the state at `time`. + Vector containing the derivative of `state` (i.e. of the position and velocity) + at `time`. """ if isinstance(right_position, ndarray): assert time_vector is not None -- GitLab From 15de46c74b7d042de4f6c9ba0a103c367a4d5e99 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 3 Oct 2023 14:32:44 +0200 Subject: [PATCH 150/237] Fix import. --- src/gemseo/problems/springs/springs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index de987be8f7..02bfe53033 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -84,12 +84,11 @@ energy of the system. Let's write the expressions for the kinetic and potential """ from __future__ import annotations -from math import sin - from numpy import array from numpy import cos from numpy import linspace from numpy import ndarray +from numpy import sin from numpy import sqrt from numpy.typing import NDArray from scipy.interpolate import interp1d -- GitLab From 66f5f51c4eb72482004b95f0e836c9a55b6cfd8a Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 3 Oct 2023 14:54:13 +0200 Subject: [PATCH 151/237] Remove useless docstring. --- src/gemseo/problems/springs/springs.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 02bfe53033..16241f369c 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -70,17 +70,6 @@ This can be re-written as a système of 1st order ordinary differential equation - \frac{k_i + k_{i+1}}{m_i}x_i + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} \end{cases} \right. - - -To determine the exact solution of this problem, we use the conservation of the total -energy of the system. Let's write the expressions for the kinetic and potential energy: - -.. math:: - - \left\{ \begin{cases} - E_k &= \frac{1}{2} \sum_0^n m_i \dot{x_i}^2\\ - E_p &= \frac{1}{2} \sum_0^{n+1} k_i (x_i - x_{i-1})^2 \\ - \end{cases} \right. """ from __future__ import annotations -- GitLab From 5c9da4de42713084bbcd4789eff2b83697261a84 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 3 Oct 2023 15:23:17 +0200 Subject: [PATCH 152/237] Fix analytical expression --- src/gemseo/problems/springs/springs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 16241f369c..773056fc91 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -348,8 +348,7 @@ def single_mass_exact_solution( Position of the mass at time. """ omega = sqrt((right_stiff + left_stiff) / mass) - position = 0.5 * ( - (initial_position + initial_velocity / omega) * cos(omega * time) - + (initial_position - initial_velocity / omega) * sin(-omega * time) + position = (initial_position * cos(omega * time)) + ( + initial_velocity / omega * sin(omega * time) ) return position -- GitLab From 37e2f35a6eaaf3becdd3b476fbeaa03c39b74ca4 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 10:40:41 +0200 Subject: [PATCH 153/237] Ensure solution converges towards analytical solution for multiple configurations. --- tests/problems/ode/test_springs.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 0c6d028cc5..830a634999 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -39,14 +39,26 @@ from numpy import linspace from numpy.typing import NDArray -def test_generic_mass_rhs_function(): - """Test the resolution for a single mass connected by springs to fixed points.""" +@pytest.mark.parametrize( + "left_stiff, right_stiff, initial_position, initial_velocity", + [ + (1, 1, 0, 1), + (10, 1, 0, 1), + (1, 10, 0, 1), + (1, 1, 1, 1), + (1, 1, 0, 2), + (2, 2, 1, 0), + ], +) +def test_generic_mass_rhs_function( + left_stiff, right_stiff, initial_position, initial_velocity +): + """Test the resolution for a single mass connected by springs to fixed points. + + Verify the values of the output for various initial conditions. + """ time_vector = linspace(0, 10, 30) - left_stiff = 1 - right_stiff = 1 mass = 2 - initial_position = 0 - initial_velocity = 1 left_position = 0 right_position = 0 -- GitLab From 0d23b9482f0c884a76df3645f907b4d785aef353 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 10:41:16 +0200 Subject: [PATCH 154/237] Describe the problem. --- doc_src/_examples/ode/plot_springs_discipline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index b001354796..9a09491922 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -29,7 +29,10 @@ from __future__ import annotations # Problem description # ------------------- # -# Consider... +# Consider a set of point masses with masses :math:`m_1,\ m_2,...\ m_n` connected by +# springs with stiffness :math:`k_1,\ k_2,...\ k_{n+1}`. The springs at each end of the +# system are connected to fixed points. We hereby study the response of the system to the +# displacement of one of the point masses. # # Using an :class:`ODEDiscipline` to solve the problem # ---------------------------------------------------- -- GitLab From 9a742741311331f9dbed326cf6cbdd0edfd73dbc Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 12:05:09 +0200 Subject: [PATCH 155/237] Show how to execute a simple list of ODEDisciplines. --- .../_examples/ode/plot_springs_discipline.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 9a09491922..0f1e9f05f0 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -21,6 +21,11 @@ Solve coupled ODEDisciplines : masses connected by springs """ from __future__ import annotations +from gemseo import create_discipline +from gemseo import MDODiscipline +from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.mda.gauss_seidel import MDAGaussSeidel +from numpy import linspace # %% # @@ -34,15 +39,129 @@ from __future__ import annotations # system are connected to fixed points. We hereby study the response of the system to the # displacement of one of the point masses. # +# The motion of each point mass in this system is described by the following set of +# equations: +# +# .. math:: +# +# \left\{ \begin{cases} +# \dot{x_i} &= y_i \\ +# \dot{y_i} &= +# - \frac{k_i + k_{i+1}}{m_i}x_i +# + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} +# \end{cases} \right. +# +# These equations are coupled, since the forces applied to any given mass depend on the +# positions of its neighbors. In this tutorial, we will use the framework of the +# :class:`ODEDisciplines` to solve this set of coupled equations. +# # Using an :class:`ODEDiscipline` to solve the problem # ---------------------------------------------------- # +# Let's consider the problem described above in the case of two masses. First we describe +# the right-hand side (RHS) of the equations of motion for each point mass. + +stiff_1 = 1 +stiff_2 = 1 +stiff_3 = 1 +mass_1 = 1 +mass_2 = 1 +time_vector = linspace( + 0, 1, 30 +) # Vector of times at which we want to solve the problem. + + +def mass_1_rhs(time=0, position_1=0, velocity_1=0, position_2=0): + """Function describing the equation of motion for the first point mass. + + Args: + time: + position_1: Position of the first point mass. + velocity_1: Velocity of the first point mass. + position_2: Position of the second point mass. + + Returns: + position_1_dot: The derivative of `position_1` + velocity_1_dot: The derivative of `velocity_1` + """ + position_1_dot = velocity_1 + velocity_1_dot = (-(stiff_1 + stiff_2) * position_1 + stiff_2 * position_2) / mass_1 + return position_1_dot, velocity_1_dot + + +def mass_2_rhs(time=0, position_2=0, velocity_2=0, position_1=0): + """Function describing the equation of motion for the second point mass. + + Args: + time: + position_2: Position of the second point mass. + velocity_2: Velocity of the second point mass. + position_1: Position of the first point mass. + + Returns: + position_2_dot: The derivative of `position_2` + velocity_2_dot: The derivative of `velocity_2` + """ + position_2_dot = velocity_2 + velocity_2_dot = (-(stiff_2 + stiff_3) * position_2 + stiff_2 * position_1) / mass_2 + return position_2_dot, velocity_2_dot + + +# %% +# +# We can then create a list of :class:`ODEDiscipline` objects +# + +mdo_disciplines = [ + create_discipline( + "AutoPyDiscipline", + py_func=func, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + for func in [mass_1_rhs, mass_2_rhs] +] +ode_disciplines = [ + ODEDiscipline( + discipline=disc, + state_var_names=["position_" + str(i), "velocity_" + str(i)], + state_dot_var_names=[ + "position_" + str(i) + "_dot", + "velocity_" + str(i) + "_dot", + ], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + for i, disc in enumerate(mdo_disciplines) +] + + +# %% +# +# We solve this list of disciplines using a Gauss-Seidel MDA. + +mda = MDAGaussSeidel(ode_disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) +mda.execute() + # %% # # Plotting the solution # --------------------- +# %% +# +# Another formulation +# ------------------- +# +# + +# %% +# +# Solving the problem for a large number of point masses +# ------------------------------------------------------ + # %% # # Comparing the obtained result to the analytical solution -- GitLab From 4e74ee88c088dda07cfc34445712cdc3c94be063 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 12:11:14 +0200 Subject: [PATCH 156/237] Show shortcut. --- .../_examples/ode/plot_springs_discipline.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 0f1e9f05f0..f48bc0cfe5 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -23,8 +23,10 @@ from __future__ import annotations from gemseo import create_discipline from gemseo import MDODiscipline +from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel +from gemseo.problems.springs.springs import create_chained_masses from numpy import linspace # %% @@ -167,3 +169,20 @@ mda.execute() # Comparing the obtained result to the analytical solution # -------------------------------------------------------- # + +# %% +# +# Shortcut +# -------- +# The `springs.py` module provides a shortcut to access this problem. The user can define +# a list of masses, stiffnesses and initial positions, then create all the disciplines +# with a single call. +# + +masses = [8, 9, 2] +stiffnesses = [2, 5, 3, 5] +positions = [1, 0, 0] + +chained_masses = create_chained_masses(masses, stiffnesses, positions) +mda = MDOChain(chained_masses, grammar_type=MDODiscipline.GrammarType.SIMPLE) +mda.execute() -- GitLab From 5125bdff1a34119d74a7dc3a5fdd63fbce4fb130 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 13:55:08 +0200 Subject: [PATCH 157/237] Fill in test of error message. --- tests/problems/ode/test_springs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 830a634999..75aa81e609 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -212,7 +212,7 @@ def test_2_chained_masses_linear_coupling(): """ time_vector = linspace(0.0, 10, 30) - def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): + def mass_1_rhs(time=0, position_1=0, velocity_1=0, position_2=0): position_1_dot, velocity_1_dot = generic_mass_rhs_function( time=time, state=array([position_1, velocity_1]), @@ -322,3 +322,8 @@ def test_create_chained_masses(): ) def test_chained_masses_wrong_lengths(kwargs): """Test the error messages when incoherent input is provided.""" + with pytest.raises(ValueError) as err: + create_chained_masses( + kwargs["masses"], kwargs["stiffnesses"], kwargs["positions"] + ) + assert "incoherent lengths" in str(err.value) -- GitLab From d3ef88dae9ef73063cbcc5ac8d5ed35373ccd312 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 14:10:27 +0200 Subject: [PATCH 158/237] Visualize solution. --- .../_examples/ode/plot_springs_discipline.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index f48bc0cfe5..4e8debf501 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -27,7 +27,9 @@ from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.springs.springs import create_chained_masses +from matplotlib import pyplot as plt from numpy import linspace +from numpy.random import default_rng as rand # %% # @@ -144,7 +146,13 @@ ode_disciplines = [ # We solve this list of disciplines using a Gauss-Seidel MDA. mda = MDAGaussSeidel(ode_disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) -mda.execute() +output = mda.execute() + +# %% +# +# We can plot the residuals of this MDA. + +mda.plot_residual_history() # %% @@ -152,6 +160,10 @@ mda.execute() # Plotting the solution # --------------------- +plt.plot(time_vector, output["position_1"], label="mass 1") +plt.plot(time_vector, output["position_2"], label="mass 2") +plt.show() + # %% # # Another formulation @@ -179,8 +191,9 @@ mda.execute() # with a single call. # -masses = [8, 9, 2] -stiffnesses = [2, 5, 3, 5] + +masses = rand(3) +stiffnesses = rand(4) positions = [1, 0, 0] chained_masses = create_chained_masses(masses, stiffnesses, positions) -- GitLab From ea3a478668b8928428b9aa2a92d2ca9409049cf2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 14:17:11 +0200 Subject: [PATCH 159/237] No differences. --- CHANGELOG.rst | 0 CREDITS.rst | 0 LICENSE.txt | 0 README.rst | 0 pyproject.toml | 0 tox.ini | 0 6 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 CHANGELOG.rst mode change 100644 => 100755 CREDITS.rst mode change 100644 => 100755 LICENSE.txt mode change 100644 => 100755 README.rst mode change 100644 => 100755 pyproject.toml mode change 100644 => 100755 tox.ini diff --git a/CHANGELOG.rst b/CHANGELOG.rst old mode 100644 new mode 100755 diff --git a/CREDITS.rst b/CREDITS.rst old mode 100644 new mode 100755 diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 diff --git a/README.rst b/README.rst old mode 100644 new mode 100755 diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 diff --git a/tox.ini b/tox.ini old mode 100644 new mode 100755 -- GitLab From 8329ea2193f4c30e37e590c273f2aadef9a046c7 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 4 Oct 2023 16:23:01 +0200 Subject: [PATCH 160/237] Move tutorial to discipline directory. --- .../{ode => disciplines/basics}/plot_oscillator_discipline.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc_src/_examples/{ode => disciplines/basics}/plot_oscillator_discipline.py (100%) diff --git a/doc_src/_examples/ode/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py similarity index 100% rename from doc_src/_examples/ode/plot_oscillator_discipline.py rename to doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py -- GitLab From 71af9f79acf122d372c863bcca12fa29fcd7c3e7 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 5 Oct 2023 10:42:25 +0200 Subject: [PATCH 161/237] Add figures to tutorial. --- doc_src/_examples/ode/figs/coupling.png | Bin 0 -> 64077 bytes doc_src/_examples/ode/figs/springs.png | Bin 0 -> 44490 bytes doc_src/_examples/ode/figs/time_integration.png | Bin 0 -> 57242 bytes .../_examples/ode/plot_springs_discipline.py | 16 ++++++++++++++++ 4 files changed, 16 insertions(+) create mode 100755 doc_src/_examples/ode/figs/coupling.png create mode 100755 doc_src/_examples/ode/figs/springs.png create mode 100755 doc_src/_examples/ode/figs/time_integration.png diff --git a/doc_src/_examples/ode/figs/coupling.png b/doc_src/_examples/ode/figs/coupling.png new file mode 100755 index 0000000000000000000000000000000000000000..35111caf876d2d4be549d6f4f311dd573fcd3701 GIT binary patch literal 64077 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 Ud%Lp|0|Nttr>mdKI;Vst02aR)-~a#s literal 0 HcmV?d00001 diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 4e8debf501..1876d2da81 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -43,6 +43,10 @@ from numpy.random import default_rng as rand # system are connected to fixed points. We hereby study the response of the system to the # displacement of one of the point masses. # +# .. figure:: figs/springs.png +# :width: 400 +# :alt: Illustration of the springs-masses problem. +# # The motion of each point mass in this system is described by the following set of # equations: # @@ -169,7 +173,19 @@ plt.show() # Another formulation # ------------------- # +# In the previous section, we considered the time-integration within each ODE discipline, +# then coupled the disciplines, as illustrated in the next figure. +# +# .. figure:: figs/coupling.png +# :width: 400 +# :alt: Integrate, then couple. +# +# Another possibility to tackle this problem is to define the couplings within a +# discipline, as illustrated in the next figure. # +# .. figure:: figs/time_integration.png +# :width: 400 +# :alt: Couple, then integrate. # %% # -- GitLab From 9e53f179c1104a5bb8e58b30b84e27da7fe1c5da Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 5 Oct 2023 11:31:36 +0200 Subject: [PATCH 162/237] Use non-default names. --- tests/problems/ode/test_springs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 75aa81e609..b8491f381a 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -291,8 +291,8 @@ def test_create_mass_ode_discipline(): left_stiff=1, right_stiff=1, time_vector=time_vector, - state_var_names=["position", "velocity"], - state_dot_var_names=["position_dot", "velocity_dot"], + state_var_names=["position_1", "velocity_1"], + state_dot_var_names=["position_1_dot", "velocity_1_dot"], initial_time=min(time_vector), final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, -- GitLab From 97358aff1757903ae30e12336f96281b36228ee2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 5 Oct 2023 11:32:52 +0200 Subject: [PATCH 163/237] Explain how to setup other formulation of problem. --- .../_examples/ode/plot_springs_discipline.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 1876d2da81..5817a8da96 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -186,6 +186,38 @@ plt.show() # .. figure:: figs/time_integration.png # :width: 400 # :alt: Couple, then integrate. +# +# To do so, we can use the list of MDO disciplines we created earlier to define an +# :class:`MDOChain`. + +mda = MDOChain( + mdo_disciplines, + grammar_type=MDODiscipline.GrammarType.SIMPLE, +) + +# %% +# +# We then define the ODE discipline that contains the couplings and execute it. + +ode_discipline = ODEDiscipline( + discipline=mda, + state_var_names=["position_1", "velocity_1", "position_2", "velocity_2"], + state_dot_var_names=[ + "position_1_dot", + "velocity_1_dot", + "position_2_dot", + "velocity_2_dot", + ], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, +) +output = ode_discipline.execute() + +plt.plot(time_vector, output["position_1"], label="mass 1") +plt.plot(time_vector, output["position_2"], label="mass 2") +plt.show() # %% # -- GitLab From 1c3262e5aa07c6672842c7ec390ecb4bf35f8be1 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 5 Oct 2023 11:36:29 +0200 Subject: [PATCH 164/237] Problem with more than two masses. --- doc_src/_examples/ode/plot_springs_discipline.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 5817a8da96..a0b64da540 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -223,6 +223,22 @@ plt.show() # # Solving the problem for a large number of point masses # ------------------------------------------------------ +# +# We first set the values of the masses and stiffnesses, as well as the initial position +# of each point mass. + +masses = rand(3) +stiffnesses = rand(4) +positions = [1, 0, 0] + +# %% +# +# The motion of each point mass can be described by the same ODE. + + +def mass_rhs(): + pass + # %% # -- GitLab From 00094ccef69a84f17916bc5562749de10ad686d0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 6 Oct 2023 10:53:19 +0200 Subject: [PATCH 165/237] Re-order parameters. --- src/gemseo/disciplines/ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index f33d947f3c..ecc0676ad2 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -76,8 +76,8 @@ class ODEDiscipline(MDODiscipline): def __init__( self, discipline: MDODiscipline, - state_var_names: list[str], time_vector: NDArray[float], + state_var_names: list[str], state_dot_var_names: list[str] | None = None, time_var_name: str = "time", initial_time: float = 0.0, -- GitLab From 87cac9ca42cac58e715618b1e15ae04900010a3e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 10 Oct 2023 15:07:37 +0200 Subject: [PATCH 166/237] Remove useless section. --- doc_src/_examples/ode/plot_springs_discipline.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index a0b64da540..ca26acb16e 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -240,12 +240,6 @@ def mass_rhs(): pass -# %% -# -# Comparing the obtained result to the analytical solution -# -------------------------------------------------------- -# - # %% # # Shortcut -- GitLab From 04adda1abff784e5e6e4d2e24bb517bb8060b0d3 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 11 Oct 2023 15:05:10 +0200 Subject: [PATCH 167/237] Add comparison to classic ODE formulation. --- tests/problems/ode/test_springs.py | 73 +++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index b8491f381a..934cd8d944 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -136,29 +136,48 @@ def test_make_rhs_function(): def test_2_chained_masses(): """Test the chained masses problem.""" time_vector = linspace(0.0, 10, 30) - - def mass_1_rhs(time=0, position_1=0, velocity_1=1, position_2=0): + mass_value_1 = 1 + mass_value_2 = 1 + stiff_1 = 1 + stiff_2 = 1 + stiff_3 = 1 + initial_position_1 = 0 + initial_velocity_1 = 1 + initial_position_2 = 0 + initial_velocity_2 = 1 + + def mass_1_rhs( + time=0, + position_1=initial_position_1, + velocity_1=initial_velocity_1, + position_2=initial_position_2, + ): position_1_dot, velocity_1_dot = generic_mass_rhs_function( time=time, state=array([position_1, velocity_1]), - mass=1, - left_stiff=1, - right_stiff=1, + mass=mass_value_1, + left_stiff=stiff_1, + right_stiff=stiff_2, left_position=0, right_position=position_2, time_vector=time_vector, ) return position_1_dot, velocity_1_dot - def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): + def mass_2_rhs( + time=0, + position_2=initial_position_2, + velocity_2=initial_velocity_2, + position_1=initial_position_1, + ): position_2_dot, velocity_2_dot = generic_mass_rhs_function( time=time, state=array([position_2, velocity_2]), - mass=1, - left_stiff=1, - right_stiff=1, - left_position=0, - right_position=position_1, + mass=mass_value_2, + left_stiff=stiff_2, + right_stiff=stiff_3, + left_position=position_1, + right_position=0, time_vector=time_vector, ) return position_2_dot, velocity_2_dot @@ -201,9 +220,39 @@ def test_2_chained_masses(): assert sorted(mda.coupling_structure.strong_couplings) == sorted( ["position_1", "position_2", "velocity_1", "velocity_2"] ) - mda.execute() + discipline_result = mda.execute() mda.plot_residual_history(save=True) + def _ode_func(time: NDArray[float], state: NDArray[float]) -> NDArray[float]: + position_1 = state[1] + velocity_1 = (stiff_1 + stiff_2) / mass_value_1 * state[ + 0 + ] + stiff_2 / mass_value_1 * state[2] + position_2 = state[3] + velocity_2 = (stiff_2 + stiff_3) / mass_value_2 * state[ + 2 + ] + stiff_2 / mass_value_2 * state[0] + return array([position_1, velocity_1, position_2, velocity_2]) + + ode_problem = ODEProblem( + _ode_func, + initial_time=min(time_vector), + initial_state=array( + [ + initial_position_1, + initial_velocity_1, + initial_position_2, + initial_velocity_2, + ] + ), + final_time=max(time_vector), + time_vector=time_vector, + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + ODESolversFactory().execute(ode_problem, "RK45", first_step=1e-6) + + assert allclose(ode_problem.result.state_vector[0], discipline_result["position"]) + def test_2_chained_masses_linear_coupling(): """Test the chained masses problem. -- GitLab From b79a0b5d18d1981ecfbbb88efdd9f0b73212b839 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 11 Oct 2023 15:47:08 +0200 Subject: [PATCH 168/237] Fix execution of ODEProblem. --- tests/problems/ode/test_springs.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 934cd8d944..08c3351780 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -247,11 +247,16 @@ def test_2_chained_masses(): ), final_time=max(time_vector), time_vector=time_vector, - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - ODESolversFactory().execute(ode_problem, "RK45", first_step=1e-6) + ODESolversFactory().execute( + ode_problem, + "RK45", + first_step=1e-6, + rtol=1e-12, + atol=1e-12, + ) - assert allclose(ode_problem.result.state_vector[0], discipline_result["position"]) + assert allclose(ode_problem.result.state_vector[0], discipline_result["position_1"]) def test_2_chained_masses_linear_coupling(): -- GitLab From 0211c43e02485465f635f8bb5620820db227542c Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 11 Oct 2023 16:33:33 +0200 Subject: [PATCH 169/237] Fix forgotten minus sign. --- tests/problems/ode/test_springs.py | 31 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 08c3351780..f5853fb088 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -141,10 +141,10 @@ def test_2_chained_masses(): stiff_1 = 1 stiff_2 = 1 stiff_3 = 1 - initial_position_1 = 0 - initial_velocity_1 = 1 + initial_position_1 = 1 + initial_velocity_1 = 0 initial_position_2 = 0 - initial_velocity_2 = 1 + initial_velocity_2 = 0 def mass_1_rhs( time=0, @@ -224,15 +224,17 @@ def test_2_chained_masses(): mda.plot_residual_history(save=True) def _ode_func(time: NDArray[float], state: NDArray[float]) -> NDArray[float]: - position_1 = state[1] - velocity_1 = (stiff_1 + stiff_2) / mass_value_1 * state[ - 0 - ] + stiff_2 / mass_value_1 * state[2] - position_2 = state[3] - velocity_2 = (stiff_2 + stiff_3) / mass_value_2 * state[ - 2 - ] + stiff_2 / mass_value_2 * state[0] - return array([position_1, velocity_1, position_2, velocity_2]) + position_1_dot = state[1] + velocity_1_dot = ( + -(stiff_1 + stiff_2) / mass_value_1 * state[0] + + stiff_2 / mass_value_1 * state[2] + ) + position_2_dot = state[3] + velocity_2_dot = ( + -(stiff_2 + stiff_3) / mass_value_2 * state[2] + + stiff_2 / mass_value_2 * state[0] + ) + return array([position_1_dot, velocity_1_dot, position_2_dot, velocity_2_dot]) ode_problem = ODEProblem( _ode_func, @@ -251,12 +253,13 @@ def test_2_chained_masses(): ODESolversFactory().execute( ode_problem, "RK45", - first_step=1e-6, rtol=1e-12, atol=1e-12, ) - assert allclose(ode_problem.result.state_vector[0], discipline_result["position_1"]) + assert allclose( + ode_problem.result.state_vector[0], discipline_result["position_1"], atol=1e-1 + ) def test_2_chained_masses_linear_coupling(): -- GitLab From c5149e013a29f44a79a68547a77f019b6c3553db Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 11 Oct 2023 16:37:41 +0200 Subject: [PATCH 170/237] Test values obtained with other formulation. --- tests/problems/ode/test_springs.py | 77 ++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index f5853fb088..258d1a368a 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -268,27 +268,46 @@ def test_2_chained_masses_linear_coupling(): IDF version of the problem with two masses. """ time_vector = linspace(0.0, 10, 30) + mass_value_1 = 1 + mass_value_2 = 1 + stiff_1 = 1 + stiff_2 = 1 + stiff_3 = 1 + initial_position_1 = 1 + initial_velocity_1 = 0 + initial_position_2 = 0 + initial_velocity_2 = 0 - def mass_1_rhs(time=0, position_1=0, velocity_1=0, position_2=0): + def mass_1_rhs( + time=0, + position_1=initial_position_1, + velocity_1=initial_velocity_1, + position_2=initial_position_2, + ): position_1_dot, velocity_1_dot = generic_mass_rhs_function( time=time, state=array([position_1, velocity_1]), - mass=1, - left_stiff=1, - right_stiff=1, + mass=mass_value_1, + left_stiff=stiff_1, + right_stiff=stiff_2, left_position=0, right_position=position_2, time_vector=time_vector, ) return position_1_dot, velocity_1_dot - def mass_2_rhs(time=0, position_2=0, velocity_2=1, position_1=0): + def mass_2_rhs( + time=0, + position_2=initial_position_2, + velocity_2=initial_velocity_2, + position_1=initial_position_1, + ): position_2_dot, velocity_2_dot = generic_mass_rhs_function( time=time, state=array([position_2, velocity_2]), - mass=1, - left_stiff=1, - right_stiff=1, + mass=mass_value_2, + left_stiff=stiff_2, + right_stiff=stiff_3, left_position=0, right_position=position_1, time_vector=time_vector, @@ -323,7 +342,47 @@ def test_2_chained_masses_linear_coupling(): final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - ode_discipline.execute() + discipline_result = ode_discipline.execute() + + def _ode_func(time: NDArray[float], state: NDArray[float]) -> NDArray[float]: + position_1_dot = state[1] + velocity_1_dot = ( + -(stiff_1 + stiff_2) / mass_value_1 * state[0] + + stiff_2 / mass_value_1 * state[2] + ) + position_2_dot = state[3] + velocity_2_dot = ( + -(stiff_2 + stiff_3) / mass_value_2 * state[2] + + stiff_2 / mass_value_2 * state[0] + ) + return array([position_1_dot, velocity_1_dot, position_2_dot, velocity_2_dot]) + + ode_problem = ODEProblem( + _ode_func, + initial_time=min(time_vector), + initial_state=array( + [ + initial_position_1, + initial_velocity_1, + initial_position_2, + initial_velocity_2, + ] + ), + final_time=max(time_vector), + time_vector=time_vector, + ) + ODESolversFactory().execute( + ode_problem, + "RK45", + rtol=1e-12, + atol=1e-12, + ) + + assert allclose( + ode_problem.result.state_vector[0], + discipline_result["position_1"], + atol=1e-6, + ) def test_create_mass_mdo_discipline(): -- GitLab From 26eeebd6219f8f07cbd537b21c7d691e572c3940 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 13 Oct 2023 14:04:19 +0200 Subject: [PATCH 171/237] Add mass as parameter to test. --- tests/problems/ode/test_springs.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 258d1a368a..7da5139eee 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -41,24 +41,28 @@ from numpy.typing import NDArray @pytest.mark.parametrize( "left_stiff, right_stiff, initial_position, initial_velocity", - [ - (1, 1, 0, 1), - (10, 1, 0, 1), - (1, 10, 0, 1), - (1, 1, 1, 1), - (1, 1, 0, 2), - (2, 2, 1, 0), + "mass"[ + (1, 1, 0, 1, 1), + (10, 1, 0, 1, 1), + (1, 1, 0, 1, 10), + (10, 1, 0, 1, 10), + (1, 10, 0, 1, 1), + (1, 10, 1, 1, 1), + (1, 1, 0, 10, 1), + (10, 10, 1, 0, 1), + (1, 10, 1, 1, 10), + (1, 1, 0, 10, 10), + (10, 10, 1, 0, 10), ], ) def test_generic_mass_rhs_function( - left_stiff, right_stiff, initial_position, initial_velocity + left_stiff, right_stiff, initial_position, initial_velocity, mass ): """Test the resolution for a single mass connected by springs to fixed points. Verify the values of the output for various initial conditions. """ time_vector = linspace(0, 10, 30) - mass = 2 left_position = 0 right_position = 0 -- GitLab From c76681887fbcdb32e871c47ca28ffebaba1eca39 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 13 Oct 2023 14:15:56 +0200 Subject: [PATCH 172/237] Fix parametrization of test. --- tests/problems/ode/test_springs.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 7da5139eee..5dd817fb2b 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -40,8 +40,8 @@ from numpy.typing import NDArray @pytest.mark.parametrize( - "left_stiff, right_stiff, initial_position, initial_velocity", - "mass"[ + "left_stiff, right_stiff, initial_position, initial_velocity, mass", + [ (1, 1, 0, 1, 1), (10, 1, 0, 1, 1), (1, 1, 0, 1, 10), @@ -56,7 +56,11 @@ from numpy.typing import NDArray ], ) def test_generic_mass_rhs_function( - left_stiff, right_stiff, initial_position, initial_velocity, mass + left_stiff, + right_stiff, + initial_position, + initial_velocity, + mass, ): """Test the resolution for a single mass connected by springs to fixed points. -- GitLab From 47ce55e7a078a450dfd8f42cebf865fb1cbf75f0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 13 Oct 2023 16:45:32 +0200 Subject: [PATCH 173/237] Test multiple parametrizations. --- tests/problems/ode/test_springs.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 5dd817fb2b..587c7ba330 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -66,7 +66,7 @@ def test_generic_mass_rhs_function( Verify the values of the output for various initial conditions. """ - time_vector = linspace(0, 10, 30) + time_vector = linspace(0, 5, 30) left_position = 0 right_position = 0 @@ -141,13 +141,18 @@ def test_make_rhs_function(): assert rhs_function(0, 0, 1)[0] == 1 -def test_2_chained_masses(): +@pytest.mark.parametrize("stiff_1", [0.1, 1]) +@pytest.mark.parametrize("stiff_2", [0.1, 1]) +@pytest.mark.parametrize("mass_value_1", [1, 10]) +def test_2_chained_masses( + stiff_1, + stiff_2, + mass_value_1, +): """Test the chained masses problem.""" time_vector = linspace(0.0, 10, 30) mass_value_1 = 1 mass_value_2 = 1 - stiff_1 = 1 - stiff_2 = 1 stiff_3 = 1 initial_position_1 = 1 initial_velocity_1 = 0 -- GitLab From d456e3558d333f14a424502d1d401ad12d28bc4f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 17 Oct 2023 11:45:18 +0200 Subject: [PATCH 174/237] Finish sentence. --- doc_src/disciplines/ode_discipline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc_src/disciplines/ode_discipline.rst b/doc_src/disciplines/ode_discipline.rst index 12f3d5a2a8..cf58b74d0a 100644 --- a/doc_src/disciplines/ode_discipline.rst +++ b/doc_src/disciplines/ode_discipline.rst @@ -17,7 +17,7 @@ What is an ODE discipline? ODE stands for Ordinary Differential Equation. -An ODE discipline is +An ODE discipline is a discipline that is defined using the solution of an ODE. How to build an ODE discipline? =============================== -- GitLab From d9cf8d1879218fe790e1f80333a32d07cfe49f51 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 17 Oct 2023 14:51:32 +0200 Subject: [PATCH 175/237] Add setter for `time_vector` property. --- src/gemseo/algos/ode/ode_problem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gemseo/algos/ode/ode_problem.py b/src/gemseo/algos/ode/ode_problem.py index c3a0030efc..7c9608cab0 100644 --- a/src/gemseo/algos/ode/ode_problem.py +++ b/src/gemseo/algos/ode/ode_problem.py @@ -105,6 +105,10 @@ class ODEProblem(BaseProblem): """The times at which the solution shall be evaluated.""" return self.__time_vector + @time_vector.setter + def time_vector(self, value): + self.__time_vector = value + def check(self) -> None: """Ensure the parameters of the problem are consistent. -- GitLab From dc7dfa4c2198eaeff473169d1006d9435e4d5f8d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 18 Oct 2023 11:58:13 +0200 Subject: [PATCH 176/237] Fix types. --- doc_src/_examples/ode/plot_springs_discipline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index ca26acb16e..e356f7558d 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -224,8 +224,8 @@ plt.show() # Solving the problem for a large number of point masses # ------------------------------------------------------ # -# We first set the values of the masses and stiffnesses, as well as the initial position -# of each point mass. +# We first set the values of the mass and stiffness of each spring, as well as the initial +# position of each point mass. masses = rand(3) stiffnesses = rand(4) @@ -250,8 +250,8 @@ def mass_rhs(): # -masses = rand(3) -stiffnesses = rand(4) +masses = list(rand(3)) +stiffnesses = list(rand(4)) positions = [1, 0, 0] chained_masses = create_chained_masses(masses, stiffnesses, positions) -- GitLab From c8834820f77d443b6380ea6bfccb9b6f51930155 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 18 Oct 2023 14:02:36 +0200 Subject: [PATCH 177/237] Fix indexes. --- doc_src/_examples/ode/plot_springs_discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index e356f7558d..ddc5b90557 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -131,10 +131,10 @@ mdo_disciplines = [ ode_disciplines = [ ODEDiscipline( discipline=disc, - state_var_names=["position_" + str(i), "velocity_" + str(i)], + state_var_names=["position_" + str(i + 1), "velocity_" + str(i + 1)], state_dot_var_names=[ - "position_" + str(i) + "_dot", - "velocity_" + str(i) + "_dot", + "position_" + str(i + 1) + "_dot", + "velocity_" + str(i + 1) + "_dot", ], time_vector=time_vector, initial_time=min(time_vector), -- GitLab From e50f6373bbf134010d823fb391e7c8188fd77fa7 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 18 Oct 2023 15:19:48 +0200 Subject: [PATCH 178/237] Get tutorial to run. --- .../_examples/ode/plot_springs_discipline.py | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index ddc5b90557..e9765d352a 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -69,50 +69,50 @@ from numpy.random import default_rng as rand # Let's consider the problem described above in the case of two masses. First we describe # the right-hand side (RHS) of the equations of motion for each point mass. +stiff_0 = 1 stiff_1 = 1 stiff_2 = 1 -stiff_3 = 1 +mass_0 = 1 mass_1 = 1 -mass_2 = 1 time_vector = linspace( 0, 1, 30 ) # Vector of times at which we want to solve the problem. -def mass_1_rhs(time=0, position_1=0, velocity_1=0, position_2=0): +def mass_0_rhs(time=0, position_0=0, velocity_0=0, position_1=0): """Function describing the equation of motion for the first point mass. Args: time: - position_1: Position of the first point mass. - velocity_1: Velocity of the first point mass. - position_2: Position of the second point mass. + position_0: Position of the first point mass. + velocity_0: Velocity of the first point mass. + position_1: Position of the second point mass. Returns: - position_1_dot: The derivative of `position_1` - velocity_1_dot: The derivative of `velocity_1` + position_0_dot: The derivative of `position_0` + velocity_0_dot: The derivative of `velocity_0` """ - position_1_dot = velocity_1 - velocity_1_dot = (-(stiff_1 + stiff_2) * position_1 + stiff_2 * position_2) / mass_1 - return position_1_dot, velocity_1_dot + position_0_dot = velocity_0 + velocity_0_dot = (-(stiff_0 + stiff_1) * position_0 + stiff_1 * position_1) / mass_0 + return position_0_dot, velocity_0_dot -def mass_2_rhs(time=0, position_2=0, velocity_2=0, position_1=0): +def mass_1_rhs(time=0, position_1=0, velocity_1=0, position_0=0): """Function describing the equation of motion for the second point mass. Args: time: - position_2: Position of the second point mass. - velocity_2: Velocity of the second point mass. - position_1: Position of the first point mass. + position_1: Position of the second point mass. + velocity_1: Velocity of the second point mass. + position_0: Position of the first point mass. Returns: - position_2_dot: The derivative of `position_2` - velocity_2_dot: The derivative of `velocity_2` + position_1_dot: The derivative of `position_1` + velocity_1_dot: The derivative of `velocity_1` """ - position_2_dot = velocity_2 - velocity_2_dot = (-(stiff_2 + stiff_3) * position_2 + stiff_2 * position_1) / mass_2 - return position_2_dot, velocity_2_dot + position_1_dot = velocity_1 + velocity_1_dot = (-(stiff_1 + stiff_2) * position_1 + stiff_1 * position_0) / mass_1 + return position_1_dot, velocity_1_dot # %% @@ -126,15 +126,15 @@ mdo_disciplines = [ py_func=func, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - for func in [mass_1_rhs, mass_2_rhs] + for func in [mass_0_rhs, mass_1_rhs] ] ode_disciplines = [ ODEDiscipline( discipline=disc, - state_var_names=["position_" + str(i + 1), "velocity_" + str(i + 1)], + state_var_names=["position_" + str(i), "velocity_" + str(i)], state_dot_var_names=[ - "position_" + str(i + 1) + "_dot", - "velocity_" + str(i + 1) + "_dot", + "position_" + str(i) + "_dot", + "velocity_" + str(i) + "_dot", ], time_vector=time_vector, initial_time=min(time_vector), @@ -144,6 +144,8 @@ ode_disciplines = [ for i, disc in enumerate(mdo_disciplines) ] +for disc in ode_disciplines: + disc.execute() # %% # @@ -164,8 +166,8 @@ mda.plot_residual_history() # Plotting the solution # --------------------- +plt.plot(time_vector, output["position_0"], label="mass 0") plt.plot(time_vector, output["position_1"], label="mass 1") -plt.plot(time_vector, output["position_2"], label="mass 2") plt.show() # %% @@ -201,12 +203,12 @@ mda = MDOChain( ode_discipline = ODEDiscipline( discipline=mda, - state_var_names=["position_1", "velocity_1", "position_2", "velocity_2"], + state_var_names=["position_0", "velocity_0", "position_1", "velocity_1"], state_dot_var_names=[ + "position_0_dot", + "velocity_0_dot", "position_1_dot", "velocity_1_dot", - "position_2_dot", - "velocity_2_dot", ], time_vector=time_vector, initial_time=min(time_vector), @@ -215,8 +217,8 @@ ode_discipline = ODEDiscipline( ) output = ode_discipline.execute() +plt.plot(time_vector, output["position_0"], label="mass 0") plt.plot(time_vector, output["position_1"], label="mass 1") -plt.plot(time_vector, output["position_2"], label="mass 2") plt.show() # %% -- GitLab From eeb206e4edace98e6512b09c7af1846a9027f75b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 18 Oct 2023 15:38:47 +0200 Subject: [PATCH 179/237] Modify initial values. --- .../_examples/ode/plot_springs_discipline.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index e9765d352a..fd2ed93412 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -74,12 +74,19 @@ stiff_1 = 1 stiff_2 = 1 mass_0 = 1 mass_1 = 1 -time_vector = linspace( - 0, 1, 30 -) # Vector of times at which we want to solve the problem. - - -def mass_0_rhs(time=0, position_0=0, velocity_0=0, position_1=0): +initial_position_0 = 1 +initial_position_1 = 0 +initial_velocity_0 = 0 +initial_velocity_1 = 0 +time_vector = linspace(0, 1, 30) # Vector of times at which to solve the problem. + + +def mass_0_rhs( + time=0, + position_0=initial_position_0, + velocity_0=initial_velocity_0, + position_1=initial_position_1, +): """Function describing the equation of motion for the first point mass. Args: @@ -97,7 +104,12 @@ def mass_0_rhs(time=0, position_0=0, velocity_0=0, position_1=0): return position_0_dot, velocity_0_dot -def mass_1_rhs(time=0, position_1=0, velocity_1=0, position_0=0): +def mass_1_rhs( + time=0, + position_1=initial_position_1, + velocity_1=initial_velocity_1, + position_0=initial_position_0, +): """Function describing the equation of motion for the second point mass. Args: -- GitLab From 439f1b0b0c942df56215c873e9f38dac360f7e2d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 18 Oct 2023 16:41:38 +0200 Subject: [PATCH 180/237] Improve error message. --- src/gemseo/algos/ode/ode_problem.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/gemseo/algos/ode/ode_problem.py b/src/gemseo/algos/ode/ode_problem.py index 7c9608cab0..39205e6b5b 100644 --- a/src/gemseo/algos/ode/ode_problem.py +++ b/src/gemseo/algos/ode/ode_problem.py @@ -115,12 +115,14 @@ class ODEProblem(BaseProblem): Raises: ValueError: If the state and time shapes are inconsistent. """ - if ( - self.result.state_vector.size != 0 - and self.result.state_vector.shape[1] != self.result.time_vector.size - ): - msg = "Inconsistent state and time shapes." - raise ValueError(msg) + if self.result.state_vector.size != 0: + state_size = self.result.state_vector.shape[1] + time_size = self.result.time_vector.size + if state_size != time_size: + raise ValueError( + "Inconsistent state and time shapes: " + + f"{state_size} and {time_size}." + ) def _func(self, state) -> ndarray: return asarray(self.rhs_function(self.result.time_vector, state)) -- GitLab From ad056df8822032b5290beab4045252070c937289 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 7 Nov 2023 15:39:21 +0100 Subject: [PATCH 181/237] Add function for creating ODEDisciplines. --- src/gemseo/disciplines/ode_discipline.py | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index ecc0676ad2..0db35e1832 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -37,6 +37,7 @@ from gemseo.core.discipline import MDODiscipline from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( MDODisciplineAdapterGenerator, ) +from gemseo.disciplines.remapping import RemappingDiscipline from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array @@ -189,3 +190,60 @@ class ODEDiscipline(MDODiscipline): } ) self.local_data["time_vector"] = self.time_vector + + +def create_ode_discipline( + mdo_discipline: MDODiscipline, + time_vector: NDArray[float], + initial_time: float, + final_time: float, + state_var_names: list[str], + state_dot_var_names: list[str], + ode_solver_options: dict | None = None, +) -> ODEDiscipline: + """Create an ODEDiscipline with custom names for the input and output. + + Args: + mdo_discipline: + time_vector: + initial_time: + final_time: + state_var_names: + state_dot_var_names: + ode_solver_options: + + Returns: + The ODEDiscipline. + """ + if ode_solver_options is None: + ode_solver_options = {} + + _state_var_names = mdo_discipline.get_input_data_names()[1:] + input_mapping = { + custom_name: default_name + for custom_name, default_name in zip(state_var_names, _state_var_names) + } + input_mapping["time"] = "time" + + _state_dot_var_names = mdo_discipline.get_output_data_names() + output_mapping = { + custom_name: default_name + for custom_name, default_name in zip(state_dot_var_names, _state_dot_var_names) + } + + renamed_discipline = RemappingDiscipline( + mdo_discipline, + input_mapping, + output_mapping, + ) + + ode_discipline = ODEDiscipline( + discipline=renamed_discipline, + state_var_names=state_var_names, + state_dot_var_names=state_dot_var_names, + time_vector=time_vector, + initial_time=initial_time, + final_time=final_time, + ode_solver_options=ode_solver_options, + ) + return ode_discipline -- GitLab From ca87d826866177e2fd696d60976ebdc2cde48551 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 7 Nov 2023 17:12:32 +0100 Subject: [PATCH 182/237] Add test for coupled disciplines. --- tests/disciplines/test_ode_discipline.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 2f02e4f227..f41d8f307f 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,6 +25,7 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline +from gemseo.disciplines.ode_discipline import create_ode_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline @@ -123,3 +124,51 @@ def test_ode_discipline_bad_grammar(): ) assert "not outputs of the discipline" in str(error_info.value) + + +def test_coupled_disciplines(): + """Test the execution of ODE Disciplines with coupled variables.""" + time_vector = linspace(0, 10, 30) + + def first_oscillator(time, first_position, first_velocity, second_position): + first_position_dot = first_velocity + first_velocity_dot = first_position - second_position + return first_position_dot, first_velocity_dot + + def second_oscillator(time, second_position, second_velocity, first_position): + second_position_dot = second_velocity + second_velocity_dot = second_position - first_position + return second_position_dot, second_velocity_dot + + first_mdo_disc = create_discipline( + "AutoPyDiscipline", + py_func=first_oscillator, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + second_mdo_disc = create_discipline( + "AutoPyDiscipline", + py_func=second_oscillator, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + + first_ode = create_ode_discipline( + mdo_discipline=first_mdo_disc, + state_var_names=["first_position", "first_velocity", "second_position"], + state_dot_var_names=["first_position_dot", "first_velocity_dot"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + second_ode = create_ode_discipline( + mdo_discipline=second_mdo_disc, + state_var_names=["second_position", "second_velocity", "first_position"], + state_dot_var_names=["second_position_dot", "second_velocity_dot"], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + + first_ode.execute() + second_ode.execute() -- GitLab From 144604701d7b46d1087951482d8ba30af4cb3cc4 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 8 Nov 2023 16:30:43 +0100 Subject: [PATCH 183/237] Add test for other formulation. --- tests/disciplines/test_ode_discipline.py | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index f41d8f307f..96c7358985 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,6 +25,7 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline +from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import create_ode_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline @@ -151,6 +152,31 @@ def test_coupled_disciplines(): grammar_type=MDODiscipline.GrammarType.SIMPLE, ) + mda = MDOChain( + [first_mdo_disc, second_mdo_disc], + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ) + ode_discipline = ODEDiscipline( + discipline=mda, + state_var_names=[ + "first_position", + "first_velocity", + "second_position", + "second_velocity", + ], + state_dot_var_names=[ + "first_position_dot", + "first_velocity_dot", + "second_position_dot", + "second_velocity_dot", + ], + time_vector=time_vector, + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, + ) + ode_discipline.execute() + first_ode = create_ode_discipline( mdo_discipline=first_mdo_disc, state_var_names=["first_position", "first_velocity", "second_position"], -- GitLab From e6756004fe47959330881190fd900c1d05d63f19 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 22 Nov 2023 16:20:56 +0100 Subject: [PATCH 184/237] Revert "Add test for other formulation." This reverts commit e50f725514d8fd8148b96432f9fdacd26c404041. --- tests/disciplines/test_ode_discipline.py | 26 ------------------------ 1 file changed, 26 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 96c7358985..f41d8f307f 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,7 +25,6 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline -from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import create_ode_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline @@ -152,31 +151,6 @@ def test_coupled_disciplines(): grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - mda = MDOChain( - [first_mdo_disc, second_mdo_disc], - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_discipline = ODEDiscipline( - discipline=mda, - state_var_names=[ - "first_position", - "first_velocity", - "second_position", - "second_velocity", - ], - state_dot_var_names=[ - "first_position_dot", - "first_velocity_dot", - "second_position_dot", - "second_velocity_dot", - ], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - ode_discipline.execute() - first_ode = create_ode_discipline( mdo_discipline=first_mdo_disc, state_var_names=["first_position", "first_velocity", "second_position"], -- GitLab From 7c9b02a9307d439ba70866aa50aa40d7509cea26 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 22 Nov 2023 16:21:04 +0100 Subject: [PATCH 185/237] Revert "Add function for creating ODEDisciplines." This reverts commit 9747960a8222c2df050afe50a9341a178b3b65d4. --- src/gemseo/disciplines/ode_discipline.py | 58 ------------------------ 1 file changed, 58 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 0db35e1832..ecc0676ad2 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -37,7 +37,6 @@ from gemseo.core.discipline import MDODiscipline from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( MDODisciplineAdapterGenerator, ) -from gemseo.disciplines.remapping import RemappingDiscipline from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array @@ -190,60 +189,3 @@ class ODEDiscipline(MDODiscipline): } ) self.local_data["time_vector"] = self.time_vector - - -def create_ode_discipline( - mdo_discipline: MDODiscipline, - time_vector: NDArray[float], - initial_time: float, - final_time: float, - state_var_names: list[str], - state_dot_var_names: list[str], - ode_solver_options: dict | None = None, -) -> ODEDiscipline: - """Create an ODEDiscipline with custom names for the input and output. - - Args: - mdo_discipline: - time_vector: - initial_time: - final_time: - state_var_names: - state_dot_var_names: - ode_solver_options: - - Returns: - The ODEDiscipline. - """ - if ode_solver_options is None: - ode_solver_options = {} - - _state_var_names = mdo_discipline.get_input_data_names()[1:] - input_mapping = { - custom_name: default_name - for custom_name, default_name in zip(state_var_names, _state_var_names) - } - input_mapping["time"] = "time" - - _state_dot_var_names = mdo_discipline.get_output_data_names() - output_mapping = { - custom_name: default_name - for custom_name, default_name in zip(state_dot_var_names, _state_dot_var_names) - } - - renamed_discipline = RemappingDiscipline( - mdo_discipline, - input_mapping, - output_mapping, - ) - - ode_discipline = ODEDiscipline( - discipline=renamed_discipline, - state_var_names=state_var_names, - state_dot_var_names=state_dot_var_names, - time_vector=time_vector, - initial_time=initial_time, - final_time=final_time, - ode_solver_options=ode_solver_options, - ) - return ode_discipline -- GitLab From eed2dec1aa3a250f306722efa6bfc112301839a2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 22 Nov 2023 16:21:10 +0100 Subject: [PATCH 186/237] Revert "Add test for coupled disciplines." This reverts commit 77d19375e86e2e5b9915e3e819b0dcfc87111c44. --- tests/disciplines/test_ode_discipline.py | 49 ------------------------ 1 file changed, 49 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index f41d8f307f..2f02e4f227 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -25,7 +25,6 @@ from __future__ import annotations import pytest from gemseo import create_discipline from gemseo import MDODiscipline -from gemseo.disciplines.ode_discipline import create_ode_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline @@ -124,51 +123,3 @@ def test_ode_discipline_bad_grammar(): ) assert "not outputs of the discipline" in str(error_info.value) - - -def test_coupled_disciplines(): - """Test the execution of ODE Disciplines with coupled variables.""" - time_vector = linspace(0, 10, 30) - - def first_oscillator(time, first_position, first_velocity, second_position): - first_position_dot = first_velocity - first_velocity_dot = first_position - second_position - return first_position_dot, first_velocity_dot - - def second_oscillator(time, second_position, second_velocity, first_position): - second_position_dot = second_velocity - second_velocity_dot = second_position - first_position - return second_position_dot, second_velocity_dot - - first_mdo_disc = create_discipline( - "AutoPyDiscipline", - py_func=first_oscillator, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - second_mdo_disc = create_discipline( - "AutoPyDiscipline", - py_func=second_oscillator, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - - first_ode = create_ode_discipline( - mdo_discipline=first_mdo_disc, - state_var_names=["first_position", "first_velocity", "second_position"], - state_dot_var_names=["first_position_dot", "first_velocity_dot"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - second_ode = create_ode_discipline( - mdo_discipline=second_mdo_disc, - state_var_names=["second_position", "second_velocity", "first_position"], - state_dot_var_names=["second_position_dot", "second_velocity_dot"], - time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) - - first_ode.execute() - second_ode.execute() -- GitLab From 329b6addb1f7cb44dd3628c1ce1d28fe17e14228 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 24 Nov 2023 10:07:11 +0100 Subject: [PATCH 187/237] Revert "Improve error message." This reverts commit 02ed52127ab15d1f7bf16d9cdf86d7b6312fb677. --- src/gemseo/algos/ode/ode_problem.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/gemseo/algos/ode/ode_problem.py b/src/gemseo/algos/ode/ode_problem.py index 39205e6b5b..46df8f5732 100644 --- a/src/gemseo/algos/ode/ode_problem.py +++ b/src/gemseo/algos/ode/ode_problem.py @@ -116,13 +116,8 @@ class ODEProblem(BaseProblem): ValueError: If the state and time shapes are inconsistent. """ if self.result.state_vector.size != 0: - state_size = self.result.state_vector.shape[1] - time_size = self.result.time_vector.size - if state_size != time_size: - raise ValueError( - "Inconsistent state and time shapes: " - + f"{state_size} and {time_size}." - ) + if self.result.state_vector.shape[1] != self.result.time_vector.size: + raise ValueError("Inconsistent state and time shapes.") def _func(self, state) -> ndarray: return asarray(self.rhs_function(self.result.time_vector, state)) -- GitLab From 89bc85fa8c08be4f044f88e1786446235d34d1bb Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 28 Feb 2024 12:52:10 +0100 Subject: [PATCH 188/237] Improve docstrings. --- tests/disciplines/test_ode_discipline.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 2f02e4f227..250b28bb86 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -36,8 +36,8 @@ from numpy import linspace from numpy import sin -def test_create_oscillator_ode_discipline(): - """Test the ODEDiscipline on the oscillator validation case.""" +def test_create_oscillator_ode_discipline() -> None: + """Test the creation of an ODE Discipline.""" time_vector = linspace(0.0, 10, 30) ode_disc = create_oscillator_ode_discipline(time_vector, omega=4) out = ode_disc.execute() @@ -49,7 +49,8 @@ def test_create_oscillator_ode_discipline(): assert allclose(out["velocity"], analytical_velocity) -def test_oscillator_ode_discipline(): +def test_oscillator_ode_discipline() -> None: + """Test an ODE Discipline representing a simple oscillator.""" time_vector = linspace(0.0, 10, 30) def rhs_function(time=0, position=0, velocity=1): # default values are necessary ! -- GitLab From 6e642bba31d7e59fa1cc0ed9eb3a2fd3a8d8925d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 28 Feb 2024 14:01:04 +0100 Subject: [PATCH 189/237] Docstrings and typehints. --- tests/disciplines/test_ode_discipline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 250b28bb86..e34360148d 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -80,13 +80,19 @@ def test_oscillator_ode_discipline() -> None: assert allclose(out["velocity"], analytical_velocity) +def test_van_der_pol_ode_discipline() -> None: + """Test the ODE Discipline with the Van der Pol problem.""" + + def test_orbital_discipline(): + """Test the ODE Discipline with the orbital problem.""" time_vector = linspace(0.0, 10, 30) ode_discipline = create_orbital_discipline(time_vector) ode_discipline.execute() -def test_ode_discipline_bad_grammar(): +def test_ode_discipline_bad_grammar() -> None: + """Test error messages when passing an ill-formed grammar.""" _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) -- GitLab From 8eff94e8bd27cee21809dbb2bfd60970017e5927 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Wed, 28 Feb 2024 18:19:56 +0100 Subject: [PATCH 190/237] Set the time vector for the result. --- src/gemseo/algos/ode/lib_scipy_ode.py | 1 + tests/algos/ode/test_lib_scipy_ode.py | 27 ++------------------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/gemseo/algos/ode/lib_scipy_ode.py b/src/gemseo/algos/ode/lib_scipy_ode.py index 382eabf024..47f10b84da 100644 --- a/src/gemseo/algos/ode/lib_scipy_ode.py +++ b/src/gemseo/algos/ode/lib_scipy_ode.py @@ -138,6 +138,7 @@ class ScipyODEAlgos(ODESolverLib): self.problem.result.state_vector = solution.y if self.problem.time_vector is None: self.problem.time_vector = solution.t + self.problem.result.time_vector = solution.t self.problem.result.n_func_evaluations = solution.nfev self.problem.result.n_jac_evaluations = solution.njev diff --git a/tests/algos/ode/test_lib_scipy_ode.py b/tests/algos/ode/test_lib_scipy_ode.py index a2f285aac8..379d76761c 100644 --- a/tests/algos/ode/test_lib_scipy_ode.py +++ b/tests/algos/ode/test_lib_scipy_ode.py @@ -28,7 +28,6 @@ from numpy import array from numpy import exp from numpy import sqrt from numpy import sum -from numpy import zeros from numpy.linalg import norm from gemseo.algos.ode.lib_scipy_ode import ScipyODEAlgos @@ -107,9 +106,8 @@ def test_ode_problem_1d(time_vector) -> None: ) analytical_solution = exp(problem.result.time_vector) - assert sqrt(sum((problem.result.state_vector - analytical_solution) ** 2)) < 1e-6 - - problem.check() + difference = problem.result.state_vector - analytical_solution + assert sqrt(sum(difference**2)) < 1e-6 assert problem.rhs_function == _func assert problem.jac == _jac @@ -204,7 +202,6 @@ def test_van_der_pol(algo_name) -> None: problem.result.solver_message == "The solver successfully reached the " "end of the integration interval." ) - problem.check() @parametrized_algo_names @@ -277,23 +274,3 @@ def test_unconverged(caplog) -> None: assert not problem.result.is_converged assert f"The ODE solver {algo_name} did not converge." in caplog.records[1].message - - -@pytest.mark.parametrize("problem", [OrbitalDynamics, VanDerPol]) -def test_check_ode_problem(problem) -> None: - """Ensure the check method of ODEProblem behaves as expected.""" - problem = problem() - assert problem.result.state_vector.size == 0 - problem.check() - - ODESolversFactory().execute(problem) - assert problem.result.state_vector is not None - problem.check() - - problem.result.time_vector = zeros(0) - try: - problem.check() - except ValueError: - pass - else: - raise ValueError -- GitLab From 5edc45d0668c365a6cf8e1d5f756e6fa379553aa Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 11:34:37 +0100 Subject: [PATCH 191/237] Remove empty function. --- tests/disciplines/test_ode_discipline.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index e34360148d..bd071de850 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -80,10 +80,6 @@ def test_oscillator_ode_discipline() -> None: assert allclose(out["velocity"], analytical_velocity) -def test_van_der_pol_ode_discipline() -> None: - """Test the ODE Discipline with the Van der Pol problem.""" - - def test_orbital_discipline(): """Test the ODE Discipline with the orbital problem.""" time_vector = linspace(0.0, 10, 30) -- GitLab From 7a408457d8bd77871bbb5544c0253851879dae9d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 15:17:10 +0100 Subject: [PATCH 192/237] Make function robust to nul distance. --- src/gemseo/problems/ode/orbital_dynamics.py | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index 5a74890c7f..6789683ee1 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -72,12 +72,17 @@ from typing import TYPE_CHECKING from numpy import array from numpy import zeros -from gemseo import create_discipline from gemseo import MDODiscipline +from gemseo import create_discipline from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.disciplines.ode_discipline import ODEDiscipline default_excentricity = 0.5 +initial_velocity_y = ( + (1 - default_excentricity**2) + / (1 - default_excentricity) + / (1 - default_excentricity) +) if TYPE_CHECKING: from numpy.typing import NDArray @@ -88,16 +93,19 @@ def _compute_rhs( position_x: float = 1 - default_excentricity, position_y: float = 0, velocity_x: float = 0, - velocity_y: float = (1 - default_excentricity**2) - / (1 - default_excentricity) - / (1 - default_excentricity), + velocity_y: float = initial_velocity_y, ) -> (float, float, float, float): # noqa:U100 """Compute the right-hand side of ODE.""" r = sqrt(position_x * position_x + position_y * position_y) position_x_dot = velocity_x position_y_dot = velocity_y - velocity_x_dot = -position_x / r / r / r - velocity_y_dot = -position_y / r / r / r + if r != 0: + velocity_x_dot = -position_x / r / r / r + velocity_y_dot = -position_y / r / r / r + else: + velocity_x_dot = 0 + velocity_y_dot = 0 + return position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot @@ -157,17 +165,16 @@ class OrbitalDynamics(ODEProblem): def create_orbital_discipline(time_vector: NDArray[float]) -> ODEDiscipline: + """Create an ODE Discipline for the Orbital problem.""" mdo_discipline = create_discipline( "AutoPyDiscipline", py_func=_compute_rhs, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - ode_discipline = ODEDiscipline( + return ODEDiscipline( discipline=mdo_discipline, state_var_names=["position_x", "position_y", "velocity_x", "velocity_y"], time_vector=time_vector, initial_time=min(time_vector), final_time=max(time_vector), ) - - return ode_discipline -- GitLab From 22189d434a905985ab86492416e54ed9d01f0904 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 15:43:06 +0100 Subject: [PATCH 193/237] Fix definition of problem. --- src/gemseo/problems/ode/orbital_dynamics.py | 24 ++++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index 6789683ee1..bc13b560f1 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -89,23 +89,21 @@ if TYPE_CHECKING: def _compute_rhs( - time: float = 0, - position_x: float = 1 - default_excentricity, - position_y: float = 0, - velocity_x: float = 0, - velocity_y: float = initial_velocity_y, -) -> (float, float, float, float): # noqa:U100 - """Compute the right-hand side of ODE.""" - r = sqrt(position_x * position_x + position_y * position_y) + time=0, + position_x=1 - default_excentricity, + position_y=0, + velocity_x=1, + velocity_y=initial_velocity_y, +): + den = position_x * position_x + position_y * position_y position_x_dot = velocity_x position_y_dot = velocity_y - if r != 0: - velocity_x_dot = -position_x / r / r / r - velocity_y_dot = -position_y / r / r / r - else: + if den == 0: velocity_x_dot = 0 velocity_y_dot = 0 - + else: + velocity_x_dot = -position_x / den + velocity_y_dot = -position_y / den return position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot -- GitLab From 964197151ed0ab098c355dac0248307c29781dfb Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 15:51:17 +0100 Subject: [PATCH 194/237] Improve ascii sketch. --- src/gemseo/problems/springs/springs.py | 88 ++++++++++++-------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index 773056fc91..df2daf010c 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -19,9 +19,9 @@ r"""The discipline for describing the motion of masses connected by springs. Consider a system of :math:`n` point masses with masses :math:`m_1$, :math:`m_2$,... -:math:`m_n` connected in series by springs. The displacement of the point masses relative -to the position at rest are denoted by :math:`x_1`, :math:`x_2`,... :math:`x_n`. Each -spring has stiffness :math:`k_1`, :math:`k_2`,... :math:`k_{n+1}`. +:math:`m_n` connected in series by springs. The displacement of the point masses +relative to the position at rest are denoted by :math:`x_1`, :math:`x_2`,... +:math:`x_n`. Each spring has stiffness :math:`k_1`, :math:`k_2`,... :math:`k_{n+1}`. Motion is assumed to only take place in one dimension, along the axes of the springs. @@ -33,13 +33,13 @@ For :math:`n=2`, the system is as follows: .. asciiart:: - | | - | k1 ________ k2 ________ k3 | - | /\ /\ | | /\ /\ | | /\ /\ | - |__/ \ / \ __| m1 |__/ \ / \ __| m2 |__/ \ / \ __| - | \ / \ / | | \ / \ / | | \ / \ / | - | \/ \/ |________| \/ \/ |________| \/ \/ | - | | | | + | | + | k1 ________ k2 ________ k3 | + | /\ /\ | | /\ /\ | | /\ /\ | + |_/ \ / \ __| m1 |__/ \ / \ __| m2 |__/ \ / \ _| + | \ / \ / | | \ / \ / | | \ / \ / | + | \/ \/ |________| \/ \/ |________| \/ \/ | + | | | | ---|---> ---|---> | x1 | x2 @@ -71,15 +71,17 @@ This can be re-written as a système of 1st order ordinary differential equation + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} \end{cases} \right. """ + from __future__ import annotations +from typing import TYPE_CHECKING + from numpy import array from numpy import cos from numpy import linspace from numpy import ndarray from numpy import sin from numpy import sqrt -from numpy.typing import NDArray from scipy.interpolate import interp1d from gemseo import create_discipline @@ -87,6 +89,9 @@ from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.disciplines.remapping import RemappingDiscipline +if TYPE_CHECKING: + from numpy.typing import NDArray + _state_var_names = ("position", "velocity") _state_dot_var_names = ("position_dot", "velocity_dot") _time_vector = linspace(0.0, 10, 30) @@ -114,7 +119,7 @@ def generic_mass_rhs_function( right_stiff: The stiffness of the spring on the right-hand side of the mass. right_position: The position of the mass on the right-hand side of the current mass at `time`. - time_vector: The times at which `left_position` and `right_position` are computed. + time_vector: The times to compute `left_position` and `right_position` at. This parameter is only used if `left_position` and `right_position` are vectors. @@ -134,17 +139,15 @@ def generic_mass_rhs_function( assert time_vector.size == left_position.size interpolated_function = interp1d(time_vector, left_position, assume_sorted=True) left_position = interpolated_function(time) - return array( - [ - state[1], - ( - -(left_stiff + right_stiff) * state[0] - + left_stiff * left_position - + right_stiff * right_position - ) - / mass, - ] - ) + return array([ + state[1], + ( + -(left_stiff + right_stiff) * state[0] + + left_stiff * left_position + + right_stiff * right_position + ) + / mass, + ]) def create_mass_mdo_discipline( @@ -175,12 +178,11 @@ def create_mass_mdo_discipline( ) return position_dot, velocity_dot - mdo_discipline = create_discipline( + return create_discipline( "AutoPyDiscipline", py_func=_mass_rhs, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - return mdo_discipline def create_mass_ode_discipline( @@ -205,8 +207,8 @@ def create_mass_ode_discipline( right_stiff: The stiffness of the spring on the right-hand side. time_vector: The times at which the solution must be evaluated. state_var_names: The names of the state variables. - state_dot_var_names: The names of the derivatives of the state variables relative - to time. + state_dot_var_names: The names of the derivatives of the state variables + relative to time. Returns: The MDODiscipline describing a single point mass. @@ -215,22 +217,16 @@ def create_mass_ode_discipline( ode_solver_options = {} mass_discipline = create_mass_mdo_discipline(mass, left_stiff, right_stiff) - input_mapping = { - custom_name: default_name - for custom_name, default_name in zip(state_var_names, _state_var_names) - } + input_mapping = dict(zip(state_var_names, _state_var_names)) input_mapping["time"] = "time" - output_mapping = { - custom_name: default_name - for custom_name, default_name in zip(state_dot_var_names, _state_dot_var_names) - } + output_mapping = dict(zip(state_dot_var_names, _state_dot_var_names)) renamed_mass_discipline = RemappingDiscipline( mass_discipline, input_mapping, output_mapping, ) - ode_discipline = ODEDiscipline( + return ODEDiscipline( discipline=renamed_mass_discipline, state_var_names=state_var_names, state_dot_var_names=state_dot_var_names, @@ -239,7 +235,6 @@ def create_mass_ode_discipline( final_time=final_time, ode_solver_options=ode_solver_options, ) - return ode_discipline def make_rhs_function(left_stiff, right_stiff, mass, left_position, right_position): @@ -275,9 +270,11 @@ def create_chained_masses( time_vector: The times for which the problem should be solved. """ if len(masses) != len(stiffnesses) - 1: - raise ValueError("Stiffnesses and masses have incoherent lengths.") + msg = "Stiffnesses and masses have incoherent lengths." + raise ValueError(msg) if len(masses) != len(positions): - raise ValueError("Masses and positions have incoherent lengths.") + msg = "Masses and positions have incoherent lengths." + raise ValueError(msg) initial_time = min(time_vector) final_time = max(time_vector) @@ -314,10 +311,10 @@ def single_mass_exact_solution( mass: float, time: float | NDArray[float], ) -> float | NDArray[float]: - """Function describing the motion of the springs problem with a single mass. + r"""Function describing the motion of the springs problem with a single mass. - In this case, the equation describing the motion of the mass :math:`(m)` connected by - springs with stiffnesses :math:`k_1` and :math:`k_2` is: + In this case, the equation describing the motion of the mass :math:`(m)` connected + by springs with stiffnesses :math:`k_1` and :math:`k_2` is: .. math:: @@ -326,8 +323,8 @@ def single_mass_exact_solution( \\dot{y} &= \frac{k_1 + k_2}{m} x \\end{cases} \right. - If :math:`x(t=0) = x_0` and :math:`y(t=0) = y_0`, then the general expression for the - position :math:`x(t)` at time :math:`t` is + If :math:`x(t=0) = x_0` and :math:`y(t=0) = y_0`, then the general expression for + the position :math:`x(t)` at time :math:`t` is .. math:: @@ -348,7 +345,6 @@ def single_mass_exact_solution( Position of the mass at time. """ omega = sqrt((right_stiff + left_stiff) / mass) - position = (initial_position * cos(omega * time)) + ( + return (initial_position * cos(omega * time)) + ( initial_velocity / omega * sin(omega * time) ) - return position -- GitLab From b6910fa394a01aee304e6d86ffd14b5198fe6d48 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 15:55:19 +0100 Subject: [PATCH 195/237] Use a single `if` statement rather than a nested `if`. --- src/gemseo/algos/ode/ode_problem.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gemseo/algos/ode/ode_problem.py b/src/gemseo/algos/ode/ode_problem.py index 46df8f5732..7c9608cab0 100644 --- a/src/gemseo/algos/ode/ode_problem.py +++ b/src/gemseo/algos/ode/ode_problem.py @@ -115,9 +115,12 @@ class ODEProblem(BaseProblem): Raises: ValueError: If the state and time shapes are inconsistent. """ - if self.result.state_vector.size != 0: - if self.result.state_vector.shape[1] != self.result.time_vector.size: - raise ValueError("Inconsistent state and time shapes.") + if ( + self.result.state_vector.size != 0 + and self.result.state_vector.shape[1] != self.result.time_vector.size + ): + msg = "Inconsistent state and time shapes." + raise ValueError(msg) def _func(self, state) -> ndarray: return asarray(self.rhs_function(self.result.time_vector, state)) -- GitLab From 43f4c8ff71d6c24cea0998dada257e88047844c2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 15:59:49 +0100 Subject: [PATCH 196/237] Fix line length. --- src/gemseo/problems/ode/oscillator.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 5762187fed..67f1abf594 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -32,7 +32,8 @@ with :math:`\omega \in \mathbb{R}` has an analytical solution x(t) = \lambda \sin(\omega t) + \mu \cos(\omega t) -where :math:`\lambda` and :math:`\mu` are two constants defined by the initial conditions. +where :math:`\lambda` and :math:`\mu` are two constants defined by the initial +conditions. This problem can be re-written as a 2-dimensional first-order ordinary differential equation (ODE). @@ -56,15 +57,20 @@ The Jacobian for this ODE is 1 & 0 \end{pmatrix}. """ + from __future__ import annotations +from typing import TYPE_CHECKING + from numpy import array -from numpy.typing import NDArray from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +if TYPE_CHECKING: + from numpy.typing import NDArray + _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) @@ -122,7 +128,7 @@ def create_oscillator_ode_discipline( py_func=_rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - ode_discipline = ODEDiscipline( + return ODEDiscipline( discipline=oscillator, state_var_names=["position", "velocity"], initial_time=min(time_vector), @@ -130,4 +136,3 @@ def create_oscillator_ode_discipline( time_vector=time_vector, ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - return ode_discipline -- GitLab From 9c53b2856b457b642910262d2f18e28df7e68ab5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 16:02:02 +0100 Subject: [PATCH 197/237] Use `from` to distinguish raised exception from errors in exception handling. --- src/gemseo/disciplines/ode_discipline.py | 45 ++++++++++++------------ 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index ecc0676ad2..d602e5ad75 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -21,15 +21,15 @@ ODE stands for Ordinary Differential Equation. """ + from __future__ import annotations +from typing import TYPE_CHECKING from typing import Any -from typing import Mapping from numpy import array from numpy import concatenate from numpy import vsplit -from numpy.typing import NDArray from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory @@ -39,6 +39,11 @@ from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( ) from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array +if TYPE_CHECKING: + from collections.abc import Mapping + + from numpy.typing import NDArray + class ODEDiscipline(MDODiscipline): r"""A discipline based on the solution to an ODE. @@ -121,7 +126,7 @@ class ODEDiscipline(MDODiscipline): super().__init__(name=name, grammar_type=discipline.grammar_type) - self.output_grammar.update_from_names(self.state_var_names + ["time_vector"]) + self.output_grammar.update_from_names([*self.state_var_names, "time_vector"]) self.ode_factory = ODESolversFactory() self.__generator_default_inputs = {} @@ -134,8 +139,7 @@ class ODEDiscipline(MDODiscipline): self._init_ode_problem(initial_time, final_time) def __ode_func(self, time, state): - val = self.ode_mdo_func(concatenate((array([time]), state))) - return val + return self.ode_mdo_func(concatenate((array([time]), state))) def __ode_func_jac(self, time, state): return self.ode_mdo_func.jac(concatenate((array([time]), state))) @@ -147,9 +151,10 @@ class ODEDiscipline(MDODiscipline): self.discipline.default_inputs, names=self.state_var_names ) except KeyError as err: - raise ValueError(f"Missing default input {err} in discipline.") + msg = f"Missing default input {err} in discipline." + raise ValueError(msg) from err self.ode_mdo_func = MDODisciplineAdapterGenerator(self.discipline).get_function( - input_names=[self.time_var_name] + self.state_var_names, + input_names=[self.time_var_name, *self.state_var_names], output_names=self.state_dot_var_names, default_inputs=self.__generator_default_inputs, ) @@ -164,28 +169,22 @@ class ODEDiscipline(MDODiscipline): def _run(self) -> None: self.__generator_default_inputs.update(self.local_data) - if self.ode_solver_options is not None: - options = self.ode_solver_options - else: - options = {} + options = self.ode_solver_options if self.ode_solver_options is not None else {} ode_result = self.ode_factory.execute( self.ode_problem, self.ode_solver_name, **options ) if not ode_result.is_converged: - raise RuntimeError( - f"ODE solver {ode_result.solver_name} failed to converge. \ + msg = f"ODE solver {ode_result.solver_name} failed to converge. \ Message = {ode_result.solver_message}" - ) + raise RuntimeError(msg) n_dim_sol = ode_result.state_vector.shape[0] split_state = vsplit(ode_result.state_vector, n_dim_sol) - self.local_data.update( - { - name: val.flatten() - for name, val in zip( - self.state_var_names, - split_state, - ) - } - ) + self.local_data.update({ + name: val.flatten() + for name, val in zip( + self.state_var_names, + split_state, + ) + }) self.local_data["time_vector"] = self.time_vector -- GitLab From e403db06ae3a80581855d6881d62100b1b13c7d8 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 29 Feb 2024 16:12:16 +0100 Subject: [PATCH 198/237] Reformatting for ruff. --- .../basics/plot_oscillator_discipline.py | 8 ++- .../_examples/ode/plot_springs_discipline.py | 10 ++-- src/gemseo/problems/springs/__init__.py | 1 + tests/disciplines/test_ode_discipline.py | 14 +++-- tests/problems/ode/test_springs.py | 58 ++++++++++--------- 5 files changed, 51 insertions(+), 40 deletions(-) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index d202789bbf..b2d90f75d5 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -19,15 +19,17 @@ Execute an ODEDiscipline: a simple oscillator ============================================= """ + from __future__ import annotations import matplotlib.pyplot as plt -from gemseo import create_discipline +from numpy import array +from numpy import linspace + from gemseo import MDODiscipline +from gemseo import create_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline -from numpy import array -from numpy import linspace # %% # This tutorial describes how to use an :class:`ODEDiscipline` in |g|. diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index fd2ed93412..112f0cdcc1 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -19,17 +19,19 @@ Solve coupled ODEDisciplines : masses connected by springs ========================================================== """ + from __future__ import annotations -from gemseo import create_discipline +from matplotlib import pyplot as plt +from numpy import linspace +from numpy.random import default_rng as rand + from gemseo import MDODiscipline +from gemseo import create_discipline from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.springs.springs import create_chained_masses -from matplotlib import pyplot as plt -from numpy import linspace -from numpy.random import default_rng as rand # %% # diff --git a/src/gemseo/problems/springs/__init__.py b/src/gemseo/problems/springs/__init__.py index bee901460e..baf97034a2 100644 --- a/src/gemseo/problems/springs/__init__.py +++ b/src/gemseo/problems/springs/__init__.py @@ -17,4 +17,5 @@ # :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES """A discipline to describe the motion of a series of springs.""" + from __future__ import annotations diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index bd071de850..4e85613cb9 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -20,21 +20,23 @@ ODE stands for Ordinary Differential Equation. """ + from __future__ import annotations import pytest -from gemseo import create_discipline -from gemseo import MDODiscipline -from gemseo.disciplines.ode_discipline import ODEDiscipline -from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline -from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline -from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function from numpy import allclose from numpy import array from numpy import cos from numpy import linspace from numpy import sin +from gemseo import MDODiscipline +from gemseo import create_discipline +from gemseo.disciplines.ode_discipline import ODEDiscipline +from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline +from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline +from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function + def test_create_oscillator_ode_discipline() -> None: """Test the creation of an ODE Discipline.""" diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 587c7ba330..61ccde46a5 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -17,11 +17,18 @@ # :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES """Tests of the problem of masses connected by springs.""" + from __future__ import annotations +from typing import TYPE_CHECKING + import pytest -from gemseo import create_discipline +from numpy import allclose +from numpy import array +from numpy import linspace + from gemseo import MDODiscipline +from gemseo import create_discipline from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.chain import MDOChain @@ -33,14 +40,13 @@ from gemseo.problems.springs.springs import create_mass_ode_discipline from gemseo.problems.springs.springs import generic_mass_rhs_function from gemseo.problems.springs.springs import make_rhs_function from gemseo.problems.springs.springs import single_mass_exact_solution -from numpy import allclose -from numpy import array -from numpy import linspace -from numpy.typing import NDArray + +if TYPE_CHECKING: + from numpy.typing import NDArray @pytest.mark.parametrize( - "left_stiff, right_stiff, initial_position, initial_velocity, mass", + ("left_stiff", "right_stiff", "initial_position", "initial_velocity", "mass"), [ (1, 1, 0, 1, 1), (10, 1, 0, 1, 1), @@ -99,7 +105,7 @@ def test_generic_mass_rhs_function( result = ode_discipline.execute() def ode_rhs(time: float, state: NDArray[float]) -> NDArray[float]: - state_dot = generic_mass_rhs_function( + return generic_mass_rhs_function( time=time, state=state, mass=mass, @@ -109,7 +115,6 @@ def test_generic_mass_rhs_function( right_position=right_position, time_vector=time_vector, ) - return state_dot ode_problem = ODEProblem( ode_rhs, @@ -230,9 +235,12 @@ def test_2_chained_masses( disciplines = [ode_discipline_1, ode_discipline_2] mda = MDAGaussSeidel(disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) - assert sorted(mda.coupling_structure.strong_couplings) == sorted( - ["position_1", "position_2", "velocity_1", "velocity_2"] - ) + assert sorted(mda.coupling_structure.strong_couplings) == sorted([ + "position_1", + "position_2", + "velocity_1", + "velocity_2", + ]) discipline_result = mda.execute() mda.plot_residual_history(save=True) @@ -252,14 +260,12 @@ def test_2_chained_masses( ode_problem = ODEProblem( _ode_func, initial_time=min(time_vector), - initial_state=array( - [ - initial_position_1, - initial_velocity_1, - initial_position_2, - initial_velocity_2, - ] - ), + initial_state=array([ + initial_position_1, + initial_velocity_1, + initial_position_2, + initial_velocity_2, + ]), final_time=max(time_vector), time_vector=time_vector, ) @@ -373,14 +379,12 @@ def test_2_chained_masses_linear_coupling(): ode_problem = ODEProblem( _ode_func, initial_time=min(time_vector), - initial_state=array( - [ - initial_position_1, - initial_velocity_1, - initial_position_2, - initial_velocity_2, - ] - ), + initial_state=array([ + initial_position_1, + initial_velocity_1, + initial_position_2, + initial_velocity_2, + ]), final_time=max(time_vector), time_vector=time_vector, ) -- GitLab From 47fa795611aeaadf42c00379600714b2648066bc Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 1 Mar 2024 11:36:11 +0100 Subject: [PATCH 199/237] Import NDArray. --- src/gemseo/problems/ode/oscillator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 67f1abf594..29817fb2bc 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -60,17 +60,13 @@ The Jacobian for this ODE is from __future__ import annotations -from typing import TYPE_CHECKING - from numpy import array +from numpy.typing import NDArray # noqa: TCH002 from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline -if TYPE_CHECKING: - from numpy.typing import NDArray - _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) @@ -117,7 +113,7 @@ def create_oscillator_ode_discipline( time: NDArray[float] = _initial_time, position: NDArray[float] = _initial_position, velocity: NDArray[float] = _initial_velocity, - ): + ) -> tuple[NDArray[float], NDArray[float]]: position_dot, velocity_dot = oscillator_ode_rhs_function( time, position, velocity, omega ) -- GitLab From 96319118ef25f97b9b2b62e17e205840665d9cb3 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 1 Mar 2024 11:39:28 +0100 Subject: [PATCH 200/237] Handle ndarray typings. --- src/gemseo/problems/ode/oscillator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 29817fb2bc..98d248d6fc 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -61,6 +61,7 @@ The Jacobian for this ODE is from __future__ import annotations from numpy import array +from numpy import ndarray from numpy.typing import NDArray # noqa: TCH002 from gemseo import create_discipline @@ -110,10 +111,10 @@ def create_oscillator_ode_discipline( """ def _rhs_function( - time: NDArray[float] = _initial_time, - position: NDArray[float] = _initial_position, - velocity: NDArray[float] = _initial_velocity, - ) -> tuple[NDArray[float], NDArray[float]]: + time: ndarray = _initial_time, + position: ndarray = _initial_position, + velocity: ndarray = _initial_velocity, + ) -> tuple[ndarray, ndarray]: position_dot, velocity_dot = oscillator_ode_rhs_function( time, position, velocity, omega ) -- GitLab From ad57b38c7cedef828dd3ed5a9c235303e5d2adb4 Mon Sep 17 00:00:00 2001 From: Matthias De Lozzo Date: Tue, 26 Mar 2024 17:45:02 +0000 Subject: [PATCH 201/237] Apply 17 suggestion(s) to 3 file(s) --- .../basics/plot_oscillator_discipline.py | 49 ++++++++++--------- .../_examples/ode/plot_springs_discipline.py | 31 ++++++------ src/gemseo/core/discipline.py | 1 + 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index b2d90f75d5..3773dfbb48 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -32,10 +32,10 @@ from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline # %% -# This tutorial describes how to use an :class:`ODEDiscipline` in |g|. +# This tutorial describes how to use an :class:`.ODEDiscipline`. # -# ODE stands for Ordinary Differential Equation. An :class:`ODEDiscipline` is an -# :class:`MDODiscipline` that is defined using an ODE. +# An :class:`.ODEDiscipline` is an :class:`MDODiscipline` +# that is defined using an ordinary differential equation (ODE). # # To illustrate the basic usage of this feature, we use a simple oscillator problem. # @@ -51,7 +51,9 @@ from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline # # \frac{d^2x}{dt^2} = -\omega ^ 2\dot{x} # -# with :math:`\omega \in \mathbb{R}`. Let's re-write this equation as a 1st order ODE. +# with :math:`\omega \in \mathbb{R}`. +# As |g| cannot solve a second-order ODE, +# let's re-write this equation as a first-order ODE: # # ..math:: # @@ -62,7 +64,10 @@ from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline # # where :math:`x` is the position and :math:`y` is the velocity of the oscillator. # -# To use |g|, we can define the right-hand side term of this equation as follows: +# Then, +# we can define define the right-hand side (RHS) function +# :math:`(t,x(t),v(t))\mapsto (v(t),-\omega^2x(t))` +# as follows: omega = 4 initial_time = 0 @@ -70,7 +75,7 @@ initial_position = array([0]) initial_velocity = array([1]) -def oscillator_ode_rhs_function( +def rhs_function( time=initial_time, position=initial_position, velocity=initial_velocity ): position_dot = velocity @@ -84,18 +89,18 @@ def oscillator_ode_rhs_function( # # We want to solve the oscillator problem for a set of time values: -time_vector = linspace(0.0, 10, 30) +time = linspace(0.0, 10, 30) # %% # Step 2: Create a discipline # ........................... # -# Next, we create an MDO discipline that will be used to build the ODE discipline. +# Next, we create an :class:`.MDODiscipline` that will be used to build the :class:`.ODEDiscipline`: # mdo_discipline = create_discipline( "AutoPyDiscipline", - py_func=oscillator_ode_rhs_function, + py_func=rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) @@ -103,30 +108,30 @@ mdo_discipline = create_discipline( # Step 3: Create and solve the ODEDiscipline # .......................................... # -# The `state_var_names` are the names of the state parameters used as input for the -# `oscillator_ode_rhs_function`. These strings are used to create the grammar of the -# discipline. +# The ``state_variable_names`` are the names of the state parameters +# used as input for the ``rhs_function``. +# These strings are used to create the grammar of the :class:`.ODEDiscipline`. # state_var_names = ["position", "velocity"] ode_discipline = ODEDiscipline( discipline=mdo_discipline, - state_var_names=state_var_names, - initial_time=min(time_vector), - final_time=max(time_vector), - time_vector=time_vector, + state_var_names=state_variable_names, + initial_time=min(time), + final_time=max(time), + time_vector=time, ) -result = ode_discipline.execute() +local_data = ode_discipline.execute() # %% # Step 4: Visualize the result # ............................ # -for name in state_var_names: - plt.plot(time_vector, result[name], label=name) +for state_variable_names in state_variable_names: + plt.plot(time_vector, local_data[state_variable_name], label=state_variable_name) plt.show() @@ -135,6 +140,6 @@ plt.show() # -------- # The oscillator discipline is provided by |g| for direct use. -time_vector = linspace(0.0, 10, 30) -ode_disc = create_oscillator_ode_discipline(time_vector, omega=4) -out = ode_disc.execute() +time = linspace(0.0, 10, 30) +ode_discipline = create_oscillator_ode_discipline(time_vector, omega=4) +ode_discipline.execute() diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 112f0cdcc1..b47fe02d97 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -35,13 +35,13 @@ from gemseo.problems.springs.springs import create_chained_masses # %% # -# This tutorial describes how to use :class:`ODEDiscipline` with coupled variables in |g|. +# This tutorial describes how to use the :class:`.ODEDiscipline` with coupled variables. # # Problem description # ------------------- # # Consider a set of point masses with masses :math:`m_1,\ m_2,...\ m_n` connected by -# springs with stiffness :math:`k_1,\ k_2,...\ k_{n+1}`. The springs at each end of the +# springs with stiffnesses :math:`k_1,\ k_2,...\ k_{n+1}`. The springs at each end of the # system are connected to fixed points. We hereby study the response of the system to the # displacement of one of the point masses. # @@ -63,10 +63,10 @@ from gemseo.problems.springs.springs import create_chained_masses # # These equations are coupled, since the forces applied to any given mass depend on the # positions of its neighbors. In this tutorial, we will use the framework of the -# :class:`ODEDisciplines` to solve this set of coupled equations. +# :class:`.ODEDisciplines` to solve this set of coupled equations. # -# Using an :class:`ODEDiscipline` to solve the problem -# ---------------------------------------------------- +# Using an :class:`.ODEDiscipline` to solve the problem +# ----------------------------------------------------- # # Let's consider the problem described above in the case of two masses. First we describe # the right-hand side (RHS) of the equations of motion for each point mass. @@ -145,11 +145,8 @@ mdo_disciplines = [ ode_disciplines = [ ODEDiscipline( discipline=disc, - state_var_names=["position_" + str(i), "velocity_" + str(i)], - state_dot_var_names=[ - "position_" + str(i) + "_dot", - "velocity_" + str(i) + "_dot", - ], + state_var_names=[f"position_{i}", f"velocity_{i}"], + state_dot_var_names=[f"position_{i}_dot", f"velocity_{i}_dot"], time_vector=time_vector, initial_time=min(time_vector), final_time=max(time_vector), @@ -166,7 +163,7 @@ for disc in ode_disciplines: # We solve this list of disciplines using a Gauss-Seidel MDA. mda = MDAGaussSeidel(ode_disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) -output = mda.execute() +local_data = mda.execute() # %% # @@ -180,8 +177,8 @@ mda.plot_residual_history() # Plotting the solution # --------------------- -plt.plot(time_vector, output["position_0"], label="mass 0") -plt.plot(time_vector, output["position_1"], label="mass 1") +plt.plot(time_vector, local_data["position_0"], label="mass 0") +plt.plot(time_vector, local_data["position_1"], label="mass 1") plt.show() # %% @@ -204,7 +201,7 @@ plt.show() # :alt: Couple, then integrate. # # To do so, we can use the list of MDO disciplines we created earlier to define an -# :class:`MDOChain`. +# :class:`.MDOChain`. mda = MDOChain( mdo_disciplines, @@ -229,10 +226,10 @@ ode_discipline = ODEDiscipline( final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) -output = ode_discipline.execute() +local_data = ode_discipline.execute() -plt.plot(time_vector, output["position_0"], label="mass 0") -plt.plot(time_vector, output["position_1"], label="mass 1") +plt.plot(time_vector, local_data["position_0"], label="mass 0") +plt.plot(time_vector, local_data["position_1"], label="mass 1") plt.show() # %% diff --git a/src/gemseo/core/discipline.py b/src/gemseo/core/discipline.py index 4a651d41bb..779a2b40cc 100644 --- a/src/gemseo/core/discipline.py +++ b/src/gemseo/core/discipline.py @@ -277,6 +277,7 @@ class MDODiscipline(Serializable): self.data_processor = None self.input_grammar = None self.output_grammar = None + # Allow to re-execute the same discipline twice, only if did not fail # and not running self.re_exec_policy = self.ReExecutionPolicy.DONE -- GitLab From f4c6a3e3efb425fc3429db01269ba680a5a2c6f1 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 10:14:54 +0200 Subject: [PATCH 202/237] Add docstrings for arguments. --- src/gemseo/disciplines/ode_discipline.py | 25 +++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index d602e5ad75..2cf5876549 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -139,13 +139,36 @@ class ODEDiscipline(MDODiscipline): self._init_ode_problem(initial_time, final_time) def __ode_func(self, time, state): + """The RHS function :math:`f`. + + Args: + time: The time for which :math:`f` should be evaluated. + state: The state for which :math:`f` should be evaluated. + + Returns: + The value of :math:`f` at `time` and `state`. + """ return self.ode_mdo_func(concatenate((array([time]), state))) def __ode_func_jac(self, time, state): + """The function to compute the Jacobian of :math:`f`. + + Args: + time: The time for which :math:`f` should be evaluated. + state: The state for which :math:`f` should be evaluated. + + Returns: + The value of the Jacobian of :math:`f` at `time` and `state`. + """ return self.ode_mdo_func.jac(concatenate((array([time]), state))) def _init_ode_problem(self, initial_time: float, final_time: float) -> None: - """Initialize the ODE problem with the user-defined parameters.""" + """Initialize the ODE problem with the user-defined parameters. + + Args: + initial_time: The start of the time interval. + final_time: The end of the time interval. + """ try: initial_state = concatenate_dict_of_arrays_to_array( self.discipline.default_inputs, names=self.state_var_names -- GitLab From 90cdbd204eba7c42f34aa020e83e22fa1f63848b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 10:19:21 +0200 Subject: [PATCH 203/237] Add type hints. --- src/gemseo/disciplines/ode_discipline.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 2cf5876549..4c20d6e29e 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -44,6 +44,8 @@ if TYPE_CHECKING: from numpy.typing import NDArray + from gemseo.typing import NumberArray + class ODEDiscipline(MDODiscipline): r"""A discipline based on the solution to an ODE. @@ -81,7 +83,7 @@ class ODEDiscipline(MDODiscipline): def __init__( self, discipline: MDODiscipline, - time_vector: NDArray[float], + time_vector: NumberArray, state_var_names: list[str], state_dot_var_names: list[str] | None = None, time_var_name: str = "time", @@ -138,7 +140,7 @@ class ODEDiscipline(MDODiscipline): self._init_ode_problem(initial_time, final_time) - def __ode_func(self, time, state): + def __ode_func(self, time: float | NumberArray, state: NumberArray): """The RHS function :math:`f`. Args: @@ -150,7 +152,7 @@ class ODEDiscipline(MDODiscipline): """ return self.ode_mdo_func(concatenate((array([time]), state))) - def __ode_func_jac(self, time, state): + def __ode_func_jac(self, time: float | NumberArray, state: NumberArray): """The function to compute the Jacobian of :math:`f`. Args: -- GitLab From 6b92cece6c7e9868f1d482e08c9f01aef495eccc Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 10:21:37 +0200 Subject: [PATCH 204/237] Apply suggestions. --- src/gemseo/disciplines/ode_discipline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 4c20d6e29e..6d540fe0cd 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -17,7 +17,7 @@ # :author: Francois Gallard # :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES -"""A discipline for ODEs. +"""A discipline for solving ordinary differential equations (ODEs). ODE stands for Ordinary Differential Equation. """ @@ -48,7 +48,7 @@ if TYPE_CHECKING: class ODEDiscipline(MDODiscipline): - r"""A discipline based on the solution to an ODE. + r"""A discipline for solving an ordinary differential equations (ODE). ODE stands for Ordinary Differential Equation. """ @@ -90,7 +90,7 @@ class ODEDiscipline(MDODiscipline): initial_time: float = 0.0, final_time: float = 1.0, ode_solver_name: str = "RK45", - ode_solver_options: Mapping[str:Any] | None = None, + ode_solver_options: Mapping[str, Any] | None = None, name: str = "", ): """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. @@ -120,7 +120,7 @@ class ODEDiscipline(MDODiscipline): self.discipline = discipline self.time_var_name = time_var_name if state_dot_var_names is None: - self.state_dot_var_names = [e + "_dot" for e in state_var_names] + self.state_dot_var_names = [f"{e}_dot" for e in state_var_names] else: self.state_dot_var_names = state_dot_var_names -- GitLab From f4037a0f1aff8c7335ecb3b4a634beacbf427f18 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:12:31 +0200 Subject: [PATCH 205/237] Apply suggestions --- .../_examples/ode/plot_springs_discipline.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index b47fe02d97..6085f1026a 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -18,7 +18,7 @@ """ Solve coupled ODEDisciplines : masses connected by springs ========================================================== -""" +""" # noqa: 205, 212, 415 from __future__ import annotations @@ -55,12 +55,14 @@ from gemseo.problems.springs.springs import create_chained_masses # .. math:: # # \left\{ \begin{cases} -# \dot{x_i} &= y_i \\ -# \dot{y_i} &= +# \dot{x_i} &= v_i \\ +# \dot{v_i} &= # - \frac{k_i + k_{i+1}}{m_i}x_i # + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} # \end{cases} \right. # +# where :math:`x` is the position of the point mass and :math:`v` is its velocity. +# # These equations are coupled, since the forces applied to any given mass depend on the # positions of its neighbors. In this tutorial, we will use the framework of the # :class:`.ODEDisciplines` to solve this set of coupled equations. @@ -69,7 +71,7 @@ from gemseo.problems.springs.springs import create_chained_masses # ----------------------------------------------------- # # Let's consider the problem described above in the case of two masses. First we describe -# the right-hand side (RHS) of the equations of motion for each point mass. +# the right-hand side (RHS) function of the equations of motion for each point mass. stiff_0 = 1 stiff_1 = 1 @@ -155,12 +157,12 @@ ode_disciplines = [ for i, disc in enumerate(mdo_disciplines) ] -for disc in ode_disciplines: - disc.execute() +for ode_discipline in ode_disciplines: + ode_discipline.execute() # %% # -# We solve this list of disciplines using a Gauss-Seidel MDA. +# We apply an MDA with the Gauss-Seidel algorithm: mda = MDAGaussSeidel(ode_disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) local_data = mda.execute() @@ -257,7 +259,7 @@ def mass_rhs(): # # Shortcut # -------- -# The `springs.py` module provides a shortcut to access this problem. The user can define +# The :mod:`.springs` module provides a shortcut to access this problem. The user can define # a list of masses, stiffnesses and initial positions, then create all the disciplines # with a single call. # -- GitLab From 1b47bc7632fad97fbd2b5bbb1c1d15cf51f17170 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:16:39 +0200 Subject: [PATCH 206/237] Docstrings and type hints. --- doc_src/_examples/ode/plot_van_der_pol.py | 33 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/doc_src/_examples/ode/plot_van_der_pol.py b/doc_src/_examples/ode/plot_van_der_pol.py index 8d36d258b3..14a28710e4 100644 --- a/doc_src/_examples/ode/plot_van_der_pol.py +++ b/doc_src/_examples/ode/plot_van_der_pol.py @@ -18,10 +18,12 @@ """ Solve an ODE: the Van der Pol problem ===================================== -""" +""" # noqa: 205, 212, 415 from __future__ import annotations +from typing import TYPE_CHECKING + import matplotlib.pyplot as plt from numpy import array from numpy import zeros @@ -30,6 +32,9 @@ from gemseo.algos.ode.ode_problem import ODEProblem from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.problems.ode.van_der_pol import VanDerPol +if TYPE_CHECKING: + from gemseo.typing import NumberArray + # %% # This tutorial describes how to solve an ordinary differential equation (ODE) # problem with |g|. A first-order ODE is a differential equation that can be @@ -86,7 +91,16 @@ from gemseo.problems.ode.van_der_pol import VanDerPol mu = 5 -def evaluate_f(time, state): +def evaluate_f(time: float, state: NumberArray): + """Evaluate the right-hand side function :math:`f` of the equation. + + Args: + time: Time at which the Jacobian should be evaluated. + state: State for which the Jacobian should be evaluated. + + Returns: + The value of :math:`f` at `time` and `state`. + """ return state[1], mu * state[1] * (1 - state[0] ** 2) - state[0] @@ -103,7 +117,16 @@ ode_problem = ODEProblem(evaluate_f, initial_state, initial_time, final_time) # problem, this would be: -def evaluate_jac(time, state): +def evaluate_jac(time: float, state: NumberArray): + """Evaluate the Jacobian of the function :math:`f`. + + Args: + time: Time at which the Jacobian should be evaluated. + state: State for which the Jacobian should be evaluated. + + Returns: + The value of the Jacobian at `time` and `state`. + """ jac = zeros((2, 2)) jac[1, 0] = -mu * 2 * state[1] * state[0] - 1 jac[0, 1] = 1 @@ -111,7 +134,7 @@ def evaluate_jac(time, state): return jac -ode_problem = ODEProblem( +ode_problem_with_jacobian = ODEProblem( evaluate_f, initial_state, initial_time, final_time, jac=evaluate_jac ) @@ -121,7 +144,7 @@ ode_problem = ODEProblem( # # Whether the Jacobian is specified or not, once the problem is defined, the ODE # solver is called on the :class:`.ODEProblem` by using the :class:`.ODESolversFactory`: -ODESolversFactory().execute(ode_problem) +ODESolversFactory().execute(ode_problem_with_jacobian) # %% # By default, the Runge-Kutta method of order 4(5) (``"RK45"``) is used, but other -- GitLab From 151e952badcefe144a89b477803dc9a770b56e5f Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:18:48 +0200 Subject: [PATCH 207/237] Solve both problems. --- doc_src/_examples/ode/plot_van_der_pol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc_src/_examples/ode/plot_van_der_pol.py b/doc_src/_examples/ode/plot_van_der_pol.py index 14a28710e4..b8f50c4b73 100644 --- a/doc_src/_examples/ode/plot_van_der_pol.py +++ b/doc_src/_examples/ode/plot_van_der_pol.py @@ -144,6 +144,7 @@ ode_problem_with_jacobian = ODEProblem( # # Whether the Jacobian is specified or not, once the problem is defined, the ODE # solver is called on the :class:`.ODEProblem` by using the :class:`.ODESolversFactory`: +ODESolversFactory().execute(ode_problem) ODESolversFactory().execute(ode_problem_with_jacobian) # %% -- GitLab From e72f67378e924e3796c9ae3e44fd2e1efab313a9 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:24:42 +0200 Subject: [PATCH 208/237] Apply suggestions --- .../basics/plot_oscillator_discipline.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index 3773dfbb48..74b35075f6 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -18,7 +18,7 @@ """ Execute an ODEDiscipline: a simple oscillator ============================================= -""" +""" # noqa: 205, 212, 415 from __future__ import annotations @@ -34,7 +34,7 @@ from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline # %% # This tutorial describes how to use an :class:`.ODEDiscipline`. # -# An :class:`.ODEDiscipline` is an :class:`MDODiscipline` +# An :class:`.ODEDiscipline` is an :class:`MDODiscipline` # that is defined using an ordinary differential equation (ODE). # # To illustrate the basic usage of this feature, we use a simple oscillator problem. @@ -49,23 +49,23 @@ from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline # # ..math:: # -# \frac{d^2x}{dt^2} = -\omega ^ 2\dot{x} +# \frac{d^2x}{dt^2} = -\omega ^ 2\frac{dx}{dt} # -# with :math:`\omega \in \mathbb{R}`. +# with :math:`\omega \in \mathbb{R}`. # As |g| cannot solve a second-order ODE, # let's re-write this equation as a first-order ODE: # # ..math:: # # \left\{\begin{array} -# \dot(x) = y \\ -# \dot(y) = -\omega^2 x +# \frac{x}{t} = v \\ +# \frac{v}{t} = -\omega^2 x # \end{array}\right. # # where :math:`x` is the position and :math:`y` is the velocity of the oscillator. # -# Then, -# we can define define the right-hand side (RHS) function +# Then, +# we can define the right-hand side (RHS) function # :math:`(t,x(t),v(t))\mapsto (v(t),-\omega^2x(t))` # as follows: @@ -79,7 +79,7 @@ def rhs_function( time=initial_time, position=initial_position, velocity=initial_velocity ): position_dot = velocity - velocity_dot = -omega * position + velocity_dot = -(omega**2) * position return position_dot, velocity_dot @@ -89,7 +89,7 @@ def rhs_function( # # We want to solve the oscillator problem for a set of time values: -time = linspace(0.0, 10, 30) +time_vector = linspace(0.0, 10, 30) # %% # Step 2: Create a discipline @@ -108,19 +108,19 @@ mdo_discipline = create_discipline( # Step 3: Create and solve the ODEDiscipline # .......................................... # -# The ``state_variable_names`` are the names of the state parameters -# used as input for the ``rhs_function``. +# The ``state_variable_names`` are the names of the state parameters +# used as input for the ``rhs_function``. # These strings are used to create the grammar of the :class:`.ODEDiscipline`. # -state_var_names = ["position", "velocity"] +state_variable_names = ["position", "velocity"] ode_discipline = ODEDiscipline( discipline=mdo_discipline, state_var_names=state_variable_names, - initial_time=min(time), - final_time=max(time), - time_vector=time, + initial_time=min(time_vector), + final_time=max(time_vector), + time_vector=time_vector, ) local_data = ode_discipline.execute() @@ -130,8 +130,8 @@ local_data = ode_discipline.execute() # ............................ # -for state_variable_names in state_variable_names: - plt.plot(time_vector, local_data[state_variable_name], label=state_variable_name) +for variable_name in state_variable_names: + plt.plot(time_vector, local_data[variable_name], label=variable_name) plt.show() -- GitLab From 904351ba11108fd9848afad8014705a49dc1319e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:25:39 +0200 Subject: [PATCH 209/237] Type hints. --- .../disciplines/basics/plot_oscillator_discipline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index 74b35075f6..f852846807 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -22,6 +22,8 @@ Execute an ODEDiscipline: a simple oscillator from __future__ import annotations +from typing import TYPE_CHECKING + import matplotlib.pyplot as plt from numpy import array from numpy import linspace @@ -31,6 +33,9 @@ from gemseo import create_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline +if TYPE_CHECKING: + from gemseo.typing import NumberArray + # %% # This tutorial describes how to use an :class:`.ODEDiscipline`. # @@ -76,7 +81,9 @@ initial_velocity = array([1]) def rhs_function( - time=initial_time, position=initial_position, velocity=initial_velocity + time: float = initial_time, + position: NumberArray = initial_position, + velocity: NumberArray = initial_velocity, ): position_dot = velocity velocity_dot = -(omega**2) * position -- GitLab From 748fd84090279d654424b6a83e97e55367a43507 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:27:07 +0200 Subject: [PATCH 210/237] Fix typo. --- doc_src/_examples/ode/plot_van_der_pol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc_src/_examples/ode/plot_van_der_pol.py b/doc_src/_examples/ode/plot_van_der_pol.py index b8f50c4b73..4e071d0180 100644 --- a/doc_src/_examples/ode/plot_van_der_pol.py +++ b/doc_src/_examples/ode/plot_van_der_pol.py @@ -95,8 +95,8 @@ def evaluate_f(time: float, state: NumberArray): """Evaluate the right-hand side function :math:`f` of the equation. Args: - time: Time at which the Jacobian should be evaluated. - state: State for which the Jacobian should be evaluated. + time: Time at which :math:`f` should be evaluated. + state: State for which the :math:`f` should be evaluated. Returns: The value of :math:`f` at `time` and `state`. -- GitLab From f1edf5ec8ec8b6d423c3ce1a4baec466b6ae7b4d Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 14:29:15 +0200 Subject: [PATCH 211/237] Add docstring. --- .../disciplines/basics/plot_oscillator_discipline.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index f852846807..c2c28dbf6a 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -85,6 +85,16 @@ def rhs_function( position: NumberArray = initial_position, velocity: NumberArray = initial_velocity, ): + """Evaluate the right-hand side function :math:`f` of the equation. + + Args: + time: Time at which :math:`f` should be evaluated. + position: Position for which :math:`f` should be evaluated. + velocity: Velocity for which :math:`f` should be evaluated. + + Returns: + The value of :math:`f` at `time`, `position` and `velocity`. + """ position_dot = velocity velocity_dot = -(omega**2) * position return position_dot, velocity_dot -- GitLab From 02d6d38e12b67bd7e7fd607f31eac23fdfff1ecf Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 15:09:42 +0200 Subject: [PATCH 212/237] Switch to d/dt notation. --- doc_src/_examples/ode/plot_springs_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 6085f1026a..67e3606525 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -55,8 +55,8 @@ from gemseo.problems.springs.springs import create_chained_masses # .. math:: # # \left\{ \begin{cases} -# \dot{x_i} &= v_i \\ -# \dot{v_i} &= +# \frac{dx_i}{dt} &= v_i \\ +# \frac{dv_i}{dt} &= # - \frac{k_i + k_{i+1}}{m_i}x_i # + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} # \end{cases} \right. -- GitLab From a4447bb69c9ab9c513b93a32718ba61808768a55 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 4 Apr 2024 15:10:27 +0200 Subject: [PATCH 213/237] Fix typo. --- .../disciplines/basics/plot_oscillator_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index c2c28dbf6a..1630722bce 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -63,8 +63,8 @@ if TYPE_CHECKING: # ..math:: # # \left\{\begin{array} -# \frac{x}{t} = v \\ -# \frac{v}{t} = -\omega^2 x +# \frac{dx}{dt} = v \\ +# \frac{dv}{dt} = -\omega^2 x # \end{array}\right. # # where :math:`x` is the position and :math:`y` is the velocity of the oscillator. -- GitLab From 2e4afe72829c7d21f81f32cedc338f4a0418b608 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 Apr 2024 15:55:40 +0200 Subject: [PATCH 214/237] Move files to the _images directory. --- doc_src/_examples/ode/plot_springs_discipline.py | 6 +++--- .../ode/figs => _images/ode}/coupling.png | Bin .../{_examples/ode/figs => _images/ode}/springs.png | Bin .../ode/figs => _images/ode}/time_integration.png | Bin 4 files changed, 3 insertions(+), 3 deletions(-) rename doc_src/{_examples/ode/figs => _images/ode}/coupling.png (100%) rename doc_src/{_examples/ode/figs => _images/ode}/springs.png (100%) rename doc_src/{_examples/ode/figs => _images/ode}/time_integration.png (100%) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 67e3606525..749fef69e3 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -45,7 +45,7 @@ from gemseo.problems.springs.springs import create_chained_masses # system are connected to fixed points. We hereby study the response of the system to the # displacement of one of the point masses. # -# .. figure:: figs/springs.png +# .. figure:: ../_images/ode/springs.png # :width: 400 # :alt: Illustration of the springs-masses problem. # @@ -191,14 +191,14 @@ plt.show() # In the previous section, we considered the time-integration within each ODE discipline, # then coupled the disciplines, as illustrated in the next figure. # -# .. figure:: figs/coupling.png +# .. figure:: ../_images/ode/coupling.png # :width: 400 # :alt: Integrate, then couple. # # Another possibility to tackle this problem is to define the couplings within a # discipline, as illustrated in the next figure. # -# .. figure:: figs/time_integration.png +# .. figure:: ../_images/ode/time_integration.png # :width: 400 # :alt: Couple, then integrate. # diff --git a/doc_src/_examples/ode/figs/coupling.png b/doc_src/_images/ode/coupling.png similarity index 100% rename from doc_src/_examples/ode/figs/coupling.png rename to doc_src/_images/ode/coupling.png diff --git a/doc_src/_examples/ode/figs/springs.png b/doc_src/_images/ode/springs.png similarity index 100% rename from doc_src/_examples/ode/figs/springs.png rename to doc_src/_images/ode/springs.png diff --git a/doc_src/_examples/ode/figs/time_integration.png b/doc_src/_images/ode/time_integration.png similarity index 100% rename from doc_src/_examples/ode/figs/time_integration.png rename to doc_src/_images/ode/time_integration.png -- GitLab From 22bba1a215efd3c699d018f829a320d63ad2d9c9 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 Apr 2024 15:59:42 +0200 Subject: [PATCH 215/237] Remove redundant example. --- .../_examples/ode/plot_springs_discipline.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 749fef69e3..e6e739f3b5 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -234,26 +234,6 @@ plt.plot(time_vector, local_data["position_0"], label="mass 0") plt.plot(time_vector, local_data["position_1"], label="mass 1") plt.show() -# %% -# -# Solving the problem for a large number of point masses -# ------------------------------------------------------ -# -# We first set the values of the mass and stiffness of each spring, as well as the initial -# position of each point mass. - -masses = rand(3) -stiffnesses = rand(4) -positions = [1, 0, 0] - -# %% -# -# The motion of each point mass can be described by the same ODE. - - -def mass_rhs(): - pass - # %% # -- GitLab From 3e265d2861788d7cd693ce4729da509e15c771bc Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 9 Apr 2024 16:38:20 +0200 Subject: [PATCH 216/237] Add noqa instruction for flake. --- tests/disciplines/test_ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index 4e85613cb9..a1ff302281 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -55,7 +55,7 @@ def test_oscillator_ode_discipline() -> None: """Test an ODE Discipline representing a simple oscillator.""" time_vector = linspace(0.0, 10, 30) - def rhs_function(time=0, position=0, velocity=1): # default values are necessary ! + def rhs_function(time=0, position=0, velocity=1): # noqa: U100 position_dot = velocity velocity_dot = -4 * position return position_dot, velocity_dot -- GitLab From 21f32c694000965be773c517c4dfcd957e45450e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 19 Apr 2024 15:03:35 +0000 Subject: [PATCH 217/237] Apply 41 suggestion(s) to 4 file(s) Co-authored-by: Matthias De Lozzo --- .../basics/plot_oscillator_discipline.py | 38 ++++------- .../_examples/ode/plot_springs_discipline.py | 68 ++++++++----------- src/gemseo/disciplines/ode_discipline.py | 25 +++---- src/gemseo/problems/springs/springs.py | 2 +- 4 files changed, 53 insertions(+), 80 deletions(-) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py index 1630722bce..6a6e36f8f1 100644 --- a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py @@ -16,9 +16,9 @@ # Contributors: # Isabelle Santos """ -Execute an ODEDiscipline: a simple oscillator -============================================= -""" # noqa: 205, 212, 415 +Create a discipline that solves an ODE +====================================== +""" from __future__ import annotations @@ -40,7 +40,7 @@ if TYPE_CHECKING: # This tutorial describes how to use an :class:`.ODEDiscipline`. # # An :class:`.ODEDiscipline` is an :class:`MDODiscipline` -# that is defined using an ordinary differential equation (ODE). +# that solves an ordinary differential equation (ODE). # # To illustrate the basic usage of this feature, we use a simple oscillator problem. # @@ -67,7 +67,7 @@ if TYPE_CHECKING: # \frac{dv}{dt} = -\omega^2 x # \end{array}\right. # -# where :math:`x` is the position and :math:`y` is the velocity of the oscillator. +# where :math:`x` is the position and :math:`v` is the velocity of the oscillator. # # Then, # we can define the right-hand side (RHS) function @@ -85,12 +85,12 @@ def rhs_function( position: NumberArray = initial_position, velocity: NumberArray = initial_velocity, ): - """Evaluate the right-hand side function :math:`f` of the equation. + """Evaluate the RHS function :math:`f` of the equation. Args: - time: Time at which :math:`f` should be evaluated. - position: Position for which :math:`f` should be evaluated. - velocity: Velocity for which :math:`f` should be evaluated. + time: The time for which :math:`f` should be evaluated. + position: The position for which :math:`f` should be evaluated. + velocity: The velocity for which :math:`f` should be evaluated. Returns: The value of :math:`f` at `time`, `position` and `velocity`. @@ -101,8 +101,10 @@ def rhs_function( # %% -# The first parameter of this function should be the time, and the following parameters -# should designate the state of the system being described. +# .. note:: +# +# The first parameter of an RHS function must be the time, +# and the others must be the state of the system at this time. # # We want to solve the oscillator problem for a set of time values: @@ -111,9 +113,7 @@ time_vector = linspace(0.0, 10, 30) # %% # Step 2: Create a discipline # ........................... -# # Next, we create an :class:`.MDODiscipline` that will be used to build the :class:`.ODEDiscipline`: -# mdo_discipline = create_discipline( "AutoPyDiscipline", @@ -124,13 +124,9 @@ mdo_discipline = create_discipline( # %% # Step 3: Create and solve the ODEDiscipline # .......................................... -# # The ``state_variable_names`` are the names of the state parameters # used as input for the ``rhs_function``. # These strings are used to create the grammar of the :class:`.ODEDiscipline`. -# - - state_variable_names = ["position", "velocity"] ode_discipline = ODEDiscipline( discipline=mdo_discipline, @@ -145,10 +141,8 @@ local_data = ode_discipline.execute() # %% # Step 4: Visualize the result # ............................ -# - -for variable_name in state_variable_names: - plt.plot(time_vector, local_data[variable_name], label=variable_name) +for state_variable_name in state_variable_names: + plt.plot(time_vector, local_data[state_variable_name], label=state_variable_name) plt.show() @@ -156,7 +150,5 @@ plt.show() # Shortcut # -------- # The oscillator discipline is provided by |g| for direct use. - -time = linspace(0.0, 10, 30) ode_discipline = create_oscillator_ode_discipline(time_vector, omega=4) ode_discipline.execute() diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index e6e739f3b5..f2060ebb4f 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -16,9 +16,9 @@ # Contributors: # Isabelle Santos """ -Solve coupled ODEDisciplines : masses connected by springs -========================================================== -""" # noqa: 205, 212, 415 +Solve a system of coupled ODEs +============================== +""" from __future__ import annotations @@ -34,7 +34,6 @@ from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.springs.springs import create_chained_masses # %% -# # This tutorial describes how to use the :class:`.ODEDiscipline` with coupled variables. # # Problem description @@ -49,8 +48,8 @@ from gemseo.problems.springs.springs import create_chained_masses # :width: 400 # :alt: Illustration of the springs-masses problem. # -# The motion of each point mass in this system is described by the following set of -# equations: +# The motion of each point mass in this system is described +# by the following set of ordinary differential equations (ODEs): # # .. math:: # @@ -61,7 +60,8 @@ from gemseo.problems.springs.springs import create_chained_masses # + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} # \end{cases} \right. # -# where :math:`x` is the position of the point mass and :math:`v` is its velocity. +# where :math:`x_i` is the position of the :math:`i`-th point mass +# and :math:`v_i` is its velocity. # # These equations are coupled, since the forces applied to any given mass depend on the # positions of its neighbors. In this tutorial, we will use the framework of the @@ -82,7 +82,9 @@ initial_position_0 = 1 initial_position_1 = 0 initial_velocity_0 = 0 initial_velocity_1 = 0 -time_vector = linspace(0, 1, 30) # Vector of times at which to solve the problem. + +# Vector of times at which to solve the problem. +time_vector = linspace(0, 1, 30) def mass_0_rhs( @@ -91,17 +93,17 @@ def mass_0_rhs( velocity_0=initial_velocity_0, position_1=initial_position_1, ): - """Function describing the equation of motion for the first point mass. + """Compute the RHS of the ODE associated with the first point mass. Args: - time: - position_0: Position of the first point mass. - velocity_0: Velocity of the first point mass. - position_1: Position of the second point mass. + time: The time at which to evaluate the RHS. + position_0: The position of the first point mass at this time. + velocity_0: The velocity of the first point mass at this time. + position_1: The position of the second point mass at this time. Returns: - position_0_dot: The derivative of `position_0` - velocity_0_dot: The derivative of `velocity_0` + The first- and second-order derivatives of the position + of the first point mass. """ position_0_dot = velocity_0 velocity_0_dot = (-(stiff_0 + stiff_1) * position_0 + stiff_1 * position_1) / mass_0 @@ -114,17 +116,17 @@ def mass_1_rhs( velocity_1=initial_velocity_1, position_0=initial_position_0, ): - """Function describing the equation of motion for the second point mass. + """Compute the RHS of the ODE associated with the secondpoint mass. Args: - time: - position_1: Position of the second point mass. - velocity_1: Velocity of the second point mass. - position_0: Position of the first point mass. + time: The time at which to evaluate the RHS. + position_1: The position of the second point mass at this time. + velocity_1: The velocity of the second point mass at this time. + position_0: The position of the first point mass at this time. Returns: - position_1_dot: The derivative of `position_1` - velocity_1_dot: The derivative of `velocity_1` + The first- and second-order derivatives of the position + of the second point mass. """ position_1_dot = velocity_1 velocity_1_dot = (-(stiff_1 + stiff_2) * position_1 + stiff_1 * position_0) / mass_1 @@ -132,21 +134,20 @@ def mass_1_rhs( # %% -# # We can then create a list of :class:`ODEDiscipline` objects # mdo_disciplines = [ create_discipline( "AutoPyDiscipline", - py_func=func, + py_func=compute_rhs, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - for func in [mass_0_rhs, mass_1_rhs] + for compute_rhs in [mass_0_rhs, mass_1_rhs] ] ode_disciplines = [ ODEDiscipline( - discipline=disc, + discipline=rhs_discipline, state_var_names=[f"position_{i}", f"velocity_{i}"], state_dot_var_names=[f"position_{i}_dot", f"velocity_{i}_dot"], time_vector=time_vector, @@ -154,37 +155,30 @@ ode_disciplines = [ final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - for i, disc in enumerate(mdo_disciplines) + for i, rhs_discipline in enumerate(mdo_disciplines) ] - for ode_discipline in ode_disciplines: ode_discipline.execute() # %% -# # We apply an MDA with the Gauss-Seidel algorithm: - mda = MDAGaussSeidel(ode_disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE) local_data = mda.execute() # %% -# # We can plot the residuals of this MDA. mda.plot_residual_history() # %% -# # Plotting the solution # --------------------- - plt.plot(time_vector, local_data["position_0"], label="mass 0") plt.plot(time_vector, local_data["position_1"], label="mass 1") plt.show() # %% -# # Another formulation # ------------------- # @@ -202,7 +196,7 @@ plt.show() # :width: 400 # :alt: Couple, then integrate. # -# To do so, we can use the list of MDO disciplines we created earlier to define an +# To do so, we can use the RHS disciplines we created earlier to define an # :class:`.MDOChain`. mda = MDOChain( @@ -211,7 +205,6 @@ mda = MDOChain( ) # %% -# # We then define the ODE discipline that contains the couplings and execute it. ode_discipline = ODEDiscipline( @@ -236,15 +229,12 @@ plt.show() # %% -# # Shortcut # -------- # The :mod:`.springs` module provides a shortcut to access this problem. The user can define # a list of masses, stiffnesses and initial positions, then create all the disciplines # with a single call. # - - masses = list(rand(3)) stiffnesses = list(rand(4)) positions = [1, 0, 0] diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 6d540fe0cd..16a13222b6 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -17,10 +17,7 @@ # :author: Francois Gallard # :author: Isabelle Santos # OTHER AUTHORS - MACROSCOPIC CHANGES -"""A discipline for solving ordinary differential equations (ODEs). - -ODE stands for Ordinary Differential Equation. -""" +"""A discipline for solving ordinary differential equations (ODEs).""" from __future__ import annotations @@ -48,10 +45,7 @@ if TYPE_CHECKING: class ODEDiscipline(MDODiscipline): - r"""A discipline for solving an ordinary differential equations (ODE). - - ODE stands for Ordinary Differential Equation. - """ + """A discipline for solving an ordinary differential equations (ODE).""" time_var_name: str """The name of the time variable.""" @@ -84,19 +78,16 @@ class ODEDiscipline(MDODiscipline): self, discipline: MDODiscipline, time_vector: NumberArray, - state_var_names: list[str], - state_dot_var_names: list[str] | None = None, + state_var_names: Sequence[str], + state_dot_var_names: Sequence[str] = (), time_var_name: str = "time", initial_time: float = 0.0, final_time: float = 1.0, ode_solver_name: str = "RK45", - ode_solver_options: Mapping[str, Any] | None = None, + ode_solver_options: Mapping[str, Any] = READ_ONLY_EMPTY_DICT, name: str = "", ): - """Initialize an ODEDiscipline containing an ODE problem and an MDODiscipline. - - Either the ode_problem or the discipline must be passed. - + """ Args: discipline: The discipline. state_var_names: The names of the state variables. @@ -206,8 +197,8 @@ class ODEDiscipline(MDODiscipline): n_dim_sol = ode_result.state_vector.shape[0] split_state = vsplit(ode_result.state_vector, n_dim_sol) self.local_data.update({ - name: val.flatten() - for name, val in zip( + state_var_name: state_var_value.flatten() + for state_var_name, state_var_value in zip( self.state_var_names, split_state, ) diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/springs/springs.py index df2daf010c..6b964f6137 100644 --- a/src/gemseo/problems/springs/springs.py +++ b/src/gemseo/problems/springs/springs.py @@ -60,7 +60,7 @@ Newton's law applied to any point mass :math:`m_i` can be written as = k_i x_{i-1} + k_{i+1} x_{i+1} - (k_i + k_{i+1}) x_i -This can be re-written as a système of 1st order ordinary differential equations: +This can be re-written as a system of first-order ordinary differential equations: .. math:: -- GitLab From bb7b5558241acd472cb3844e0a6d4145b1cd065b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 19 Apr 2024 17:10:30 +0200 Subject: [PATCH 218/237] Add useful imports. --- src/gemseo/disciplines/ode_discipline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 16a13222b6..3d4acd04bb 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -34,10 +34,12 @@ from gemseo.core.discipline import MDODiscipline from gemseo.core.mdofunctions.mdo_discipline_adapter_generator import ( MDODisciplineAdapterGenerator, ) +from gemseo.utils.constants import READ_ONLY_EMPTY_DICT from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array if TYPE_CHECKING: from collections.abc import Mapping + from collections.abc import Sequence from numpy.typing import NDArray @@ -87,7 +89,8 @@ class ODEDiscipline(MDODiscipline): ode_solver_options: Mapping[str, Any] = READ_ONLY_EMPTY_DICT, name: str = "", ): - """ + """Create ODEDiscipline. + Args: discipline: The discipline. state_var_names: The names of the state variables. -- GitLab From 1b6fe74e8b7c1225d3a84df7fffee0eebe4e959e Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Fri, 19 Apr 2024 18:11:11 +0200 Subject: [PATCH 219/237] Move springs problem --- .../_examples/ode/plot_springs_discipline.py | 12 +++++------ .../problems/{springs => ode}/springs.py | 0 src/gemseo/problems/springs/__init__.py | 21 ------------------- tests/problems/ode/test_springs.py | 12 +++++------ 4 files changed, 12 insertions(+), 33 deletions(-) rename src/gemseo/problems/{springs => ode}/springs.py (100%) delete mode 100644 src/gemseo/problems/springs/__init__.py diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index f2060ebb4f..f6b61344a1 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -31,7 +31,7 @@ from gemseo import create_discipline from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel -from gemseo.problems.springs.springs import create_chained_masses +from gemseo.problems.ode.springs import create_chained_masses # %% # This tutorial describes how to use the :class:`.ODEDiscipline` with coupled variables. @@ -48,7 +48,7 @@ from gemseo.problems.springs.springs import create_chained_masses # :width: 400 # :alt: Illustration of the springs-masses problem. # -# The motion of each point mass in this system is described +# The motion of each point mass in this system is described # by the following set of ordinary differential equations (ODEs): # # .. math:: @@ -60,7 +60,7 @@ from gemseo.problems.springs.springs import create_chained_masses # + \frac{k_i}{m_i}x_{i-1} + \frac{k_{i+1}}{m_i}x_{i+1} # \end{cases} \right. # -# where :math:`x_i` is the position of the :math:`i`-th point mass +# where :math:`x_i` is the position of the :math:`i`-th point mass # and :math:`v_i` is its velocity. # # These equations are coupled, since the forces applied to any given mass depend on the @@ -84,7 +84,7 @@ initial_velocity_0 = 0 initial_velocity_1 = 0 # Vector of times at which to solve the problem. -time_vector = linspace(0, 1, 30) +time_vector = linspace(0, 1, 30) def mass_0_rhs( @@ -102,7 +102,7 @@ def mass_0_rhs( position_1: The position of the second point mass at this time. Returns: - The first- and second-order derivatives of the position + The first- and second-order derivatives of the position of the first point mass. """ position_0_dot = velocity_0 @@ -125,7 +125,7 @@ def mass_1_rhs( position_0: The position of the first point mass at this time. Returns: - The first- and second-order derivatives of the position + The first- and second-order derivatives of the position of the second point mass. """ position_1_dot = velocity_1 diff --git a/src/gemseo/problems/springs/springs.py b/src/gemseo/problems/ode/springs.py similarity index 100% rename from src/gemseo/problems/springs/springs.py rename to src/gemseo/problems/ode/springs.py diff --git a/src/gemseo/problems/springs/__init__.py b/src/gemseo/problems/springs/__init__.py deleted file mode 100644 index baf97034a2..0000000000 --- a/src/gemseo/problems/springs/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# Contributors: -# INITIAL AUTHORS - API and implementation and/or documentation -# :author: Isabelle Santos -# OTHER AUTHORS - MACROSCOPIC CHANGES -"""A discipline to describe the motion of a series of springs.""" - -from __future__ import annotations diff --git a/tests/problems/ode/test_springs.py b/tests/problems/ode/test_springs.py index 61ccde46a5..a43db914f2 100644 --- a/tests/problems/ode/test_springs.py +++ b/tests/problems/ode/test_springs.py @@ -34,12 +34,12 @@ from gemseo.algos.ode.ode_solvers_factory import ODESolversFactory from gemseo.core.chain import MDOChain from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.mda.gauss_seidel import MDAGaussSeidel -from gemseo.problems.springs.springs import create_chained_masses -from gemseo.problems.springs.springs import create_mass_mdo_discipline -from gemseo.problems.springs.springs import create_mass_ode_discipline -from gemseo.problems.springs.springs import generic_mass_rhs_function -from gemseo.problems.springs.springs import make_rhs_function -from gemseo.problems.springs.springs import single_mass_exact_solution +from gemseo.problems.ode.springs import create_chained_masses +from gemseo.problems.ode.springs import create_mass_mdo_discipline +from gemseo.problems.ode.springs import create_mass_ode_discipline +from gemseo.problems.ode.springs import generic_mass_rhs_function +from gemseo.problems.ode.springs import make_rhs_function +from gemseo.problems.ode.springs import single_mass_exact_solution if TYPE_CHECKING: from numpy.typing import NDArray -- GitLab From 460a4328e947502ce18f1f258452851e447ec1f2 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 Apr 2024 09:15:56 +0200 Subject: [PATCH 220/237] Type hints. --- src/gemseo/problems/ode/oscillator.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index 98d248d6fc..a9fddbe9fc 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -60,14 +60,18 @@ The Jacobian for this ODE is from __future__ import annotations +from typing import TYPE_CHECKING + from numpy import array from numpy import ndarray -from numpy.typing import NDArray # noqa: TCH002 from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline from gemseo.disciplines.ode_discipline import ODEDiscipline +if TYPE_CHECKING: + from gemseo.typing import NumberArray + _initial_time = array([0.0]) _initial_position = array([0.0]) _initial_velocity = array([1.0]) @@ -75,11 +79,11 @@ _omega = 4 def oscillator_ode_rhs_function( - time: NDArray[float] = _initial_time, - position: NDArray[float] = _initial_position, - velocity: NDArray[float] = _initial_velocity, + time: NumberArray = _initial_time, + position: NumberArray = _initial_position, + velocity: NumberArray = _initial_velocity, omega: float = _omega, -) -> (NDArray[float], NDArray[float]): # noqa:U100 +) -> (NumberArray, NumberArray): # noqa:U100 """Right-hand side of the oscillator ODE problem. Args: @@ -98,7 +102,7 @@ def oscillator_ode_rhs_function( def create_oscillator_ode_discipline( - time_vector: NDArray[float], omega: float = _omega + time_vector: NumberArray, omega: float = _omega ) -> ODEDiscipline: """Create the ODE discipline for solving the oscillator problem. -- GitLab From dbc83966c08e09b849e599a36952d82ceb69b0bc Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 Apr 2024 09:19:04 +0200 Subject: [PATCH 221/237] Type hints. --- src/gemseo/algos/ode/ode_solver_lib.py | 5 ++--- src/gemseo/problems/ode/orbital_dynamics.py | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/gemseo/algos/ode/ode_solver_lib.py b/src/gemseo/algos/ode/ode_solver_lib.py index 3c85244d8d..9ac7888afa 100644 --- a/src/gemseo/algos/ode/ode_solver_lib.py +++ b/src/gemseo/algos/ode/ode_solver_lib.py @@ -28,9 +28,8 @@ from gemseo.algos.algorithm_library import AlgorithmDescription from gemseo.algos.algorithm_library import AlgorithmLibrary if TYPE_CHECKING: - from numpy.typing import NDArray - from gemseo.algos.ode.ode_problem import ODEProblem + from gemseo.typing import NumberArray LOGGER = logging.getLogger(__name__) @@ -57,7 +56,7 @@ class ODESolverLib(AlgorithmLibrary): self, problem: ODEProblem, algo_name: str, - result: NDArray[float], + result: NumberArray, **options: Any, ) -> None: # noqa: D107 if not self.problem.result.is_converged: diff --git a/src/gemseo/problems/ode/orbital_dynamics.py b/src/gemseo/problems/ode/orbital_dynamics.py index bc13b560f1..fb7cff0b03 100644 --- a/src/gemseo/problems/ode/orbital_dynamics.py +++ b/src/gemseo/problems/ode/orbital_dynamics.py @@ -85,7 +85,7 @@ initial_velocity_y = ( ) if TYPE_CHECKING: - from numpy.typing import NDArray + from gemseo.typing import NumberArray def _compute_rhs( @@ -107,7 +107,7 @@ def _compute_rhs( return position_x_dot, position_y_dot, velocity_x_dot, velocity_y_dot -def _compute_rhs_jacobian(time: float, state: NDArray[float]) -> NDArray[float]: # noqa:U100 +def _compute_rhs_jacobian(time: float, state: NumberArray) -> NumberArray: # noqa:U100 """Compute the Jacobian of the right-hand side of the ODE.""" x, y, _, _ = state jac = zeros((4, 4)) @@ -127,7 +127,7 @@ class OrbitalDynamics(ODEProblem): self, eccentricity: float = 0.5, use_jacobian: bool = True, - state_vector: NDArray[float] | None = None, + state_vector: NumberArray | None = None, ) -> None: r""" Args: @@ -162,7 +162,7 @@ class OrbitalDynamics(ODEProblem): ) -def create_orbital_discipline(time_vector: NDArray[float]) -> ODEDiscipline: +def create_orbital_discipline(time_vector: NumberArray) -> ODEDiscipline: """Create an ODE Discipline for the Orbital problem.""" mdo_discipline = create_discipline( "AutoPyDiscipline", -- GitLab From 05776bce73bac3a87fef2febfdb8c68ee3ed8e14 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 Apr 2024 15:26:40 +0200 Subject: [PATCH 222/237] Create a discipline deriving from ODEDiscipline rather than a function --- src/gemseo/problems/ode/oscillator.py | 105 ++++++++++++-------------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/src/gemseo/problems/ode/oscillator.py b/src/gemseo/problems/ode/oscillator.py index a9fddbe9fc..f5b860a32a 100644 --- a/src/gemseo/problems/ode/oscillator.py +++ b/src/gemseo/problems/ode/oscillator.py @@ -63,7 +63,6 @@ from __future__ import annotations from typing import TYPE_CHECKING from numpy import array -from numpy import ndarray from gemseo import create_discipline from gemseo.core.discipline import MDODiscipline @@ -78,62 +77,52 @@ _initial_velocity = array([1.0]) _omega = 4 -def oscillator_ode_rhs_function( - time: NumberArray = _initial_time, - position: NumberArray = _initial_position, - velocity: NumberArray = _initial_velocity, - omega: float = _omega, -) -> (NumberArray, NumberArray): # noqa:U100 - """Right-hand side of the oscillator ODE problem. - - Args: - time: The value of the time. - position: The position of the system at `time`. - velocity: The value of the first derivative of the position at `time`. - omega: Period squared of the oscillator. - - Returns: - The derivative of the position at `time`. - The derivative of the velocity at `time`. - """ - position_dot = velocity - velocity_dot = -omega * position - return position_dot, velocity_dot - - -def create_oscillator_ode_discipline( - time_vector: NumberArray, omega: float = _omega -) -> ODEDiscipline: - """Create the ODE discipline for solving the oscillator problem. - - Args: - time_vector: The vector of times at which to solve the problem. - omega: Period squared of the oscillator. - - Returns: - The ODEDiscipline representing the oscillator. - """ - - def _rhs_function( - time: ndarray = _initial_time, - position: ndarray = _initial_position, - velocity: ndarray = _initial_velocity, - ) -> tuple[ndarray, ndarray]: - position_dot, velocity_dot = oscillator_ode_rhs_function( - time, position, velocity, omega +class OscillatorDiscipline(ODEDiscipline): + """A discipline representing a simple oscillator.""" + + omega: float + """Period squared of the oscillator.""" + + def __init__(self, omega: float, time_vector: NumberArray): + """Create an OscillatorDiscipline. + + Args: + omega: The period squared of the oscillator. + time_vector: The times at which the solution is evaluated. + """ + self.omega = omega + + super().__init__( + time_vector=time_vector, + state_var_names=["position", "velocity"], + state_dot_var_names=["position_dot", "velocity_dot"], + discipline=create_discipline( + "AutoPyDiscipline", + py_func=self._compute_oscillator_rhs, + grammar_type=MDODiscipline.GrammarType.SIMPLE, + ), + initial_time=min(time_vector), + final_time=max(time_vector), + ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - return position_dot, velocity_dot - oscillator = create_discipline( - "AutoPyDiscipline", - py_func=_rhs_function, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - return ODEDiscipline( - discipline=oscillator, - state_var_names=["position", "velocity"], - initial_time=min(time_vector), - final_time=max(time_vector), - time_vector=time_vector, - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, - ) + def _compute_oscillator_rhs( + self, + time: NumberArray = _initial_time, + position: NumberArray = _initial_position, + velocity: NumberArray = _initial_velocity, + ) -> tuple[NumberArray, NumberArray]: + """Right-hand side of the oscillator ODE problem. + + Args: + time: The value of the time. + position: The position of the system at `time`. + velocity: The value of the first derivative of the position at `time`. + + Returns: + The derivative of the position at `time`. + The derivative of the velocity at `time`. + """ + position_dot = velocity + velocity_dot = -self.omega * position + return position_dot, velocity_dot -- GitLab From 31fbc157bda4306a46be6198922c41a29702ebd0 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 Apr 2024 16:49:42 +0200 Subject: [PATCH 223/237] Test discipline deriving from ODEDiscipline rather than a function --- tests/disciplines/test_ode_discipline.py | 33 ++++++------------------ 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/tests/disciplines/test_ode_discipline.py b/tests/disciplines/test_ode_discipline.py index a1ff302281..db46509da6 100644 --- a/tests/disciplines/test_ode_discipline.py +++ b/tests/disciplines/test_ode_discipline.py @@ -34,14 +34,13 @@ from gemseo import MDODiscipline from gemseo import create_discipline from gemseo.disciplines.ode_discipline import ODEDiscipline from gemseo.problems.ode.orbital_dynamics import create_orbital_discipline -from gemseo.problems.ode.oscillator import create_oscillator_ode_discipline -from gemseo.problems.ode.oscillator import oscillator_ode_rhs_function +from gemseo.problems.ode.oscillator import OscillatorDiscipline def test_create_oscillator_ode_discipline() -> None: """Test the creation of an ODE Discipline.""" time_vector = linspace(0.0, 10, 30) - ode_disc = create_oscillator_ode_discipline(time_vector, omega=4) + ode_disc = OscillatorDiscipline(time_vector=time_vector, omega=4) out = ode_disc.execute() analytical_position = sin(2 * time_vector) / 2 @@ -54,28 +53,13 @@ def test_create_oscillator_ode_discipline() -> None: def test_oscillator_ode_discipline() -> None: """Test an ODE Discipline representing a simple oscillator.""" time_vector = linspace(0.0, 10, 30) - - def rhs_function(time=0, position=0, velocity=1): # noqa: U100 - position_dot = velocity - velocity_dot = -4 * position - return position_dot, velocity_dot - - mdo_discipline = create_discipline( - "AutoPyDiscipline", - py_func=rhs_function, - grammar_type=MDODiscipline.GrammarType.SIMPLE, - ) - ode_discipline = ODEDiscipline( - discipline=mdo_discipline, - state_var_names=["position", "velocity"], + oscillator_discipline = OscillatorDiscipline( + omega=4, time_vector=time_vector, - initial_time=min(time_vector), - final_time=max(time_vector), - ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - assert ode_discipline is not None + assert oscillator_discipline is not None - out = ode_discipline.execute() + out = oscillator_discipline.execute() analytical_position = sin(2 * time_vector) / 2 assert allclose(out["position"], analytical_position) analytical_velocity = cos(2 * time_vector) @@ -100,9 +84,8 @@ def test_ode_discipline_bad_grammar() -> None: position=_initial_position, velocity=_initial_velocity, ): - position_dot, velocity_dot = oscillator_ode_rhs_function( - time, position, velocity, 4 - ) + position_dot = velocity + velocity_dot = -4 * position return position_dot, velocity_dot oscillator = create_discipline( -- GitLab From 0aefc062185b64193c6020a9e79bcad95633fbea Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 Apr 2024 16:56:22 +0200 Subject: [PATCH 224/237] Fix ascii art. --- src/gemseo/problems/ode/springs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gemseo/problems/ode/springs.py b/src/gemseo/problems/ode/springs.py index 6b964f6137..8785ec2696 100644 --- a/src/gemseo/problems/ode/springs.py +++ b/src/gemseo/problems/ode/springs.py @@ -34,14 +34,14 @@ For :math:`n=2`, the system is as follows: .. asciiart:: | | - | k1 ________ k2 ________ k3 | - | /\ /\ | | /\ /\ | | /\ /\ | + | k1 ________ k2 ________ k3 | + | /\ /\ | | /\ /\ | | /\ /\ | |_/ \ / \ __| m1 |__/ \ / \ __| m2 |__/ \ / \ _| - | \ / \ / | | \ / \ / | | \ / \ / | - | \/ \/ |________| \/ \/ |________| \/ \/ | - | | | | - ---|---> ---|---> - | x1 | x2 + | \ / \ / | | \ / \ / | | \ / \ / | + | \/ \/ |________| \/ \/ |________| \/ \/ | + | | | | + ---|---> ---|---> + | x1 | x2 The force of a spring with stiffness :math:`k` is -- GitLab From 0683131d08d6a8d7e23ec737751ffa2e74ad2a46 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 23 Apr 2024 16:57:25 +0200 Subject: [PATCH 225/237] Which law of motion. --- src/gemseo/problems/ode/springs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/problems/ode/springs.py b/src/gemseo/problems/ode/springs.py index 8785ec2696..6771ac89e4 100644 --- a/src/gemseo/problems/ode/springs.py +++ b/src/gemseo/problems/ode/springs.py @@ -52,7 +52,7 @@ The force of a spring with stiffness :math:`k` is where :math:`x` is the displacement of the extremity of the spring. -Newton's law applied to any point mass :math:`m_i` can be written as +Newton's second law applied to any point mass :math:`m_i` can be written as .. math:: -- GitLab From a7f39d358806a8de33b8098c80840eea315f28e5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Tue, 30 Apr 2024 13:52:21 +0000 Subject: [PATCH 226/237] Documentation skeleton. --- doc_src/disciplines/ode_discipline.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc_src/disciplines/ode_discipline.rst b/doc_src/disciplines/ode_discipline.rst index cf58b74d0a..0719dd9baa 100644 --- a/doc_src/disciplines/ode_discipline.rst +++ b/doc_src/disciplines/ode_discipline.rst @@ -12,15 +12,18 @@ .. _odediscipline: +ODE disciplines +=============== + What is an ODE discipline? -========================== +-------------------------- ODE stands for Ordinary Differential Equation. An ODE discipline is a discipline that is defined using the solution of an ODE. How to build an ODE discipline? -=============================== +------------------------------- From an existing discipline *************************** @@ -29,4 +32,4 @@ From an existing ODE problem **************************** How to execute an ODE discipline? -================================= +--------------------------------- -- GitLab From 2cd3ddacd39d0de9db772992b11bc12ced4b2dbf Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 08:32:18 +0000 Subject: [PATCH 227/237] Add illustrations. --- .../_images/ode/ode-discipline-workflow.png | Bin 0 -> 16810 bytes doc_src/_images/ode/ode_problem.png | Bin 0 -> 8565 bytes doc_src/_images/ode/springs-disciplines.png | Bin 0 -> 46982 bytes doc_src/disciplines/ode_discipline.rst | 18 ++++++++++++++---- 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 doc_src/_images/ode/ode-discipline-workflow.png create mode 100644 doc_src/_images/ode/ode_problem.png create mode 100644 doc_src/_images/ode/springs-disciplines.png diff --git a/doc_src/_images/ode/ode-discipline-workflow.png b/doc_src/_images/ode/ode-discipline-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..82818b3b4c83e380b25ae2a30aba76795c60908e GIT binary patch literal 16810 zcmeAS@N?(olHy`uVBq!ia0y~yV02_)U_8gc#=yY9_F}gU0|NtRfk$L90|Vb-5N14{ zzaoW!fkCpwHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IX1_lKNPZ!6KiaBrZ zmhVVT-TCk1`@PGJzUY-o>{95sB$8>iYC%WRVJ52vt}2mKhc7Qawlit1sL)|nTF}56 z;*hd}eJLl?qL7!yB92SW^Q4}ttE=CWua=!Y=}pS}xhXSB>(^WKmT@*;n0Lu}@!UWB&T!l(cKE$*k{tXE@6*EdOR@ znwGX#VaxZ7j>4(tOmPCcwomUgGJd^8!A!vC#QI~sat7gdO7C3RTK?W~*O@cBl1?2o zUM83mS$X&R=_4~{ex234VCK}pN!)r7r=Ad~kQthwoxu5k@xio?j(f{aiJ4DQV~}Px zR(^C(F)vv)LFq$k)29L{L&a^#NKr7w;TVs%yO?FuqnY(gW-Z3=i^T5G>z|HpiXH%3nIe@^1>y8$Ol zI3!HZ%+$FmoHO~*Ch2ku zU0LPK$H|mDm zf&Wa`#p3cgW$aE^DZY$(a9QxL~!c$Go?R!S$Lr9z^nshKfi7}VB)FH z5UiB+&qLki(&`4!pHJ5-J>qQWm=PTDhAHTrX+yW074Kbz%EmNhm>o-#j*^7)m@MxxQPO94@7$F+L`9p2P{3YdXN1S;lMclks z-!R|wrv~Glu+R5q9Qq=-j#m`D%BL>cysSil_o&RvFIC(wN2c!CRNQcydH-@L27i{mRh(ZK)IDxBg{bec z(+scB`7qP5?Bm;x9qZHPGu@l^{i{?0_X8u(Xnvsvxz*)7U=JuVH<^m#eP5{*F(1XU`PnKgD&`&JT9K&CA|V^sd=#g-nkbvJmFP3-J= zty(MhLv!VdQj-tM-A;Q)30AOg(9~rA$x^`kVfn2^CF|sG_k_f(x)F7$+jr6zodQ>v zFV}Oj`4dDR{OPE0yQGm|9(4FHOOW&Fooh={Cz#9%N#9<3@0rg7z6bqBk2QpOM$O8) zdpG&Obhc=9rFWCR#6DQPg!>8Ok~_h-m6E0{`99h1FxP>0)mmGnG@jIIXHh|Zk6+rF z8ta{QY1jnK)PFg5N!7*AN{=c%Uo*?IPGiqw^jlxd*Q;I|ew#^~x!B|Dsc;vU+=+WN zJ+m3#GhS1UZOBupeO{^`mYY+qdC4`GJIM2a{gPKr-pY&K?bORGO$dL`*zu#dEe9!veadXYJxefJZH{Sa^*q|u$`n&kHk891YMX%p*kGJ$${ceN$zhc|U zeG-3amOi^b|M}l!j`@Yp4{iIu`ef1b>)yr&+dlk@JFu_y{gzKRwxmVNtFSh^JwK^> z;FhjT_tVDplcTO?Z!c8;@QUsB4h{X{{K@C588ly9^}c`jz&hS*zmwvg)dj`AGH<9f zef|A;u|wrt|L4Vf%IxU8m-{uX=NTPRXV`wwRf(7i*P<*DIE;jOfbBE3kUW6_#C@ zaM$MUn)vIp8{{WyJ&{*{uCZQQbD>di~^ConNo{(E|$dC8nJ(T!Ieo|3vvFH8s zmzM+`n6>2B>2B4%cDHm`J*=8u8Sa~Yh2xw2CBq*D_x`c&eE8ab>+0vvFD_|2{e3di zovAxtmu!WBL3^enD01kNLAI-)o=wE%x(N$c+80n&P5MbZXvi+pfLl|3tRm z_qT62bNlrf{by3oCf2zu=K9R9dB%SKxnHM6`%b<+pk?~`_~a;C<@ebO?-Wc?KYy+t zzRx-6<2rr+d}q_}Fa8O8t#1EHOsw9&cJj0&KZ6G82G7=?j~nibJ*!`S&P?hVizlzc z)bmw(2HVp9NriZae-E*XPW&fm`trB-hOoqceXYp>ClA^&2ZMGTe(<9`w&4$X4{ga;5UiksB^{O!rtE-XtPmf5mOqG<~1# zx9!)>_#LtCY$#LBjzoY&~m3;hG<1V1JuO6yPdGCTilyWO1E zGo!z{UP`Z(`;l_xiniOT+3T+G<~y9KG%FJRFs-9v$A*8Q2joAW-gtl~;kU+>W;ZFm zim1AsJeuLrAt$HamlpiL$}_&+WzOvLzt=@xX-@c<(6iRZW0uRQmyC17db_j3pFg(? zDqJ31CoH(twX`TYWXXc?Vms?-1<-VM(x_9ltU&7p% zv-=`eiJ5IaxhE;FEHh|t{8Ib7VL$(>&#qzfaAjXnR-WmaI>l}Nl0wVr7guq%zI=Ww zV*SKZ-+%Z_PJPEvmSK=z$8+!G&*YzhtK}_Nf5e#Nm+o0)_B`Z+)hfn6&m1omdM8$W zRy%EKz3%9*4ZNPAtj#xDvi5%6qxSrAM$kzo6Z`qESREuP?98KD>-zk%D~seTHoeWx-tsbF)`&p#a*Vk+#PMXnw&n*1esFQbL^^JVSH zR}XzjIKFX)@v5^wx37xOy_8kz^3l97p^hg_V}ECum*{7%%gjX|1V8e7vQKJy$P#3F zSX^bU`>UFI<=Aa2*blDdx@<1G?6}yn^@|F-u&)=ktZ_aM1d%g7Sif4A~g$JB5b5;x0$7x9xbpuH#DT+XZ))K7YKV?c7uk zTb*|cqqg5F>@GTdB4w5OVf`$viQVQ)*6i)Qr0uk>epS!B7e~@(xS6VNs^4err``3q z^?c5b5N?&Vj#dX1MZ?d_p8Hm{qGDgJ#feQvH$K}A_Ga>lxxCY*R?PTZee{o-(xaOl z7L8YyZwrbo@-V-=!Gq1w(|Xdce~znye}|n_G7nsveS3BE%U2Cv+PBmycUGS>a99E6q6e81s8(FS%4% zub277L)ramwej_Z`;7Ph5nmaV93c2HKPcPcfZn8ITQ}WXq@*ObASx*1+SIHgxNBnU5);00JAy3?V%SBb8>N_Sbx&P#4;S$Dg*#Ud^%P$FJo8Qwh-~7h0 z>7IWVg)#3_&omFp|1DWLJ9|Raj{VIh7S)T{Zoiz_u_NkI=8`*y`wqOFqFiH|M{Y=X|C*r(F+Zd250$sV-ssU+m$!Jl6B@7lqzAKPp^et{>9# zn-ua=Z6lMWL(ZWCVaMZ{=Vv@TZnLq#&*an8jvdt#Qmvj^TpQ{lC*Uc1ED^LU|Z z(7Uyl^gl4py8q^DV9@&pWe?e(=ar`B+;fyFW7ibF@lN2d{3I!wzrbVV+rDPcgD(^=d4A|+J8kP&eK&@o zqa#?wmaS&K3121arGg_>?-cH?d3cKLb7F#cXI1^wfI``q$9SLfPdaky+q9)0EdxKY zuX5eDuB+qbq>r0Mv1|BRwEa5MZ`UK2OP___@#mVfU%5BmeqGu>kQYq>uf4bjw?>8bK&xfp8Tukta_~KdzHIl zZyr@C^F#>>ihJ#vQqptJ?^NhIAFG#g%Dt^~-G0?iujDnK;-f0MF}wS!{^Teld6sqA zMo9~=EnM>3ecK~{)AjGqy_7tCAdqYAEbm`@;ma!j$}$Lk>{ycPxpUuM&*O!WLGJ(R zC*?gT44nKZ$h|=S6!e=RcbmRrq?Q->I{`$q(1XTTiZAzk6ce z{h}q0zneek>5y1s{Hx>gl#`dmOV02Ve*PG&Znk^Z?3sHP`C3*SGhyX^1Fr_VOJLS+M-`p7tgmrKAs{XWVW~oGgD!{%9xnJ&xLcJA$v-ZEpA6G^xn8XRr?G(0!n( z^vuNh6&dSYGp*CUT5v=iKeFP+!xPPEHI){(Ej}%fj9;+6{`W+$$&tG3j<1(w9{4Yv zrCA{-0g+p=WV)<$i}^?X=}f++YlXeLc63AtMVKi~TV=j~{rqn0Sq8>3ENi)s*_;2CN=R$InJm4>KKq+c z@%py`lg@}02yE%|j*aFYb|3&UXDPa#3GVU(?x9!#UuGs7|Rwo@V>`MA_ z=coAA&42F9c)!l@nkZk>HPPdLa`v`2T1UtH>3jdDeS@1>GsA1h?UUQP;^Ebra8u1H|ZJE4Ea6>kO|6^EF-5X71jXdoT4^ z&JqibDF2>W=KuSTUjO{8{k>AI?L&nO+ljkR-I*GH@8vqfYubEG*Ss$;$eb>@x$p9G zyYC-_lGDEKXHP0O{kNZev!MDGF9BjOzN(~Qk_4_XFs^$9jzO-r0m9%aV-m_H?oNEts?e%M&ddqvS zU&GW}U5nZGUArGY@Am%q{7Xq`*D4<$`|_79sr%GxMn?mY&-<%yS)DJlk4#IuYxwkm z84Ta*8#@eRrj>HnLtvpwI|Ju1^vx;s}dH+Nc&r36Z8m_$l@}-&QV-;DYmo}c^ zp1;;E>HlICJJ+b{gR7YSq%fv9<~3|**yPyqdOLpF`bO+r!(C(H|DW+kI{)RM^T!Uf zfBK@|$gXXy@|7{3NsV!Te+T#O`FqYyG-2Dpb7NuJHM>c5!h+p*dG)=7SybQh)J*;I zv~ShSziXD?1@udHCgPp z!i@EI)EVcmn<@IFTAS%KBRk{!>0iEe{KyucY@wu5D|JI=hmum=<;5mGOnmImn2)<0 zsjr&Y$5*}4z&2aOOygznf$T zLf%nEN@?3S-L-nH$9tx$qwii_+$EL=NB!O}Kf0xd`5Eulg;^HIcMGfNvhEQO{A+vb z|D?+QYwNa8{qAVHj3)W1Z`Inb z@bcK5Sz$5hbC#TcR;katS!J)r%kz`IO-_bL>TYi8~SwbAB%lu_LX;IRbv%i?)+2v%=6dsCF{MTmYscE;^TLCI(Pp3 zh{=7|%_8fkEb;f+)f@8M%>B}RRoNSFDxQ`e_x@G$ukTra?Y(fXs{aYE`CFHKR=unC za=+A>`+D-XXD-p2v2)Tt#g`tQk3Snpi|^aL_0y{L>n+oAuOEK;_+zB!b+4+%kbF!3 zOSO~gR5JHkz238P!n9NNe($0@jxU{bZB1-`;l>-!SUep!JQ8=lb8ORrZIx9*A8a3# zw*HKMnmUi=g!-wY2bQ&_K4W^^DEm1%-t&EThviXOPb-ce@d*wAXRoo_vF~xev`z41 zgraZ$>d3OAv%ZI|xOqO~LCS-MStfmqW=#8-YFsYep7nH2_8!)Xt1}gweux+ z&zI|-tcYVfHc@J=+4=SRz5l+?Yn}ByEK5Cbk{N@Z(p-Jl+9i+wdp>wP$xco%UF_NZ zU4Btv=li=l4=BG~@_cGi;W^H{@EQBkShkf5y<9S%PlnM)@oQnVoS4XCJtfuykq4GH zELBd@dC6jBng2ZY!1dQT|81AZpR25_a!yE1kbbac&zYzLhOMRRSnT*@c>Y-LFHf(F zt&|DdJU@9Q+kwfArA1QHPoCaC;qvd>w_@^y=bwH(FQ(%C?N*Z-rX5=j?oyw?^uYH( z-I8Nd|0~IdKiE8J+sar?@rTPNGClabWY_(rpVDWt%$X*Bx4+TyO5emSOfQ|xxOQ+= zl)Ch&ml{^G7qA9siv90=RsOxA?!6vcU3vSqjqjg+{&xNR?;nOQ%?&tj2>npgWawx0 zoBn0lp;!AAW%oC)6l-3uz2q^&gJWTrKXno|V#5kH0(4&kTEF2PpYOEW`=0HX=d~(XSMT$}Lx0|1zj^Fp>(AZhH_lg9{`0xSy7KDz zjn#>_tFO)cpEIxA?dHT|m>h+yD63<_Gw$|3KI%u?eB2&tQsQTBspOfbO z-@I^HYGA!g;e{`mM|W(Gc*atE_~@mdPd)w?Udc(A-%+vb;GdH}fBx#QSbxyS{>*uA zhJ5{V9UZ5YA8o7Bc0FPfw2?pIY1!*HTy~D9{+?f4`0hpclSj52z6*T6+;^X8a`MAR zukss~fBGV(v!-aze;L8cQaQitKYtEdqEx-SSKsG)*@^cOp56@dXZ7>do-&CG35rkl z(q_n?vL#V`()JV4I?W0z`IZGK+65hcxKCd2`?cAMnWd|~2A%!YcQ<%Oo$u`5^?MBV zhRl8)_GZrf_kA7iuXP1hihln0XVYC1zN0$>mp?UOKl<|L(^E$)CtXt&y{Q<=X%b{} zZ#J)T@7i6RS%u$yJf%&eD_C-USDkApv6;PwM|pfqLT&p?NM(w3<$_ zCYnCjtGi}ffYk0afqMHY_G~}bf3W2BW*Jr+N2%recs|%YsGanWuSQL2-Ma&J-Nzqq z1P$~E9lgXmU2Eb|OOAiMHzrT{eXH%*+R4fbZ`^%)`@nkk$bU;ab}atUw&bygC5K1( zHp}jiIjej8?@HgOU%}ba@pIC%iAB@QH++bA)f%Q~dQ7LQ>-cZ!1Mk@*{~hgkaVq_! z>qTMJxm+?HS+RFJLe~FO_x$a8B;UPa`_adS5hs^R7^L?VSj9~gd#+HytI6S%_3=%L zK+PrtTYaUxy!59r$G;x^^T{hH`}t|p(`$_9u%u1B^D*e{vO>XNn|sCLN6!lho@cy2 z^-JxgquE+3BkV&jwQjxt-azzt#{R4csn@l9syj5g^AB?zIKT2+<*x-x{K7uncl(-k zXGPtrnX)VD#P7>K;$IS^Xus~H(bNP(&F`C~GmovAc)ZNqwTPxD1AH+(OxzJ14Ke8)!Uv1$~DiuGxd(c=7+vhryb;6l61?X zdXd514-x#~E@zh9(%Lh9$t^*l?nRGsw`b`Wt*t3PdxtT^?fV}6={jPQ+RudMt>?Wk z3$_*@tvYw{J-*blp{Tg?nY(cP;8? z)tj`X_AS$-uqj^(J?+=eEX_{*wO{M8_h+lslbWu_HNW})@XwxoOKd;yO3vDqImdM? zN6DTg&czM7%L>jL}ou z+sfn8dAExDrB9Q~s&cQ-_#+$kDnXt(tJ(kQ4&O;z!sC{I+nud(-C1sSd>^~s#1I+7 z)+Mr@p-$ctiY^8lBpC8HSsvdYd{tqy&IkJ?j}C8MlBa)t@~g*T4D%LRxvafg8|%F5 zZ56|7hH&pH1Ae>xL6>BgEX=i6?Omg>YhKp>L$M9js&kcJ_AiOv{r=p8@9VU~w#93_ z6b#~fJjY2YUSVsPo%bo({|rC+FP)Xun^2LuEpnWZBbN`Q!({m+@qOaZM8D` z*t@qmt6OXT%3oD_R&^j=#Wuj=Uiri*r{zI=12dNEdcN9HGkuBm)VxI>)Gs~1-%^rS zqbRn|+3(}--3vb4-Fms9NUFeXUYz`nuuF%%%%@d|{4jo5*ZTLsLczk^YWducTO-&V zBLZ~pN&T?lj&IbKQ+fKttg9o**7IYE?3Zhg1Pg64b3VNNxwh0p*||*ZFNaIkOxp!% zdlh=c?b=Hwt(zJDo^g+h)a>XzXRkYDc6QvK7`040GpnmytNqo-HjP)rbM0FHzg+P3vDN;YMuH#zdoZrM^vSyO)yI1? zL`wcSNoij0J9x=_|G6TDc;weO}}Qz^Z^s$}Oc?GM*Fet1sy4YkY9{8#KAtmYpd&@cSx+2iGYG8eVKEmFHa zeU0;~1^>)H6r4L~5>;0ll(1v-xeM#ke$RA~TDn1LrS_r6OYBRk>Lz@>rgFD2weo@D zgIQkAo_^jgNB;U=i*FYBa{S5H{8f`buh7{Y6IXWi&o$wn#eA>x6nA#))qQ#VdywDZ zr3Bno!)&xdcwZN>aTQzBg$-pluvpsDSSNj@!c}>33n3rUaBYzf6LKh z$OT=@XbM^b|bSAk?{8yZ8nvi;FzOb$1uBSh{b@P%VFTMTRoyK^6OY!P|hrWfS zKe%6fP5401t>~TJQil)KH~%d1^N!vgIQ@%Xg875_f*=2uU6a+EbdBv__m@ilo(^~S z3h~u}f1@u+Oa8dO&%H_ZemnJ0Awv28@#FQA{~btgwd&rZ@lxo)e&t8o z)NbDt+tI~V@1*xq^3r_0ShhbpO7mn@V&mDP{`x=ZpY*S0-=;kI0(;(n+!2?&m)K9( zFJxh>^iC?}_wq^2yO(rVefht2zUp5_(Ix*sTI`Cpp0SS9#9*FZrwDlDwWQx}nH@8h*`vf_)??Q=U)JgJO%8^b@NDfKOzUE7-#6f$*# z4!mZ5#`M0ggS-3m6XCz>55%j+dj8s+J+bM!>d*hZ(v?a|aw{s|XFaG_ee24#?=%Cu z_by{4KPi=y{BB1U{BOQ<;LNNocf5an{vQ8K>AU3;`R~sTL{9p)Vr_g+$NouuuIF8j zTt2{Gz2pDs#?#eX>!mIA_|$_vb{aP5UuQcIC0NL|GL9$c!z`OCvhGLZ8*EoTd$Uh- z#-_ohWg8Uf9}2N{id5K?V0Xb6>_P@bF)gU_R9dz$Jx{F`ds=t>7P>OWUJf1nwLnc zCC62KbEnNqQ<*!Z|KP^bXM2M$^-ijr6|b-SQu=4bnbY%MD|zPU zB-(3x{#v%A-mA)T`QJj%bnjb56H3dcmvk%;cc*N#_|} zPtX&oJ@D)2-pQ}u23)%Car6^E)9UA?(^$4K*D=J*){hVs_KXd=$FL?geR4kAHpXic zOL$iPdKeqB|8+PcJ7fL%%)IM-OOiL#&aIq1;oNFj)m%R-|9vi2a;N5Jmp{nz{B3fp zamn0<>W0M)y(`SNtmUd{2r(^=XfEB)6vOg{Nyqh<+%%P|Pv`ladMeG=G@(vI)O^d% zclWt2&*oPb6f87fxkx@i+w*MHG;1v-rXTzNY~q(NIUe-E`BMKE`y~l>jaL@!vt4=n zoRjX#o#*d{don(XOQ=uSyyslu0iU4xXMJz~H<59EW%s$}-tlis7)mnd?Kk@Hnoo3M zxRUJgt&Q9b(`jS7V|ImK9`{%wT=GWK! zmU@u=^XdDBugZ)1MSk!zygRc0FY6rxqYbY;avvW}SSG&AeUaRIzXab)o%@6?tl0Q; z_M}&(r)T)TvWQvlx;?gBWLdP9(X%{L>*v$&_jPo0ZCdXC%qX_Y}6e*3umS@7Xs z=%ize^A%Vh#(ExH^~}rl)6nt5 zykOT7<4>*U3ItlRK5_)*EtEa_bb9WNkk|9{eDwVvEBrXBc6&$Tl-CmL(zz~wwd%cY zJkPO6qO`82dNLE^0rPC8gw(n1o=5jDtPr`OswB7S=CeOBEcM=XuVZ^^1XwIqsWF88 z=@4O2d|D*V;<)sp!S3hN?N?mq)l`}{y+mpy^IyJ6VJCE-O(?n_uy*sh*z1fN4mcJ( zFwEG#X~&P|{FQoz0?AyL&sr(Y_x>xqNB+lgjwOrdZV|C{J+;=Csc%||yKmlxd9vJ> zn{};4re9+_(Dd5Fa_!Ovzh3@zw<}y$yW~*c-sKL({&W ztxo%;^H)8+X%-xOXU>{Rs|NZTe!pIFBf7h5=cVP+daUviZuSQ!`kiJH6r5z|yXth5 z*pJ0aR(tH6ci&VZYFk|0yXA%FwksFqeFzL%d{`>>-uE2O&7Q9JZhd?pExL?<`jr*i z53J_8eDuyQ`9^Nf(h2$Oa#Ow-CuF&ZthVG~cl6jfy{j{K-R`)=T{ks)!$Nme*_=MX za`mA`nf1(jue<~eeJ{OR682!Lr{!MRMt7B3T_wM@kxPYg=D)f4k|WBS5loq8E2 zmzGp8-r9T6XzRQcK2N$YR(pP3(LTZMcH)7BUXSnDUtaw2UbhPS!7Oue59gb=Hs;pN z^Ub;SktrT%{^l==sm)z3N|%a)rMj4Px*jjOfB1p;!sW%&*Rm=-Qhi`NX`k~iK0cMLx8A!+ecxB>@~8I6 zq(7&grhRHWy#HIqR^iNdE22(v{>x-J)-UEh*GKx#Gd?DQ_;oGWSZ&=hZLs~H1 z{Yc*uJ=v*qJ|#}-x;sfMMF3jJ+sp9y{p zaH;7G@&9cf@|)Z5?Hl)sry8{aJsmHXa20vj|7n{WuKlV1`Rms0ev*DtD!E>-Ja|@C z*xlfjv{Ea-tFbMuE`7)Co{nr!^X`U_&iEW2&1XmA?eElH-552wGJi?b^fK?sPj{`I zF>T?O-WU&;JraWP%;!$z=LTDIEnnj$&$9Ww=(6Qrw^m$ivSg5Re$~yIImP+%#$#ta zs+eESYnm$jiCfUHdc|#NKF`$?iWW}IYAjY>6!|*Ex~p`~(MR`GTVEcQUgAB8Z_1We z0^fGezSOJuO7q^v<-&K9+g@p{I{tcs;5&=>n|bzU-@AlJ@ijTP%yHgzRAhz1gHMZl3I(R`aKDuYD4>Pg-#K_6bis zTi;%-?#bNr?vm;0Jk^(bC)IiEDs)Qya?p24)sktRzcwtrOA}A+&`ZjhTpu3V{Lfpp-Rkwu8o@PpJTrnH zl&>;A7J2EG*W(@g13v$I>h-ucd~IcY?W(`J>%aFV2%GnW)N=d~UHSRlr2`ueY@GCL zLf);&+mgQmFKM(Ke0PZFWlLUJ^_t}k%1V#Yb(eje=lJYD|JB6ct=|?_POO)Ex$flr z(^Fr+o#F7R=8xFY*{pY5Pvz!#mR>m-DY$XLj_J`x{4D=`vPA!iyv#exKcTO3>FZOc zx7aUYzqIzFI-@+ZoyJSMpzD*o41cg*YRw7c<~Tl~>vg_5ul*dwmuU|QR~=7n-t)$l zXU(>VsV4V+6nJi*aPF$8=i?2>C*+Bjhltx9)@~|pid^@nUObcG|Jv5bWJw7IdDeZ5 zIWmH0t<=shs4RT!dFe!-b7QA2%YiZ`hqvE)5gGTvc=K{{82QzD~=Zw(ZoO)@>7;Kh0aRzpA2bO5)9F zCT$hzK@4-ePpSU*Kl`eEa?$qW&sOi%N)O06zdE`h+9jryae?OK<>9}@J7@RLx7v8M z&a>OpGkZdw-Y%t=DZltvu0FJOZCQNauHUDMO!LpH{oQ$Sj@4?OZF{#$SWH?x(LMWp z+hpy7+1(+RXWM=#e%a1!- zWSp^Za;=8)v-EIbJy9b(S!Zd!R$G)vfkj!^^GT_Zu5W=k!QlHrKk_X0%e$Ogy?rdhx-$ zyLo0wOl~LZe4oRz%TL`(|7PGB7W7UjIc@1Pg^W=` zAw%}_(&pv)do*&7uaRSSd_9Asw(683|K3uI+ZIWm4qJa=Z|@12btcP_BkK5<_J3>s z?MPVB_@Uz@|N9He|8=*llY6;kW+-Sf-6taH+|mOx7J}!+kL@=xU-!a?>#PjR+U9*# zmwuh^-XS;Rf}es8%YmgFvET1rzE-(yv*uQ#Vu3Avui7&!i<*8tZd$B%zQTL+Q+5`I zhJF7s&rUO!)?;^ky?mFjqVV7Qcqhvw2q~P_rKQ5cje-}7u%@G-v zwdPq-op()-RULJ_P_<~=YS4`Q`W@XpUyn4bGkPbLZ2M%+0yuj~$AcC9_FM9L-rb-2 zLi+A&mdMVQxb|QC4s+aIOp8-L?&b3L^OfUUlI!=JEAjn#r04I;ZsCqy^<~M|EGKb< z@l<&2hx-TVvUzg|2}V0U=k8KU@i5%Yqt9`PN{zdCNLxURYJ6W62Gxa9{=c^_$G ze#O9icd=dV%5ct2p9%$!?9%=9c(#(+nmuwi1QyMxyyJgu->TOujf`JU0+qCpZ@M4o z8Ks3~@ih$|VsZ6chU%x2wwM$sytJ{>R+4*PIO)u-X9^jIH)?*^Yx6h%!5O(_M{_)4 zWb&Gqy!WW`x^&sJzg_jNQs$~9vQ=~aR!{nJ`A~V;?j_}XiPEz@cdh(TJO66JPJZvI zJ?DOY>{xPJC06I9hUa?8N9mT1mufsGp5Gqme6x@DiNcL}wvD0EZ8U57H~E*gh=3rUAyVCIS($J>FYTC z`SGESAFeJkK2|5wpQrLo*pkM7*+M4E;ICJiXhHovyA{1(?dHyZzjSZ+!AikGzq&c1 zg7>>SzB9aLe*P>qjOkqVN1L1W_Wvb!DEYrv&Rl7BDW%kcqi;dS4%Q!`b(UROe21#m z&(C`I?dzm%QaP-i+6?k1^p`)&0{OGC0CUe-P@U_Ti{o~@-*yt?Wfb? zFKU_(r92P`(tBv~Ab82O@PR^kQ{V3%J#kr>+|%6 z$DTV+UiNf70UBV;jo3XYDCMW_OZL0-zi(FFAo(HhLDi+Bza%{6^B-?EDaxvg>$>7_ z>4e^rU)Bxtdk)mS-nqhHZ}MJNm0Xsb$?^YI8LSRck#l`~Ei1dRxJg;zc}khe-yhmX zk1kezG?VXrmHUIj2Xh5KUU#ZGr_RbB$m`bJ)v>v8v+7&!J+}L6lU!nM9(tAiq;{XN z(!3Mz=jy!ldl0?E;jiHXZ`0ScGp;0Y6;!y?tT%XH<|#h0X#Q=c<6gg9UCv}a{cQ5V z{gPp|sAl-R6;po%|NObFV@H|9@9bw`LEh^dwo2=KUijyaQbFCrzug|IcQ5gLMC1*^HUl*Y+yGV-Ve}v91_aoj*emQ!~PoC_zw7BrOf`v)4$KRvZSi`jU*fZZy zQSy5kWOc5$<;-gD(6*yjYSuhUgZWB&eh`fBm#o>3F8a7te4uc>R!6}Nx< zx_x#)kappdmsPwf?Gw6YZhrA6;DDFatJ-gGJ)TSO=x_x;w>6)@GqL-ybMQ+Y-5|pc z$p`$FEc)pt3{F#&MZ0;O! zIbu6$om!|mPlm!o6S);vZ2uW4&CA|0_u;9-_p}y=&s(?ILv_R1l!U*DM@5$;@7DU@ z=oz24Sn!My`@w}XWd)~)$9Y&WTQL5R(OmvXHz;q>^k-trs_#DB8Ppy=yDv$6QS6Fu zt=Z3(9u&_!xpIj@M5e+@k?QZtrAcjkVy&CMu8^BtFPiQlHTyEtO2e$JY-=xvPq<_+ zc%w0-J}b9CK!#CJaMH2~$BOdUYMd@@S9;Var=A-ae&*yn&KTPhpSJcgA1>5enSF87 zlCW=H2UeSO+>sD8@4v85O0&#;m8{iv_N(1T=KVCB@wZSQXLj=gj?RvqJ1xq^R@%KZ z2s*bROtpPm%Jxg!IJCFTyDu!LUY>ni@MF`G&7QT4+oy!AoV~{6UdgIxpNsC?m#1Gp zBkd)7zgk6hnzZj4*E|Wy$mbJw#WU1wIXE{(YwhyYCO7@oA8!^b&X~I=_Hu4(?b`b} z2l*_w8q^A0Ej)BSXuo385szElC5yQlvXvhlJGAHN%CaDPbEPY97;EhHcgt{>=WTZc z6}xx-K6?K1>8B+R{kCe^{=PnKOWs=_Pif}+FMYV8!`3W`-x_Rva$$zf*@f4i zJjz=t`qw$cTK2z^QrwFD*FJdOnwe!jxAMS5)9yX&64RI0-#w&OA338;yKuG)i~GHm zA;sojrQEej?H4yp?%L_Si-aM_aOYdj@Uah!tJ+t|zH6@>7`7GRn?p}-c zjQ0BVhU2~TgXKRfJy)&0t{f#2wrSOpa*ewazH(i16?)JuSm=9JYpR1;_ph~ww*K?_ z%4fN|{CCnPujdLDhm;=4trGon+w0b%w28(m*k;-`?w*o&sERpr$E~gd8)H3llD{)v z{`|%6z}ZQ4+%`_75f3^}EDq6fJ#WtZ-TT+tCHYThJI&F2>G(ixL7@1ohZeK1JP0Zb zb-8mhP_`c01nHCQoACULZ-C4VY@5%f1Z$su=XNAA>t8>)0ywt&Q>3PEy{gVA^ z-O;@42haV;O#H=sRC1^NE$fE%Ptn4jN1x?3EcbBTB;2){$B@@Kc74(G$tOeJ_x`ZD zWO486ihCL*N4~kA`dMT9KL6#O_4-Wn)kTA^E3)`RBsyN zy476VzNh&S(-%sDGDOZ@gepIw57ZF#?$%Kuok2W&2%8c6IB%_NMcbiym%n_&iIe+@?U6`~KDCk1~St)^9uYAT_9Aj-23T zv8e0?_b2PMou7Eja=VJsyGdK5UoQG}KJ2>jg(+N973MH7Fv^3@1vq(Gyyq)qdHcJ2 zr&(ssEWI?PIo&5%M`GQ}M;m5NojrJW*rPAUUgk6JpUYF%1KdvEzZp5ErOg#N!q&t& z_4?vQ%L5S&CdzTQS09-|IeKQz@guB@os;`|XBhi$;J0+oNZU4J iw(y5)!*93$%YXi?5EeA?>0Sl~1_n=8KbLh*2~7Yee%VO? literal 0 HcmV?d00001 diff --git a/doc_src/_images/ode/ode_problem.png b/doc_src/_images/ode/ode_problem.png new file mode 100644 index 0000000000000000000000000000000000000000..817501457806d23b0fca61e779c5488a6bf927e3 GIT binary patch literal 8565 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 ycdefM(ou7#&!=nOWkA*{YN*O Date: Thu, 2 May 2024 08:53:25 +0000 Subject: [PATCH 228/237] More illustrations. --- doc_src/_images/ode/module_structure.png | Bin 0 -> 83967 bytes doc_src/disciplines/ode_discipline.rst | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 doc_src/_images/ode/module_structure.png diff --git a/doc_src/_images/ode/module_structure.png b/doc_src/_images/ode/module_structure.png new file mode 100644 index 0000000000000000000000000000000000000000..54294f243226e3039b2b768fbd9ad0850eb7ae6f GIT binary patch literal 83967 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 pftIg);QAAE|Bk>l<1_!{A6}}zH)X=}HU Date: Thu, 2 May 2024 11:51:01 +0000 Subject: [PATCH 229/237] Add note on the names of derivatives. --- doc_src/disciplines/ode_discipline.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc_src/disciplines/ode_discipline.rst b/doc_src/disciplines/ode_discipline.rst index b5313dfbea..96536da9a8 100644 --- a/doc_src/disciplines/ode_discipline.rst +++ b/doc_src/disciplines/ode_discipline.rst @@ -17,9 +17,11 @@ ODE disciplines The typical workflow is to define a list of :class:`ODEDiscipline` s from a list of :class:`ODEProblem` s, then execute the list of disciplines like any other. + .. image:: ../_images/ode/ode-discipline-workflow.png + In this tutorial, we explain this procedure in detail. @@ -46,9 +48,14 @@ How to build an ODE discipline? In practice, an :class:`ODEDiscipline` is defined from an :class:`MDODiscipline` and an :class:`ODEProblem`. + .. image:: ../_images/ode/module_structure.png +Note: By default, the name of the first-order derivative of a variable is the name of that variable to which "_dot" is appended. + + How to execute an ODE discipline? --------------------------------- + -- GitLab From fecc977d823fcb21e5606e59923d8d59ee8b0a65 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:46:18 +0000 Subject: [PATCH 230/237] Move module --- .../disciplines/{basics => types}/plot_oscillator_discipline.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc_src/_examples/disciplines/{basics => types}/plot_oscillator_discipline.py (100%) diff --git a/doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py similarity index 100% rename from doc_src/_examples/disciplines/basics/plot_oscillator_discipline.py rename to doc_src/_examples/disciplines/types/plot_oscillator_discipline.py -- GitLab From f7af2ab426129a2d3431c8dac702fd59c4328ec9 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:47:33 +0000 Subject: [PATCH 231/237] Make function name start with a verb. --- .../disciplines/types/plot_oscillator_discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py index 6a6e36f8f1..fa53c613e1 100644 --- a/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py @@ -80,7 +80,7 @@ initial_position = array([0]) initial_velocity = array([1]) -def rhs_function( +def compute_rhs_function( time: float = initial_time, position: NumberArray = initial_position, velocity: NumberArray = initial_velocity, @@ -117,7 +117,7 @@ time_vector = linspace(0.0, 10, 30) mdo_discipline = create_discipline( "AutoPyDiscipline", - py_func=rhs_function, + py_func=compute_rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) @@ -125,7 +125,7 @@ mdo_discipline = create_discipline( # Step 3: Create and solve the ODEDiscipline # .......................................... # The ``state_variable_names`` are the names of the state parameters -# used as input for the ``rhs_function``. +# used as input for the ``compute_rhs_function``. # These strings are used to create the grammar of the :class:`.ODEDiscipline`. state_variable_names = ["position", "velocity"] ode_discipline = ODEDiscipline( -- GitLab From 96893add90fc0dab9818a99649a883fe36c834f8 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:48:45 +0000 Subject: [PATCH 232/237] Improve name of discipline. --- .../_examples/disciplines/types/plot_oscillator_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py b/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py index fa53c613e1..0754b38ded 100644 --- a/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_oscillator_discipline.py @@ -115,7 +115,7 @@ time_vector = linspace(0.0, 10, 30) # ........................... # Next, we create an :class:`.MDODiscipline` that will be used to build the :class:`.ODEDiscipline`: -mdo_discipline = create_discipline( +rhs_discipline = create_discipline( "AutoPyDiscipline", py_func=compute_rhs_function, grammar_type=MDODiscipline.GrammarType.SIMPLE, @@ -129,7 +129,7 @@ mdo_discipline = create_discipline( # These strings are used to create the grammar of the :class:`.ODEDiscipline`. state_variable_names = ["position", "velocity"] ode_discipline = ODEDiscipline( - discipline=mdo_discipline, + discipline=rhs_discipline, state_var_names=state_variable_names, initial_time=min(time_vector), final_time=max(time_vector), -- GitLab From 1221b0942f7cf0c33df83c80f5d757b30b98e0ed Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:50:19 +0000 Subject: [PATCH 233/237] Variables cannot be coupled. --- doc_src/_examples/ode/plot_springs_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index f6b61344a1..8f17c980cf 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -34,7 +34,7 @@ from gemseo.mda.gauss_seidel import MDAGaussSeidel from gemseo.problems.ode.springs import create_chained_masses # %% -# This tutorial describes how to use the :class:`.ODEDiscipline` with coupled variables. +# This tutorial describes how to use the :class:`.ODEDiscipline` with coupled ODEs. # # Problem description # ------------------- -- GitLab From 08895f4a8d9e113404942f18cc673631d8d74727 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:51:16 +0000 Subject: [PATCH 234/237] Make names more explicit. --- doc_src/_examples/ode/plot_springs_discipline.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 8f17c980cf..42ffc9e648 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -73,9 +73,9 @@ from gemseo.problems.ode.springs import create_chained_masses # Let's consider the problem described above in the case of two masses. First we describe # the right-hand side (RHS) function of the equations of motion for each point mass. -stiff_0 = 1 -stiff_1 = 1 -stiff_2 = 1 +stiffness_0 = 1 +stiffness_1 = 1 +stiffness_2 = 1 mass_0 = 1 mass_1 = 1 initial_position_0 = 1 @@ -106,7 +106,7 @@ def mass_0_rhs( of the first point mass. """ position_0_dot = velocity_0 - velocity_0_dot = (-(stiff_0 + stiff_1) * position_0 + stiff_1 * position_1) / mass_0 + velocity_0_dot = (-(stiffness_0 + stiffness_1) * position_0 + stiffness_1 * position_1) / mass_0 return position_0_dot, velocity_0_dot @@ -129,7 +129,7 @@ def mass_1_rhs( of the second point mass. """ position_1_dot = velocity_1 - velocity_1_dot = (-(stiff_1 + stiff_2) * position_1 + stiff_1 * position_0) / mass_1 + velocity_1_dot = (-(stiffness_1 + stiffness_2) * position_1 + stiffness_1 * position_0) / mass_1 return position_1_dot, velocity_1_dot -- GitLab From 17cb5f4a481b8b6be1a15bd420e0b15a5cab5d06 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:52:44 +0000 Subject: [PATCH 235/237] Make name of function start with a verb. --- doc_src/_examples/ode/plot_springs_discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 42ffc9e648..025d33c624 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -87,7 +87,7 @@ initial_velocity_1 = 0 time_vector = linspace(0, 1, 30) -def mass_0_rhs( +def compute_mass_0_rhs( time=0, position_0=initial_position_0, velocity_0=initial_velocity_0, @@ -110,7 +110,7 @@ def mass_0_rhs( return position_0_dot, velocity_0_dot -def mass_1_rhs( +def compute_mass_1_rhs( time=0, position_1=initial_position_1, velocity_1=initial_velocity_1, @@ -143,7 +143,7 @@ mdo_disciplines = [ py_func=compute_rhs, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) - for compute_rhs in [mass_0_rhs, mass_1_rhs] + for compute_rhs in [compute_mass_0_rhs, compute_mass_1_rhs] ] ode_disciplines = [ ODEDiscipline( -- GitLab From fa1463028d0cfa157c593ff4f82d096d4107a1a5 Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:53:36 +0000 Subject: [PATCH 236/237] Improve names. --- doc_src/_examples/ode/plot_springs_discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc_src/_examples/ode/plot_springs_discipline.py b/doc_src/_examples/ode/plot_springs_discipline.py index 025d33c624..958fa34ea8 100644 --- a/doc_src/_examples/ode/plot_springs_discipline.py +++ b/doc_src/_examples/ode/plot_springs_discipline.py @@ -137,7 +137,7 @@ def compute_mass_1_rhs( # We can then create a list of :class:`ODEDiscipline` objects # -mdo_disciplines = [ +rhs_disciplines = [ create_discipline( "AutoPyDiscipline", py_func=compute_rhs, @@ -155,7 +155,7 @@ ode_disciplines = [ final_time=max(time_vector), ode_solver_options={"rtol": 1e-12, "atol": 1e-12}, ) - for i, rhs_discipline in enumerate(mdo_disciplines) + for i, rhs_discipline in enumerate(rhs_disciplines) ] for ode_discipline in ode_disciplines: ode_discipline.execute() @@ -200,7 +200,7 @@ plt.show() # :class:`.MDOChain`. mda = MDOChain( - mdo_disciplines, + rhs_disciplines, grammar_type=MDODiscipline.GrammarType.SIMPLE, ) -- GitLab From 98fe122cec2f977cee8cd1bb37fe8f7a41504b1b Mon Sep 17 00:00:00 2001 From: Isabelle Santos Date: Thu, 2 May 2024 14:54:59 +0000 Subject: [PATCH 237/237] Improve names. --- src/gemseo/disciplines/ode_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/disciplines/ode_discipline.py b/src/gemseo/disciplines/ode_discipline.py index 3d4acd04bb..28a063bb13 100644 --- a/src/gemseo/disciplines/ode_discipline.py +++ b/src/gemseo/disciplines/ode_discipline.py @@ -114,7 +114,7 @@ class ODEDiscipline(MDODiscipline): self.discipline = discipline self.time_var_name = time_var_name if state_dot_var_names is None: - self.state_dot_var_names = [f"{e}_dot" for e in state_var_names] + self.state_dot_var_names = [f"{name}_dot" for name in state_var_names] else: self.state_dot_var_names = state_dot_var_names -- GitLab