From bec11637aa116d17fdf48564e0e23b1c5215553f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gallard?= Date: Wed, 9 Sep 2026 21:56:30 +0200 Subject: [PATCH 01/16] Add GEMSEO interface, examples and tests Add philote_mdo.gemseo, providing PhiloteDiscipline (a GEMSEO Discipline that calls a remote Philote-MDO server) and GEMSEOtoPhiloteDiscipline (the opposite direction, serving a GEMSEO discipline over gRPC). Add matching examples (Paraboloid, OpenAeroStruct, and the Sellar problem driven from OpenMDAO) and integration tests under tests/, adapted to the current OpenMdaoSubProblem() + add_group() construction pattern. Also add the gemseo extra to pyproject.toml, add gemseo to the CI test dependencies, and fix proto compilation on Windows. --- .github/workflows/tests.yaml | 2 +- CHANGELOG.md | 6 + examples/openaerostruct_to_gemseo.py | 150 +++++++++++++++++++++ examples/paraboloid_gemseo.py | 123 +++++++++++++++++ examples/sellar_gemseo_to_openmdao.py | 161 +++++++++++++++++++++++ philote_mdo/gemseo/__init__.py | 31 +++++ philote_mdo/gemseo/gemseo_to_philote.py | 155 ++++++++++++++++++++++ philote_mdo/gemseo/philote_to_gemseo.py | 165 +++++++++++++++++++++++ pyproject.toml | 1 + tests/test_gemseo_to_philote.py | 103 +++++++++++++++ tests/test_openmdao_to_gemseo.py | 96 ++++++++++++++ tests/test_philote_to_gemseo.py | 77 +++++++++++ tests/test_sellar_gemseo_to_openmdao.py | 167 ++++++++++++++++++++++++ utils/compile_proto.py | 46 ++++--- 14 files changed, 1266 insertions(+), 17 deletions(-) create mode 100644 examples/openaerostruct_to_gemseo.py create mode 100644 examples/paraboloid_gemseo.py create mode 100644 examples/sellar_gemseo_to_openmdao.py create mode 100644 philote_mdo/gemseo/__init__.py create mode 100644 philote_mdo/gemseo/gemseo_to_philote.py create mode 100644 philote_mdo/gemseo/philote_to_gemseo.py create mode 100644 tests/test_gemseo_to_philote.py create mode 100644 tests/test_openmdao_to_gemseo.py create mode 100644 tests/test_philote_to_gemseo.py create mode 100644 tests/test_sellar_gemseo_to_openmdao.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5f6c9d9..60e67b9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 coverage[toml] grpcio grpcio-tools protoletariat numpy scipy openmdao + pip install flake8 coverage[toml] grpcio grpcio-tools protoletariat numpy scipy openmdao gemseo if [ -f requirements.txt ]; then pip install -r requirements.txt; fi sudo apt install -y protobuf-compiler - name: Install Package diff --git a/CHANGELOG.md b/CHANGELOG.md index e94db42..484dd98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 twice for an implicit output, which is quadratic in the number of dynamic variables. Both now index the metadata by type and name once. Applying shapes to 1,000 dynamic variables drops from 922 ms to 120 ms. +- Added a GEMSEO interface (`philote_mdo.gemseo`) allowing GEMSEO + disciplines to be called from Philote-MDO, and Philote-MDO disciplines + (local or remote, e.g. served from OpenMDAO) to be called from GEMSEO. +- Added examples and tutorials demonstrating interoperability between + GEMSEO, OpenMDAO and OpenAeroStruct through Philote-MDO. ### Bug Fixes @@ -121,6 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 removes redundant work inside `compute_totals` on every gradient call. Results were already correct, since the totals are indexed by the `(of, wrt)` pair rather than by position (#80). +- Fixed the compilation of the proto files on Windows. ### Documentation & Infrastructure diff --git a/examples/openaerostruct_to_gemseo.py b/examples/openaerostruct_to_gemseo.py new file mode 100644 index 0000000..187d5c0 --- /dev/null +++ b/examples/openaerostruct_to_gemseo.py @@ -0,0 +1,150 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +"""Trim a remote OpenAeroStruct aerostructural discipline with GEMSEO. + +This example starts a gRPC server exposing ``philote_examples``' +``OasAerostructDiscipline`` -- a coupled OpenAeroStruct aerostructural +analysis of a single wing (VLM aerodynamics + FEM structure, run through +OpenMDAO) wrapped as a Philote-MDO explicit discipline -- connects a +:class:`~philote_mdo.gemseo.philote_to_gemseo.PhiloteDiscipline` client to +it, and uses this discipline in a GEMSEO +:class:`~gemseo.scenarios.mdo_scenario.MDOScenario`: find the angle of +attack ``alpha`` that minimizes the drag coefficient ``CD`` while trimming +the wing at a target lift coefficient ``CL = 0.5``, using the +gradient-based SLSQP algorithm. + +The remote discipline only exposes the partial derivatives of ``CL`` and +``CD`` with respect to ``alpha`` (see the two +``declare_subproblem_partial`` calls in +``OasAerostructDiscipline._build_discipline``), so ``alpha`` is the only +usable design variable, and ``CL``/``CD`` the only usable objective and +constraint, for a gradient-based scenario. The other flight-condition and +mission parameters are kept fixed at their default values. + +This requires GEMSEO, OpenMDAO, OpenAeroStruct and ``philote-examples`` to +be installed (``pip install gemseo openaerostruct philote-examples``). +""" + +from __future__ import annotations + +from concurrent import futures + +import grpc +import numpy as np +import philote_mdo.general as pmdo +from gemseo import create_design_space +from gemseo import create_scenario +from gemseo import execute_post +from gemseo.settings.formulations import DisciplinaryOpt_Settings +from gemseo.settings.opt import SLSQP_Settings +from gemseo.settings.post import OptHistoryView_Settings +from numpy import array +from openaerostruct.utils.constants import grav_constant +from philote_examples import OasAerostructDiscipline + +from philote_mdo.gemseo import PhiloteDiscipline + +HOST = "localhost" +"""The host of the Philote discipline server.""" + +PORT = 50052 +"""The port of the Philote discipline server.""" + +FIXED_INPUTS = { + "v": array([248.136]), + "Mach_number": array([0.84]), + "re": array([1e6]), + "rho": array([0.38]), + "CT": array([grav_constant * 17.0e-6]), + "R": array([11.165e6]), + "W0": array([0.4 * 3e5]), + "speed_of_sound": array([295.4]), + "load_factor": array([1.0]), + "empty_cg": np.zeros(3), +} +"""The flight-condition and mission inputs that are not design variables. + +These are set as default input values of the discipline, so that the +scenario only has to provide a value for the "alpha" design variable. +""" + + +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + """Serve the OAS aerostructural discipline and connect a GEMSEO client to it. + + Args: + server: The started gRPC server to attach the OAS aerostructural + Philote discipline to. + + Returns: + A GEMSEO discipline connected to the remote OAS discipline, with + its non-design inputs (flight conditions and mission parameters) + set to their default values. + """ + discipline = pmdo.ExplicitServer(discipline=OasAerostructDiscipline()) + discipline.attach_to_server(server) + oas_discipline = PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) + oas_discipline.default_input_data.update(FIXED_INPUTS) + return oas_discipline + + +def create_server() -> grpc.Server: + """Create and start the gRPC server used to serve Philote disciplines. + + Returns: + The started gRPC server. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server + + +if __name__ == "__main__": + server = create_server() + + try: + oas_disc = create_discipline(server) + + design_space = create_design_space() + # alpha is in degrees, as declared by OasAerostructDiscipline. + design_space.add_variable( + "alpha", lower_bound=-10.0, upper_bound=15.0, value=5.0 + ) + + scenario = create_scenario( + [oas_disc], + objective_name="CD", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + # Trim the wing: CL = 0.5 + scenario.add_constraint("CL", constraint_type="eq", value=0.5) + scenario.execute(algo_settings_model=SLSQP_Settings(max_iter=20)) + + out = oas_disc.local_data + print( + "Trimmed solution:", + {name: out[name] for name in ("alpha", "CL", "CD", "failure")}, + ) + execute_post(scenario, OptHistoryView_Settings(save=True, show=False)) + + finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) diff --git a/examples/paraboloid_gemseo.py b/examples/paraboloid_gemseo.py new file mode 100644 index 0000000..265bec0 --- /dev/null +++ b/examples/paraboloid_gemseo.py @@ -0,0 +1,123 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +"""Optimize a remote Philote discipline with GEMSEO. + +This example starts a gRPC server exposing the +``philote_mdo.examples.Paraboloid`` explicit discipline through the +Philote-MDO protocol, connects a ``philote_mdo.gemseo.PhiloteDiscipline`` +client to it, and uses this discipline like any other GEMSEO discipline in +a mono-disciplinary optimization scenario: minimize ``f_xy(x, y)`` under +the constraint ``0 <= x + y <= 10``. + +This requires GEMSEO to be installed (``pip install gemseo``). +""" + +from __future__ import annotations + +from concurrent import futures + +import grpc +from gemseo import create_design_space +from gemseo import create_scenario +from gemseo.disciplines.auto_py import AutoPyDiscipline +from gemseo.settings.formulations import DisciplinaryOpt_Settings +from gemseo.settings.opt import COBYQA_Settings +from numpy import ndarray + +from philote_mdo.examples import Paraboloid +from philote_mdo.gemseo import PhiloteDiscipline +from philote_mdo.general import ExplicitServer + +HOST = "localhost" +"""The host of the Philote discipline server.""" + +PORT = 50051 +"""The port of the Philote discipline server.""" + + +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + """Serve the Paraboloid discipline and connect a GEMSEO client to it. + + Args: + server: The started gRPC server to attach the Paraboloid Philote + discipline to. + + Returns: + A GEMSEO discipline connected to the remote Paraboloid discipline. + """ + discipline = ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) + + +def create_server() -> grpc.Server: + """Create and start the gRPC server used to serve Philote disciplines. + + Returns: + The started gRPC server. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server + + +def compute_constraint(x: ndarray, y: ndarray) -> ndarray: + """Compute the constraint function ``g``. + + Args: + x: The first paraboloid variable. + y: The second paraboloid variable. + + Returns: + The constraint value ``g``, bounded between 0 and 10 in the scenario. + """ + # AutoPyDiscipline infers the output name from this assignment, so the + # variable must be named "g" and returned as-is (not e.g. "return x + y"). + g = x + y + return g + + +if __name__ == "__main__": + server = create_server() + + try: + paraboloid_disc = create_discipline(server) + design_space = create_design_space() + design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) + design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) + + scenario = create_scenario( + [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], + objective_name="f_xy", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + + # 0 <= g <= 10 + scenario.add_constraint("g", constraint_type="ineq", positive=True) + scenario.add_constraint("g", constraint_type="ineq", value=10.0) + scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) + + print( + "Optimal design found:", design_space.get_current_value(as_dict=True) + ) + finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) diff --git a/examples/sellar_gemseo_to_openmdao.py b/examples/sellar_gemseo_to_openmdao.py new file mode 100644 index 0000000..98b3265 --- /dev/null +++ b/examples/sellar_gemseo_to_openmdao.py @@ -0,0 +1,161 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery +"""Optimize the GEMSEO Sellar problem from OpenMDAO through Philote-MDO. + +This example serves each discipline of GEMSEO's customizable Sellar MDO +problem (``Sellar1``, ``Sellar2``, ``SellarSystem``) as an independent +Philote-MDO gRPC server, using ``philote_mdo.gemseo.GEMSEOtoPhiloteDiscipline``, +and builds an OpenMDAO model that calls them through +``philote_mdo.openmdao.RemoteExplicitComponent``. The algebraic coupling +between ``Sellar1`` and ``Sellar2`` (through ``y_1``, ``y_2``) is resolved +with a Gauss-Seidel solver, and OpenMDAO's SLSQP driver optimizes the +coupled system using the analytic gradients computed by the GEMSEO +disciplines and transferred over gRPC. + +This demonstrates the "OpenMDAO calls a remote GEMSEO discipline" direction +of the ``philote_mdo.gemseo`` integration, complementing ``PhiloteDiscipline`` +(used for the opposite direction: GEMSEO calling a remote discipline, e.g. +served from OpenMDAO -- see ``examples/paraboloid_gemseo.py``). + +The optimal solution of this classic problem is well known: +``x_1 = 0``, ``x_shared = [1.9776, 0]``, ``obj = 3.1834``. + +This requires GEMSEO to be installed (``pip install gemseo``). +""" + +from __future__ import annotations + +from concurrent import futures +from typing import TYPE_CHECKING + +import grpc +import openmdao.api as om +from gemseo.problems.mdo.sellar.sellar_1 import Sellar1 +from gemseo.problems.mdo.sellar.sellar_2 import Sellar2 +from gemseo.problems.mdo.sellar.sellar_system import SellarSystem + +from philote_mdo.gemseo import GEMSEOtoPhiloteDiscipline +from philote_mdo.general import ExplicitServer +from philote_mdo.openmdao import RemoteExplicitComponent + +if TYPE_CHECKING: + from gemseo.core.discipline.discipline import Discipline + +HOST = "localhost" +"""The host of the Philote discipline servers.""" + +PORT_D1 = 50061 +"""The port serving the Sellar1 discipline (computes y_1).""" + +PORT_D2 = 50062 +"""The port serving the Sellar2 discipline (computes y_2).""" + +PORT_SYSTEM = 50063 +"""The port serving the SellarSystem discipline (computes obj, c_1, c_2).""" + + +def create_discipline_server(discipline: Discipline, port: int) -> grpc.Server: + """Serve a GEMSEO discipline as a Philote-MDO explicit discipline. + + Args: + discipline: The GEMSEO discipline to serve. + port: The port to serve it on. + + Returns: + The started gRPC server. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + philote_discipline = GEMSEOtoPhiloteDiscipline(discipline) + ExplicitServer(discipline=philote_discipline).attach_to_server(server) + server.add_insecure_port(f"[::]:{port}") + server.start() + return server + + +def create_remote_component(port: int) -> RemoteExplicitComponent: + """Create an OpenMDAO component connected to a remote Philote discipline. + + Args: + port: The port of the Philote discipline server to connect to. + + Returns: + An OpenMDAO explicit component calling the remote discipline. + """ + return RemoteExplicitComponent(channel=grpc.insecure_channel(f"{HOST}:{port}")) + + +if __name__ == "__main__": + servers = [ + create_discipline_server(Sellar1(), PORT_D1), + create_discipline_server(Sellar2(), PORT_D2), + create_discipline_server(SellarSystem(), PORT_SYSTEM), + ] + + try: + prob = om.Problem() + model = prob.model + + # Sellar1 and Sellar2 are coupled through y_1 and y_2: group them so + # that this algebraic loop can be resolved by a dedicated solver. + cycle = model.add_subsystem("cycle", om.Group(), promotes=["*"]) + cycle.add_subsystem("d1", create_remote_component(PORT_D1), promotes=["*"]) + cycle.add_subsystem("d2", create_remote_component(PORT_D2), promotes=["*"]) + cycle.nonlinear_solver = om.NonlinearBlockGS() + # Compute the exact total derivatives across the coupling loop. + cycle.linear_solver = om.DirectSolver() + + model.add_subsystem( + "system", create_remote_component(PORT_SYSTEM), promotes=["*"] + ) + + model.add_design_var("x_1", lower=0.0, upper=10.0) + model.add_design_var("x_2", lower=0.0, upper=10.0) + model.add_design_var("x_shared", lower=[-10.0, 0.0], upper=[10.0, 10.0]) + model.add_objective("obj") + model.add_constraint("c_1", upper=0.0) + model.add_constraint("c_2", upper=0.0) + + prob.driver = om.ScipyOptimizeDriver(optimizer="SLSQP", tol=1e-8) + + prob.setup() + + # The Philote-MDO protocol only transfers variable names, shapes and + # units, not GEMSEO's default values, so the non-design parameters + # and the design variables' starting point must be set explicitly + # here, matching gemseo's SellarDesignSpace and get_initial_data. + prob.set_val("x_1", 1.0) + prob.set_val("x_2", 1.0) + prob.set_val("x_shared", [4.0, 3.0]) + prob.set_val("alpha", 3.16) + prob.set_val("beta", 24.0) + prob.set_val("gamma", 0.2) + + prob.run_driver() + + print( + "Optimal solution:", + { + name: prob.get_val(name) + for name in ("x_1", "x_2", "x_shared", "obj", "c_1", "c_2") + }, + ) + finally: + # Stop the servers explicitly: + # their worker threads would otherwise keep the process alive. + for server in servers: + server.stop(0) diff --git a/philote_mdo/gemseo/__init__.py b/philote_mdo/gemseo/__init__.py new file mode 100644 index 0000000..5f14781 --- /dev/null +++ b/philote_mdo/gemseo/__init__.py @@ -0,0 +1,31 @@ +# Philote-Python +# +# Copyright 2022-2024 Christopher A. Lupp +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# This work has been cleared for public release, distribution unlimited, case +# number: AFRL-2023-5713. +# +# The views expressed are those of the authors and do not reflect the +# official guidance or position of the United States Government, the +# Department of Defense or of the United States Air Force. +# +# Statement from DoD: The Appearance of external hyperlinks does not +# constitute endorsement by the United States Department of Defense (DoD) of +# the linked websites, of the information, products, or services contained +# therein. The DoD does not exercise any editorial, security, or other +# control over the information you may find at these locations. +from .gemseo_to_philote import GEMSEOtoPhiloteDiscipline +from .philote_to_gemseo import PhiloteDiscipline diff --git a/philote_mdo/gemseo/gemseo_to_philote.py b/philote_mdo/gemseo/gemseo_to_philote.py new file mode 100644 index 0000000..63c3d32 --- /dev/null +++ b/philote_mdo/gemseo/gemseo_to_philote.py @@ -0,0 +1,155 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +"""A Philote explicit discipline wrapping a GEMSEO discipline.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from gemseo.core.serializable import GoogleDocstringInheritanceMeta +from numpy import ndarray +from scipy.sparse import issparse + +from philote_mdo.general.explicit_discipline import ExplicitDiscipline + +if TYPE_CHECKING: + from gemseo.core.discipline.discipline import Discipline + + +class GEMSEOtoPhiloteDiscipline( + ExplicitDiscipline, metaclass=GoogleDocstringInheritanceMeta +): + """A Philote-MDO explicit discipline wrapping a GEMSEO discipline. + + This adapts a GEMSEO :class:`~gemseo.core.discipline.discipline.Discipline` + to the :class:`~philote_mdo.general.explicit_discipline.ExplicitDiscipline` + interface, so that it can be exposed to remote Philote clients (e.g. an + OpenMDAO component or a :class:`~philote_mdo.gemseo.philote_to_gemseo.PhiloteDiscipline`) + through a :class:`~philote_mdo.general.explicit_server.ExplicitServer`, + for example:: + + server = ExplicitServer( + discipline=GEMSEOtoPhiloteDiscipline(my_discipline) + ) + server.attach_to_server(grpc_server) + + All the input and output variables of the wrapped discipline are + exposed as flat (1D) continuous Philote variables, and the Jacobian of + every output with respect to every input is made available, computed + on demand using GEMSEO's own differentiation capabilities + (see :meth:`~gemseo.core.discipline.discipline.Discipline.linearize`). + """ + + gemseo_discipine: Discipline + """The GEMSEO discipline to be wrapped.""" + + default_data_size: int + """The default size used to declare a variable that has no default value.""" + + def __init__(self, gemseo_discipine: Discipline, default_data_size: int = 1): + """Initialize the GEMSEO discipline. + + Args: + gemseo_discipine: The GEMSEO discipline to be wrapped. + default_data_size: The default data size to be declared for an + input or output variable when the wrapped GEMSEO discipline + does not define a default value (as a + :class:`~numpy.ndarray`) for it in its grammars. + """ + super().__init__() + self.gemseo_discipine = gemseo_discipine + self.default_data_size = default_data_size + + def setup(self): + """Declare the Philote inputs and outputs from the GEMSEO grammars. + + Every name of the wrapped GEMSEO discipline's input and output + grammars is declared as a flat (1D) continuous Philote variable. + Its size is taken from the corresponding default value in + :attr:`~gemseo.core.discipline.discipline.Discipline.default_input_data` + or + :attr:`~gemseo.core.discipline.discipline.Discipline.default_output_data` + when it is a :class:`~numpy.ndarray`, and from + :attr:`.default_data_size` otherwise. + """ + disc = self.gemseo_discipine + for input_name in disc.input_grammar.names: + data = disc.default_input_data.get(input_name) + size = self.default_data_size + if isinstance(data, ndarray): + size = data.size + self.add_input(input_name, shape=(size,)) + for output_name in disc.output_grammar.names: + data = disc.default_output_data.get(output_name) + size = self.default_data_size + if isinstance(data, ndarray): + size = data.size + self.add_output(output_name, shape=(size,)) + + def setup_partials(self): + """Declare the Jacobian of every output with respect to every input. + + No sparsity pattern is assumed: all the input-output pairs of the + wrapped GEMSEO discipline are declared as partial derivatives to + be computed by :meth:`.compute_partials`. + """ + for output_name in self.gemseo_discipine.output_grammar.names: + for input_name in self.gemseo_discipine.input_grammar.names: + self.declare_partials(output_name, input_name) + + def compute(self, inputs, outputs): + """Execute the wrapped GEMSEO discipline. + + Args: + inputs: The input values, indexed by input name. + outputs: The dictionary to be filled with the output values + computed by the wrapped GEMSEO discipline, indexed by + output name. + """ + out = self.gemseo_discipine.execute(inputs) + outputs.update({k: out[k] for k in self.gemseo_discipine.output_grammar.names}) + + def compute_partials(self, inputs, partials): + """Linearize the wrapped GEMSEO discipline. + + Args: + inputs: The input values, indexed by input name. + partials: The dictionary to be filled with the Jacobian matrices + computed by the wrapped GEMSEO discipline, indexed by + ``(output_name, input_name)`` pairs. + """ + jac = self.gemseo_discipine.linearize(inputs, compute_all_jacobians=True) + output_names = self.gemseo_discipine.output_grammar.names + input_names = self.gemseo_discipine.input_grammar.names + + for out_k, jac_in in jac.items(): + if out_k not in output_names: + # Some GEMSEO disciplines (e.g. the Sellar ones) populate + # their Jacobian with extra entries beyond their own + # declared outputs; only forward what setup_partials() + # actually declared to the Philote-MDO client. + continue + for in_k, jac_loc in jac_in.items(): + if in_k not in input_names: + continue + # The Philote-MDO wire protocol only transfers dense arrays, + # while a GEMSEO discipline may return a sparse Jacobian + # matrix (e.g. scipy.sparse.dia_matrix). + if issparse(jac_loc): + jac_loc = jac_loc.toarray() + partials[out_k, in_k] = jac_loc diff --git a/philote_mdo/gemseo/philote_to_gemseo.py b/philote_mdo/gemseo/philote_to_gemseo.py new file mode 100644 index 0000000..2768ec0 --- /dev/null +++ b/philote_mdo/gemseo/philote_to_gemseo.py @@ -0,0 +1,165 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +"""A GEMSEO discipline wrapping a remote Philote explicit discipline server.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import ClassVar + +from gemseo.core.discipline.discipline import Discipline +from gemseo.core.grammars.factory import GrammarType +from numpy import prod + +import philote_mdo.general as pm +import philote_mdo.generated.data_pb2 as data + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class PhiloteDiscipline(Discipline): + """A GEMSEO discipline connected to a remote Philote explicit discipline. + + This discipline acts as a Philote-MDO client: at construction, it + connects to a Philote explicit discipline server through a gRPC + channel, triggers the remote ``Setup`` and requests the input/output + and partials metadata. This metadata is used to build the GEMSEO + input and output grammars, so that the resulting discipline can be + used like any other GEMSEO discipline, e.g. in an + :class:`~gemseo.mda.base_mda.BaseMDA`. + + Executing the discipline (:meth:`.execute`) and linearizing it + (:meth:`.linearize`) transparently call the remote server to compute + the outputs and the Jacobian, respectively. + """ + + default_grammar_type: ClassVar[GrammarType] = GrammarType.SIMPLE + """The default type of grammar.""" + + def __init__(self, channel, name="", **options): + """Initialize the discipline and connect its Philote client. + + Args: + channel: The gRPC channel to the Philote discipline server, + e.g. created with ``grpc.insecure_channel("localhost:50051")``. + name: The name of the discipline. + If empty, use the name of the remote discipline class. + **options: The discipline options to send to the server, + if any. + + Raises: + ValueError: When ``channel`` is empty or ``None``. + """ + if not channel: + msg = "No channel provided, the Philote client will not be able to connect." + raise ValueError(msg) + # The shape of each input and output variable, indexed by name; + # filled in by _initialize_grammars() and used by _compute_jacobian() + # to reshape the flattened partial derivatives sent by the server. + self._shapes = {} + # generic Philote client + self._client = pm.ExplicitClient(channel=channel) + + # call the init function of the explicit component + super().__init__(name=name) + + self._client.send_stream_options() + if options: + self._client.send_options(options) + + # run setup + self._client.run_setup() + self._client.get_variable_definitions() + self._client.get_partials_definitions() + self._initialize_grammars() + + def _run(self, input_data: dict) -> dict: + """Compute the function evaluation. + + This sends the input values to the remote Philote discipline server + through the ``ComputeFunction`` RPC and returns the resulting output + values. + + Args: + input_data: The input data, without namespace prefixes. + + Returns: + The output data computed by the remote discipline. + """ + return self._client.run_compute(input_data) + + def _compute_jacobian( + self, + input_names: Iterable[str] = (), + output_names: Iterable[str] = (), + ) -> None: + """Compute one Jacobian matrix per input-output pair. + + This sends the current input values to the remote Philote discipline + server through the ``ComputeGradient`` RPC, reshapes the resulting + flattened partial derivatives according to the input and output + shapes obtained by :meth:`._initialize_grammars`, and stores them + in :attr:`.jac` as a dictionary + ``{output_name: {input_name: jacobian_matrix}}``. + + Note: + The remote discipline always returns the Jacobian of every + declared output with respect to every declared input, so + ``input_names`` and ``output_names`` are currently not used to + restrict the RPC request: the full Jacobian is always computed + and stored. + + Args: + input_names: The names of the inputs against which to differentiate the + outputs. If empty, use all the inputs. + output_names: The names of the outputs to be differentiated. + If empty, use all the outputs. + """ + jac_flat = self._client.run_compute_partials(self.get_input_data()) + for jac_key, jac_val in jac_flat.items(): + out_name, in_name = jac_key + out_size = int(prod(self._shapes[out_name])) + in_size = int(prod(self._shapes[in_name])) + jac_loc = jac_val.reshape(out_size, in_size) + if out_name not in self.jac: + self.jac[out_name] = {in_name: jac_loc} + else: + self.jac[out_name][in_name] = jac_loc + + def _initialize_grammars(self): + """Set up the GEMSEO discipline input and output grammars. + + This builds the input and output grammars, and populates + :attr:`._shapes`, from the variable metadata already retrieved + from the remote discipline server by + :meth:`~philote_mdo.general.discipline_client.DisciplineClient.get_variable_definitions`. + It does not perform any RPC call itself. + """ + input_names = [] + output_names = [] + for var in self._client._var_meta: + if var.type == data.kInput: + input_names.append(var.name) + self._shapes[var.name] = tuple(var.shape) + + if var.type == data.kOutput: + output_names.append(var.name) + self._shapes[var.name] = tuple(var.shape) + self.input_grammar.update_from_names(input_names) + self.output_grammar.update_from_names(output_names) diff --git a/pyproject.toml b/pyproject.toml index 0b45c86..4b2b7e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ Documentation = "https://mdo-standards.github.io/Philote-Python/" [project.optional-dependencies] openmdao = ["openmdao>=3.0"] +gemseo = ["gemseo>=6.0"] dev = [ "pytest>=7.0", "pytest-cov>=4.0", diff --git a/tests/test_gemseo_to_philote.py b/tests/test_gemseo_to_philote.py new file mode 100644 index 0000000..8fe5326 --- /dev/null +++ b/tests/test_gemseo_to_philote.py @@ -0,0 +1,103 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +from concurrent import futures +import unittest +import grpc +from numpy.testing import assert_allclose +from scipy.sparse import issparse +from gemseo.problems.mdo.sellar.sellar_1 import Sellar1 +import philote_mdo.general as pmdo +from philote_mdo.gemseo import GEMSEOtoPhiloteDiscipline +from philote_mdo.gemseo import PhiloteDiscipline + +PORT = "[::]:50051" +CHANNEL = "localhost:50051" + + +class GEMSEOToPhiloteTests(unittest.TestCase): + """ + Integration tests for GEMSEOtoPhiloteDiscipline, which wraps a GEMSEO + discipline as a Philote explicit discipline server. + + A GEMSEO Sellar1 discipline is served, and a PhiloteDiscipline client is + used to check that the remote outputs and Jacobian match those of a + local (non-remote) Sellar1 instance. + """ + + def test_sellar1_compute(self): + """ + The remote GEMSEO discipline produces the same output as a local one. + """ + sellar1 = Sellar1() + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + discipline = pmdo.ExplicitServer( + discipline=GEMSEOtoPhiloteDiscipline(sellar1) + ) + discipline.attach_to_server(server) + server.add_insecure_port(PORT) + server.start() + + remote_sellar1 = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + out = remote_sellar1.execute(sellar1.default_input_data) + out_local = sellar1.execute() + + server.stop(0) + + assert_allclose(out["y_1"], out_local["y_1"]) + + def test_sellar1_linearize(self): + """ + The Jacobian of the remote GEMSEO discipline matches the local one. + """ + sellar1 = Sellar1() + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + discipline = pmdo.ExplicitServer( + discipline=GEMSEOtoPhiloteDiscipline(sellar1) + ) + discipline.attach_to_server(server) + server.add_insecure_port(PORT) + server.start() + + remote_sellar1 = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + remote_sellar1.execute(sellar1.default_input_data) + remote_sellar1.linearize( + sellar1.default_input_data, compute_all_jacobians=True + ) + jac_local = sellar1.linearize( + sellar1.default_input_data, compute_all_jacobians=True + ) + + server.stop(0) + + for input_name in ("x_1", "x_shared", "y_2", "gamma"): + jac_loc = jac_local["y_1"][input_name] + if issparse(jac_loc): + jac_loc = jac_loc.toarray() + assert_allclose( + remote_sellar1.jac["y_1"][input_name].ravel(), + jac_loc.ravel(), + atol=1e-8, + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_openmdao_to_gemseo.py b/tests/test_openmdao_to_gemseo.py new file mode 100644 index 0000000..f6d3628 --- /dev/null +++ b/tests/test_openmdao_to_gemseo.py @@ -0,0 +1,96 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +from concurrent import futures +import unittest +import grpc +from numpy import array +import philote_mdo.general as pmdo +import philote_mdo.openmdao as pmdo_om +from philote_mdo.examples.sellar import SellarMDA +from philote_mdo.gemseo import PhiloteDiscipline + +PORT = "[::]:50051" +CHANNEL = "localhost:50051" + + +def _serve_sellar_mda(server): + """ + Wraps the OpenMDAO SellarMDA group as a Philote explicit discipline and + attaches it to the given (not yet started) gRPC server. + """ + subprob = pmdo_om.OpenMdaoSubProblem() + subprob.add_group(SellarMDA()) + subprob.add_mapped_input("x", "x", shape=(1,), units="") + subprob.add_mapped_input("z", "z", shape=(2,), units="") + subprob.add_mapped_output("obj", "obj", shape=(1,), units="") + subprob.add_mapped_output("con1", "con1", shape=(1,), units="") + subprob.declare_subproblem_partial("obj", "x") + subprob.declare_subproblem_partial("obj", "z") + subprob.declare_subproblem_partial("con1", "x") + subprob.declare_subproblem_partial("con1", "z") + + discipline = pmdo.ExplicitServer(discipline=subprob) + discipline.attach_to_server(server) + + +class OpenMDAOToGEMSEOTests(unittest.TestCase): + """ + Integration tests for the Sellar MDA (an OpenMDAO group wrapped through + OpenMdaoSubProblem) served to a GEMSEO PhiloteDiscipline client. + """ + + def test_sellar_mda_compute(self): + """ + Integration test for the Sellar MDA in GEMSEO. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + _serve_sellar_mda(server) + server.add_insecure_port(PORT) + server.start() + + sellar_mda = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + out = sellar_mda.execute({"x": array([1.0]), "z": array([2.0, 3.0])}) + + server.stop(0) + + self.assertIn("obj", out) + self.assertIn("con1", out) + + def test_sellar_mda_linearize(self): + """ + Integration test for the Sellar MDA Jacobian in GEMSEO. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + _serve_sellar_mda(server) + server.add_insecure_port(PORT) + server.start() + + sellar_mda = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + inputs = {"x": array([1.0]), "z": array([2.0, 3.0])} + sellar_mda.linearize(inputs, compute_all_jacobians=True) + ok = sellar_mda.check_jacobian(inputs, step=1e-7, threshold=1e-4) + + server.stop(0) + + self.assertTrue(ok) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_philote_to_gemseo.py b/tests/test_philote_to_gemseo.py new file mode 100644 index 0000000..45fc9f5 --- /dev/null +++ b/tests/test_philote_to_gemseo.py @@ -0,0 +1,77 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +from concurrent import futures +import unittest +import grpc +from numpy import array +import philote_mdo.general as pmdo +from philote_mdo.examples import Paraboloid +from philote_mdo.gemseo import PhiloteDiscipline + +PORT = "[::]:50051" +CHANNEL = "localhost:50051" + + +class PhiloteToGEMSEOTests(unittest.TestCase): + """ + Integration tests for PhiloteDiscipline, which wraps a remote Philote + explicit discipline server as a GEMSEO discipline. + """ + + def test_paraboloid_compute(self): + """ + Integration test for the Paraboloid compute function. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + discipline = pmdo.ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + server.add_insecure_port(PORT) + server.start() + + paraboloid_disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + out = paraboloid_disc.execute({"x": array([1.0]), "y": array([2.0])}) + + server.stop(0) + + self.assertEqual(out["f_xy"][0], 39.0) + + def test_paraboloid_linearize(self): + """ + Integration test for the Paraboloid linearization, checked against + the analytic Jacobian at (x, y) = (1, 2). + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + discipline = pmdo.ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + server.add_insecure_port(PORT) + server.start() + + paraboloid_disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + inputs = {"x": array([1.0]), "y": array([2.0])} + paraboloid_disc.linearize(inputs, compute_all_jacobians=True) + + server.stop(0) + + self.assertEqual(paraboloid_disc.jac["f_xy"]["x"][0][0], -2.0) + self.assertEqual(paraboloid_disc.jac["f_xy"]["y"][0][0], 13.0) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sellar_gemseo_to_openmdao.py b/tests/test_sellar_gemseo_to_openmdao.py new file mode 100644 index 0000000..ee8e65b --- /dev/null +++ b/tests/test_sellar_gemseo_to_openmdao.py @@ -0,0 +1,167 @@ +# Philote-Python +# +# Copyright 2026 IRT Saint Exupery +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# originally authored by Francois Gallard, IRT Saint Exupery. +from concurrent import futures +import unittest +import grpc +import openmdao.api as om +from gemseo.problems.mdo.sellar.sellar_1 import Sellar1 +from gemseo.problems.mdo.sellar.sellar_2 import Sellar2 +from gemseo.problems.mdo.sellar.sellar_system import SellarSystem +import philote_mdo.general as pmdo +import philote_mdo.openmdao as pmdo_om +from philote_mdo.gemseo import GEMSEOtoPhiloteDiscipline + +PORT_D1 = "[::]:50061" +CHANNEL_D1 = "localhost:50061" +PORT_D2 = "[::]:50062" +CHANNEL_D2 = "localhost:50062" +PORT_SYSTEM = "[::]:50063" +CHANNEL_SYSTEM = "localhost:50063" + + +def _serve(discipline, port): + """ + Serves a GEMSEO discipline as a Philote explicit discipline. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + philote_discipline = GEMSEOtoPhiloteDiscipline(discipline) + pmdo.ExplicitServer(discipline=philote_discipline).attach_to_server(server) + server.add_insecure_port(port) + server.start() + return server + + +class SellarGEMSEOToOpenMDAOTests(unittest.TestCase): + """ + Integration tests for the GEMSEO Sellar MDO problem (Sellar1, Sellar2, + SellarSystem), served as three Philote explicit discipline servers and + driven from an OpenMDAO model through RemoteExplicitComponent. + + The y_1/y_2 algebraic coupling between Sellar1 and Sellar2 is resolved + by a Gauss-Seidel solver, and both the total derivatives and the + gradient-based (SLSQP) optimum of the coupled model are checked. + """ + + def setUp(self): + self._servers = [ + _serve(Sellar1(), PORT_D1), + _serve(Sellar2(), PORT_D2), + _serve(SellarSystem(), PORT_SYSTEM), + ] + + prob = om.Problem() + model = prob.model + + # Sellar1 and Sellar2 are coupled through y_1 and y_2: group them so + # that this algebraic loop can be resolved by a dedicated solver. + cycle = model.add_subsystem("cycle", om.Group(), promotes=["*"]) + cycle.add_subsystem( + "d1", + pmdo_om.RemoteExplicitComponent( + channel=grpc.insecure_channel(CHANNEL_D1) + ), + promotes=["*"], + ) + cycle.add_subsystem( + "d2", + pmdo_om.RemoteExplicitComponent( + channel=grpc.insecure_channel(CHANNEL_D2) + ), + promotes=["*"], + ) + cycle.nonlinear_solver = om.NonlinearBlockGS(iprint=0) + # Compute the exact total derivatives across the coupling loop. + cycle.linear_solver = om.DirectSolver() + + model.add_subsystem( + "system", + pmdo_om.RemoteExplicitComponent( + channel=grpc.insecure_channel(CHANNEL_SYSTEM) + ), + promotes=["*"], + ) + + model.add_design_var("x_1", lower=0.0, upper=10.0) + model.add_design_var("x_2", lower=0.0, upper=10.0) + model.add_design_var("x_shared", lower=[-10.0, 0.0], upper=[10.0, 10.0]) + model.add_objective("obj") + model.add_constraint("c_1", upper=0.0) + model.add_constraint("c_2", upper=0.0) + + prob.setup() + + # The Philote-MDO protocol only transfers variable names, shapes and + # units, not GEMSEO's default values, so they must be set explicitly. + prob.set_val("x_1", 1.0) + prob.set_val("x_2", 1.0) + prob.set_val("x_shared", [4.0, 3.0]) + prob.set_val("alpha", 3.16) + prob.set_val("beta", 24.0) + prob.set_val("gamma", 0.2) + + self._prob = prob + + def tearDown(self): + for server in self._servers: + server.stop(0) + + def test_sellar_derivatives(self): + """ + The total derivatives through the remote GEMSEO disciplines are + correct, checked against finite differences, across the y_1/y_2 + coupling loop and the three remote disciplines. + """ + self._prob.run_model() + + check_data = self._prob.check_totals( + of=["obj", "c_1", "c_2"], + wrt=["x_1", "x_2", "x_shared"], + out_stream=None, + ) + + for (of, wrt), error in check_data.items(): + rel_error = error["rel error"] + value = rel_error.forward + if value is None: + value = rel_error.reverse + self.assertLess(value, 1e-4, "d{}/d{}: relative error {} too large.".format(of, wrt, value)) + + def test_sellar_optimum(self): + """ + SLSQP finds the well-known optimum of the Sellar problem: + obj = 3.18339, x_1 = 0, x_shared = [1.9776, 0], with c_1 active. + + Reference values from Sellar, R., Batill, S., and Renaud, J. + (1996), "Response surface based, concurrent subspace optimization + for multidisciplinary system design". + """ + self._prob.driver = om.ScipyOptimizeDriver(optimizer="SLSQP", tol=1e-8) + + self._prob.run_driver() + + self.assertAlmostEqual(self._prob.get_val("obj")[0], 3.18339, places=4) + self.assertAlmostEqual(self._prob.get_val("x_1")[0], 0.0, places=3) + self.assertAlmostEqual(self._prob.get_val("x_shared")[0], 1.9776, places=3) + self.assertAlmostEqual(self._prob.get_val("x_shared")[1], 0.0, places=3) + # The constraints shall be satisfied, c_1 being active at the optimum. + self.assertAlmostEqual(self._prob.get_val("c_1")[0], 0.0, places=4) + self.assertLess(self._prob.get_val("c_2")[0], 0.0) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/utils/compile_proto.py b/utils/compile_proto.py index c7e625e..dc3a6ce 100644 --- a/utils/compile_proto.py +++ b/utils/compile_proto.py @@ -38,35 +38,49 @@ def main(): print("Compiling proto files.") proto_include = os.path.join(resources.files("grpc_tools"), "_proto") + generated_dir = "./philote_mdo/generated/" # proto files proto_files = ["data.proto", "disciplines.proto"] + # A FileDescriptorSet, generated alongside the Python code below, lets + # protoletariat rewrite the generated imports without shelling out to a + # standalone "protoc" executable, which most environments do not have on + # PATH (grpc_tools bundles its own protoc, used in-process below). + descriptor_set_path = os.path.join(generated_dir, "_descriptor.bin") + # compile the proto files for use in python - grpc_tools.protoc.main( + return_code = grpc_tools.protoc.main( [ "grpc_tools.protoc", "-I{}".format(proto_include), "-I{}".format("./proto"), - "--python_out=./philote_mdo/generated/", - "--pyi_out=./philote_mdo/generated/", - "--grpc_python_out=./philote_mdo/generated/", + "--python_out={}".format(generated_dir), + "--pyi_out={}".format(generated_dir), + "--grpc_python_out={}".format(generated_dir), + "--descriptor_set_out={}".format(descriptor_set_path), + "--include_imports", ] + proto_files ) + if return_code != 0: + msg = "Compiling the proto files failed (see the protoc output above)." + raise RuntimeError(msg) - - # call protoletariat to convert absolute imports to relative ones - protol.main( - [ - "--in-place", - "--dont-create-package", - "--python-out=./philote_mdo/generated/", - "protoc", - "--proto-path=./proto", - ] - + proto_files - ) + try: + # call protoletariat to convert absolute imports to relative ones, + # using the FileDescriptorSet generated above. + protol.main( + [ + "--python-out={}".format(generated_dir), + "--in-place", + "--dont-create-package", + "raw", + descriptor_set_path, + ] + ) + finally: + os.remove(descriptor_set_path) if __name__ == "__main__": From 1f2abfffd4bc0b9e328af812df1095b655fbc552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gallard?= Date: Wed, 9 Sep 2026 21:56:38 +0200 Subject: [PATCH 02/16] Add GEMSEO tutorials to the Docusaurus site Add a "Working with GEMSEO" section with two tutorials, ported from the retired Jupyter Book docs: using PhiloteDiscipline to optimize a remote Paraboloid discipline, and coupling OpenAeroStruct with GEMSEO through an OpenMDAO sub-problem. Enable @docusaurus/theme-mermaid so the flowchart diagrams on these pages render. Note: docs/package-lock.json was not regenerated (no Node/npm available in this environment) -- run `npm install` in docs/ before the next `npm ci` build. --- docs/docs/gemseo/gemseo-openaerostruct.md | 208 +++++++++++++++++ docs/docs/gemseo/gemseo.md | 263 ++++++++++++++++++++++ docs/docusaurus.config.ts | 5 + docs/package.json | 1 + docs/sidebars.ts | 8 + 5 files changed, 485 insertions(+) create mode 100644 docs/docs/gemseo/gemseo-openaerostruct.md create mode 100644 docs/docs/gemseo/gemseo.md diff --git a/docs/docs/gemseo/gemseo-openaerostruct.md b/docs/docs/gemseo/gemseo-openaerostruct.md new file mode 100644 index 0000000..c9a8549 --- /dev/null +++ b/docs/docs/gemseo/gemseo-openaerostruct.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 2 +title: "Coupling OpenAeroStruct and GEMSEO" +--- + +# Coupling OpenAeroStruct and GEMSEO through Philote-MDO + +This tutorial builds on [Using GEMSEO with Philote-MDO](./gemseo.md) and +walks through +[`examples/openaerostruct_to_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/openaerostruct_to_gemseo.py), +a more realistic case: instead of a toy analytic function, the remote +discipline is an [OpenAeroStruct](https://github.com/mdolab/OpenAeroStruct) +aerostructural analysis -- a coupled VLM aerodynamic model and a +finite-element structural model, solved together with an OpenMDAO +nonlinear solver -- and it is optimized with a gradient-based algorithm. + +This example additionally requires OpenMDAO, OpenAeroStruct and the +[`philote-examples`](https://pypi.org/project/philote-examples/) package +(`pip install openaerostruct philote-examples`), which provides the +`OasAerostructDiscipline` used below. + +## Why this matters: hiding an OpenMDAO MDA behind a Philote-MDO discipline + +OpenAeroStruct is written for [OpenMDAO](https://openmdao.org): the +`philote-examples` package provides `OasAerostructDiscipline`, which +builds an OpenMDAO `Group` combining `AerostructGeometry` and +`AerostructPoint` for a wing, resolves the aero-structural coupling +internally with `NonlinearBlockGS`, and wraps the whole `om.Problem` as a +**single Philote-MDO explicit discipline**. + +This is the key interoperability point: from the Philote-MDO (and +therefore GEMSEO) side, this coupled, iterative OpenMDAO analysis is +completely invisible. `PhiloteDiscipline` only ever sees a stateless +function `inputs -> outputs` plus a Jacobian -- exactly like the +`Paraboloid` discipline of the [previous tutorial](./gemseo.md). GEMSEO +does not need OpenMDAO, OpenAeroStruct, or any of their dependencies +installed: it only talks gRPC to a server that happens to run them. + +```mermaid +flowchart LR + subgraph GEMSEO_process["GEMSEO process"] + PD["PhiloteDiscipline"] + SC["MDOScenario
(SLSQP)"] + SC --> PD + end + subgraph Server_process["Server process"] + OAS["OasAerostructDiscipline
(Philote explicit discipline)"] + subgraph OM["OpenMDAO Problem"] + VLM["VLM aerodynamics"] + FEM["FEM structure"] + VLM <-->|"NonlinearBlockGS"| FEM + end + OAS --> OM + end + PD <-- "gRPC: alpha -> CL, CD, ...
+ Jacobian" --> OAS +``` + +## A gradient-based scenario is limited to what the server differentiates + +`OasAerostructDiscipline` only declares two partial derivatives on the +server side (in its `_build_discipline` method): + +```python +self.declare_subproblem_partial("CD", "alpha") +self.declare_subproblem_partial("CL", "alpha") +``` + +Only declared partials are computed and sent to the client: a +gradient-based GEMSEO scenario can therefore only use `alpha` as a design +variable, and `CL`/`CD` as objective/constraint candidates -- using any +other input as a design variable, or any other output as an objective or +constraint, would make GEMSEO request a Jacobian entry the server never +computes. + +This shapes the scenario: **trim the wing** by finding the angle of +attack `alpha` that **minimizes drag** `CD` while holding the **lift +coefficient** `CL` at a target value, a classic aerostructural design +problem. + +## Walkthrough + +### Fixing the non-design inputs + +The Philote-MDO protocol only transfers variable names, shapes and units +over gRPC -- not GEMSEO's default values. Since `alpha` is the only +design variable, every other input of the discipline (flight conditions +and mission parameters) must be given a value explicitly, or GEMSEO would +have no default to execute the discipline with. This is done once, by +updating the connected discipline's `default_input_data`: + +```python +FIXED_INPUTS = { + "v": array([248.136]), + "Mach_number": array([0.84]), + "re": array([1e6]), + "rho": array([0.38]), + "CT": array([grav_constant * 17.0e-6]), + "R": array([11.165e6]), + "W0": array([0.4 * 3e5]), + "speed_of_sound": array([295.4]), + "load_factor": array([1.0]), + "empty_cg": np.zeros(3), +} + + +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + discipline = pmdo.ExplicitServer(discipline=OasAerostructDiscipline()) + discipline.attach_to_server(server) + oas_discipline = PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) + oas_discipline.default_input_data.update(FIXED_INPUTS) + return oas_discipline +``` + +:::note +This is not specific to GEMSEO: any Philote-MDO client that does not set +a value for a non-design input runs into the same issue. The +[Sellar example](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py), +which drives remote *GEMSEO* disciplines from *OpenMDAO*, sets its fixed +parameters explicitly with `prob.set_val(...)` for the exact same reason. +::: + +### Serving the discipline + +As in the previous tutorial, serving the discipline only requires +starting a gRPC server and attaching an `ExplicitServer` wrapping it: + +```python +def create_server() -> grpc.Server: + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server +``` + +### Building and running the scenario + +The design space has a single variable, `alpha` (in degrees); the +scenario minimizes `CD` under the equality constraint `CL = 0.5`, using +the gradient-based `SLSQP` algorithm -- made possible because the server +provides the `CL`/`CD` derivatives with respect to `alpha` analytically: + +```python +if __name__ == "__main__": + server = create_server() + + try: + oas_disc = create_discipline(server) + + design_space = create_design_space() + # alpha is in degrees, as declared by OasAerostructDiscipline. + design_space.add_variable( + "alpha", lower_bound=-10.0, upper_bound=15.0, value=5.0 + ) + + scenario = create_scenario( + [oas_disc], + objective_name="CD", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + # Trim the wing: CL = 0.5 + scenario.add_constraint("CL", constraint_type="eq", value=0.5) + scenario.execute(algo_settings_model=SLSQP_Settings(max_iter=20)) + + out = oas_disc.local_data + print( + "Trimmed solution:", + {name: out[name] for name in ("alpha", "CL", "CD", "failure")}, + ) + execute_post(scenario, OptHistoryView_Settings(save=True, show=False)) + + finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) +``` + +Running the script converges in a handful of iterations and prints: + +```text +Trimmed solution: { + 'alpha': array([4.07490213]), + 'CL': array([0.5]), + 'CD': array([0.03553854]), + 'failure': array([-0.91249284]), +} +``` + +`CL` reaches the target `0.5` exactly (the equality constraint fully +determines `alpha` here, since there is a single design variable), and +`failure` stays negative, meaning the structure remains within its +allowable stress margin at the trimmed condition. + +## The full script + +See the actual, always up-to-date source at +[`examples/openaerostruct_to_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/openaerostruct_to_gemseo.py) +in the repository. + +## Where to go next + +[`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py) +shows the mirror image of this tutorial: instead of GEMSEO calling an +OpenMDAO/OpenAeroStruct discipline, GEMSEO's own Sellar disciplines are +served over gRPC with `GEMSEOtoPhiloteDiscipline` and optimized from an +OpenMDAO model with SLSQP -- the same coupling pattern used here (an +internal Gauss-Seidel solver resolving an algebraic loop behind the +scenes), but with the two frameworks swapped. diff --git a/docs/docs/gemseo/gemseo.md b/docs/docs/gemseo/gemseo.md new file mode 100644 index 0000000..be03dcc --- /dev/null +++ b/docs/docs/gemseo/gemseo.md @@ -0,0 +1,263 @@ +--- +sidebar_position: 1 +title: "Using GEMSEO with Philote-MDO" +--- + +# Using GEMSEO with Philote-MDO + +## What problem does this solve? + +[Philote-MDO](https://github.com/MDO-Standards/Philote-Python) is a gRPC +protocol for exchanging MDO disciplines between tools. A discipline is +served by one process (possibly on another machine, in another language, +or built with another MDO framework) and called by a client process as if +it were local: the client sends input values, and the server streams back +the output values and, if available, the Jacobian. + +`philote_mdo.gemseo` implements this protocol on the GEMSEO side, in +**both** directions: + +- `PhiloteDiscipline` is a GEMSEO `Discipline` that connects to *any* + Philote-MDO server (be it a `philote-mdo` discipline, an OpenAeroStruct + model, or an OpenMDAO component served through + `philote_mdo.openmdao.RemoteExplicitComponent`) and exposes it as a + normal discipline, usable in an `MDOScenario` or an `MDA` like any + other. +- `GEMSEOtoPhiloteDiscipline` does the opposite: it wraps a GEMSEO + `Discipline` so that it can be served over gRPC and called from any + Philote-MDO client, in particular an OpenMDAO model. + +```mermaid +flowchart LR + subgraph GEMSEO_process["GEMSEO process"] + PD["PhiloteDiscipline"] + end + subgraph Server_process["Remote server process"] + S["Philote-MDO server
(any framework)"] + end + PD <-- "gRPC: values + Jacobian" --> S + + subgraph GEMSEO_process2["GEMSEO process"] + D["A GEMSEO Discipline"] + GTP["GEMSEOtoPhiloteDiscipline"] + D --> GTP + end + subgraph Client_process["Remote client process"] + C["Philote-MDO client
(e.g. OpenMDAO)"] + end + GTP <-- "gRPC: values + Jacobian" --> C +``` + +Both directions carry the discipline's Jacobian, not just its output +values, so a remote discipline can be used in a gradient-based +optimization exactly like a local one. + +This page walks through the first, and simplest, direction -- +`PhiloteDiscipline` consuming a remote discipline -- using the complete +example [`examples/paraboloid_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/paraboloid_gemseo.py). +The [next tutorial](./gemseo-openaerostruct.md) covers a realistic, +coupled OpenMDAO analysis and the opposite direction is demonstrated in +[`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py). + +## Installation + +```bash +pip install philote-mdo gemseo +``` + +`philote_mdo.gemseo` (which provides `PhiloteDiscipline` and +`GEMSEOtoPhiloteDiscipline`) is part of `philote-mdo` itself; GEMSEO is +not a hard dependency of the package, so it must be installed separately +(or via the `philote-mdo[gemseo]` extra). + +## Walkthrough: optimizing a remote Paraboloid discipline + +The example serves `philote_mdo.examples.Paraboloid`, a toy discipline +computing `f_xy(x, y)`, over gRPC, and uses it as the objective of a +GEMSEO optimization scenario that also has a constraint computed locally. +The full script is reproduced at the end of this page; the sections below +explain it piece by piece. + +### Serving the discipline + +Any Philote-MDO discipline (here, the built-in `Paraboloid` example) is +served by wrapping it in a `philote_mdo.general.ExplicitServer` and +attaching that server to a started gRPC server: + +```python +def create_server() -> grpc.Server: + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server +``` + +### Connecting a GEMSEO client to it + +`PhiloteDiscipline` is the GEMSEO-side client. Connecting it to the +channel above is enough: at construction, it queries the server for the +discipline's inputs, outputs and available partial derivatives, and +builds its GEMSEO grammars from that metadata -- no manual declaration is +needed. + +```python +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + discipline = ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) +``` + +From this point on, `paraboloid_disc` behaves like any other GEMSEO +`Discipline`: `paraboloid_disc.execute(...)` calls the remote +`ComputeFunction` RPC, and `paraboloid_disc.linearize(...)` calls the +remote `ComputeGradient` RPC. + +### Adding a local constraint + +Not every discipline of a scenario has to be remote. Here, the +constraint `g = x + y` is computed locally through GEMSEO's +`AutoPyDiscipline`, which turns a plain Python function into a +discipline by inspecting its signature and its `return` statement: + +```python +def compute_constraint(x: ndarray, y: ndarray) -> ndarray: + # AutoPyDiscipline infers the output name from this assignment, so the + # variable must be named "g" and returned as-is (not e.g. "return x + y"). + g = x + y + return g +``` + +:::warning +`AutoPyDiscipline` infers the output name(s) by parsing the literal +`return` statement of the function, so it must be `return g` with `g` +assigned beforehand, not an inlined expression like `return x + y`. +::: + +### Running the scenario + +The remote discipline and the local constraint discipline are combined in +a standard `MDOScenario`, built with `create_scenario`: + +```python +server = create_server() + +try: + paraboloid_disc = create_discipline(server) + design_space = create_design_space() + design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) + design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) + + scenario = create_scenario( + [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], + objective_name="f_xy", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + + # 0 <= g <= 10 + scenario.add_constraint("g", constraint_type="ineq", positive=True) + scenario.add_constraint("g", constraint_type="ineq", value=10.0) + scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) + + print("Optimal design found:", design_space.get_current_value(as_dict=True)) +finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) +``` + +Running the script prints the optimal design found by COBYQA: + +```text +Optimal design found: {'x': array([7.]), 'y': array([-7.])} +``` + +which matches `f_xy(7, -7) = -27`, the known minimum of the paraboloid +under `0 <= x + y <= 10` (here reached at the boundary `g = 0`). + +## The full script + +```python title="examples/paraboloid_gemseo.py" +from __future__ import annotations + +from concurrent import futures + +import grpc +from gemseo import create_design_space +from gemseo import create_scenario +from gemseo.disciplines.auto_py import AutoPyDiscipline +from gemseo.settings.formulations import DisciplinaryOpt_Settings +from gemseo.settings.opt import COBYQA_Settings +from numpy import ndarray + +from philote_mdo.examples import Paraboloid +from philote_mdo.gemseo import PhiloteDiscipline +from philote_mdo.general import ExplicitServer + +HOST = "localhost" +PORT = 50051 + + +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + discipline = ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) + + +def create_server() -> grpc.Server: + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server + + +def compute_constraint(x: ndarray, y: ndarray) -> ndarray: + # AutoPyDiscipline infers the output name from this assignment, so the + # variable must be named "g" and returned as-is (not e.g. "return x + y"). + g = x + y + return g + + +if __name__ == "__main__": + server = create_server() + + try: + paraboloid_disc = create_discipline(server) + design_space = create_design_space() + design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) + design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) + + scenario = create_scenario( + [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], + objective_name="f_xy", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + + # 0 <= g <= 10 + scenario.add_constraint("g", constraint_type="ineq", positive=True) + scenario.add_constraint("g", constraint_type="ineq", value=10.0) + scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) + + print("Optimal design found:", design_space.get_current_value(as_dict=True)) + finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) +``` + +See the actual, always up-to-date source at +[`examples/paraboloid_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/paraboloid_gemseo.py) +in the repository. + +## Where to go next + +- [Coupling OpenAeroStruct and GEMSEO](./gemseo-openaerostruct.md) applies + the same `PhiloteDiscipline` pattern to a realistic, internally-coupled + OpenMDAO analysis, and shows how the discipline's non-design inputs + must be set explicitly since Philote-MDO does not transfer GEMSEO's + default values. +- [`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py) + demonstrates the opposite direction with `GEMSEOtoPhiloteDiscipline`: + GEMSEO's Sellar disciplines are served over gRPC and optimized from an + OpenMDAO model. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 89eb25d..587a113 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -27,6 +27,11 @@ const config: Config = { locales: ["en"], }, + markdown: { + mermaid: true, + }, + themes: ["@docusaurus/theme-mermaid"], + stylesheets: [ { href: "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css", diff --git a/docs/package.json b/docs/package.json index 77fdde2..03d7252 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,6 +17,7 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 421f70f..4206142 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -27,6 +27,14 @@ const sidebars: SidebarsConfig = { "openmdao/openmdao-groups", ], }, + { + type: "category", + label: "Working with GEMSEO", + items: [ + "gemseo/gemseo", + "gemseo/gemseo-openaerostruct", + ], + }, { type: "category", label: "About", From fda56c00edb429189609f96b48df2f455c9b3660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gallard?= Date: Tue, 15 Sep 2026 13:31:15 +0200 Subject: [PATCH 03/16] Minor changes : remove changes on doc config, and update gemseo dep version in pyproject.toml --- docs/docusaurus.config.ts | 5 ----- docs/package.json | 1 - pyproject.toml | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 587a113..89eb25d 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -27,11 +27,6 @@ const config: Config = { locales: ["en"], }, - markdown: { - mermaid: true, - }, - themes: ["@docusaurus/theme-mermaid"], - stylesheets: [ { href: "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css", diff --git a/docs/package.json b/docs/package.json index 03d7252..77fdde2 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,7 +17,6 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", - "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", diff --git a/pyproject.toml b/pyproject.toml index 4b2b7e2..cf4e65e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ Documentation = "https://mdo-standards.github.io/Philote-Python/" [project.optional-dependencies] openmdao = ["openmdao>=3.0"] -gemseo = ["gemseo>=6.0"] +gemseo = ["gemseo>=6.0,<7.0"] dev = [ "pytest>=7.0", "pytest-cov>=4.0", From 2debb0c86c26728bfd12797fe0ea225e1811bd5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gallard?= Date: Tue, 15 Sep 2026 13:47:24 +0200 Subject: [PATCH 04/16] Add tests to close coverage gaps in philote_mdo.gemseo Covers the ValueError on a missing channel, sending discipline options to the server, the ndarray-sized default_output_data branch in GEMSEOtoPhiloteDiscipline.setup(), and the defensive skip of Jacobian entries for outputs outside a wrapped discipline's own output grammar. Also marks the TYPE_CHECKING-only imports as pragma: no cover, since they can never execute at runtime. Co-Authored-By: Claude Sonnet 5 --- philote_mdo/gemseo/gemseo_to_philote.py | 2 +- philote_mdo/gemseo/philote_to_gemseo.py | 2 +- tests/test_gemseo_to_philote.py | 42 +++++++++++++++++++++++++ tests/test_philote_to_gemseo.py | 32 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/philote_mdo/gemseo/gemseo_to_philote.py b/philote_mdo/gemseo/gemseo_to_philote.py index 63c3d32..8e33c39 100644 --- a/philote_mdo/gemseo/gemseo_to_philote.py +++ b/philote_mdo/gemseo/gemseo_to_philote.py @@ -27,7 +27,7 @@ from philote_mdo.general.explicit_discipline import ExplicitDiscipline -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from gemseo.core.discipline.discipline import Discipline diff --git a/philote_mdo/gemseo/philote_to_gemseo.py b/philote_mdo/gemseo/philote_to_gemseo.py index 2768ec0..7b643b8 100644 --- a/philote_mdo/gemseo/philote_to_gemseo.py +++ b/philote_mdo/gemseo/philote_to_gemseo.py @@ -29,7 +29,7 @@ import philote_mdo.general as pm import philote_mdo.generated.data_pb2 as data -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from collections.abc import Iterable diff --git a/tests/test_gemseo_to_philote.py b/tests/test_gemseo_to_philote.py index 8fe5326..cca321c 100644 --- a/tests/test_gemseo_to_philote.py +++ b/tests/test_gemseo_to_philote.py @@ -17,7 +17,9 @@ # originally authored by Francois Gallard, IRT Saint Exupery. from concurrent import futures import unittest +from unittest.mock import patch import grpc +from numpy import array from numpy.testing import assert_allclose from scipy.sparse import issparse from gemseo.problems.mdo.sellar.sellar_1 import Sellar1 @@ -98,6 +100,46 @@ def test_sellar1_linearize(self): atol=1e-8, ) + def test_setup_uses_default_output_data_size(self): + """ + The size of a Philote output declared by setup() is taken from the + wrapped discipline's default_output_data when it holds an ndarray, + instead of falling back to default_data_size. + """ + sellar1 = Sellar1() + sellar1.default_output_data.update({"y_1": array([0.0, 0.0])}) + + wrapper = GEMSEOtoPhiloteDiscipline(sellar1) + wrapper.setup() + + y_1_meta = next(m for m in wrapper._var_meta if m.name == "y_1") + self.assertEqual(y_1_meta.shape, [2]) + + def test_compute_partials_skips_output_not_in_grammar(self): + """ + Jacobian entries returned by the wrapped discipline for an output + that is not part of its own output grammar are ignored, rather than + being forwarded to the Philote-MDO client. + """ + sellar1 = Sellar1() + wrapper = GEMSEOtoPhiloteDiscipline(sellar1) + wrapper.setup() + wrapper.setup_partials() + + inputs = sellar1.default_input_data + real_jac = sellar1.linearize(inputs, compute_all_jacobians=True) + jac_with_extra_output = dict(real_jac) + jac_with_extra_output["not_a_declared_output"] = { + "x_1": array([[1.0]]) + } + + partials = {} + with patch.object(sellar1, "linearize", return_value=jac_with_extra_output): + wrapper.compute_partials(inputs, partials) + + self.assertIn(("y_1", "x_1"), partials) + self.assertNotIn(("not_a_declared_output", "x_1"), partials) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_philote_to_gemseo.py b/tests/test_philote_to_gemseo.py index 45fc9f5..72e9f77 100644 --- a/tests/test_philote_to_gemseo.py +++ b/tests/test_philote_to_gemseo.py @@ -21,6 +21,7 @@ from numpy import array import philote_mdo.general as pmdo from philote_mdo.examples import Paraboloid +from philote_mdo.examples import Rosenbrock from philote_mdo.gemseo import PhiloteDiscipline PORT = "[::]:50051" @@ -72,6 +73,37 @@ def test_paraboloid_linearize(self): self.assertEqual(paraboloid_disc.jac["f_xy"]["x"][0][0], -2.0) self.assertEqual(paraboloid_disc.jac["f_xy"]["y"][0][0], 13.0) + def test_missing_channel_raises_value_error(self): + """ + Constructing a PhiloteDiscipline without a channel raises ValueError. + """ + with self.assertRaises(ValueError): + PhiloteDiscipline(channel=None) + + with self.assertRaises(ValueError): + PhiloteDiscipline(channel="") + + def test_rosenbrock_with_options(self): + """ + Discipline options passed to the constructor are sent to the server + and used to build a Rosenbrock discipline of the requested dimension. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + discipline = pmdo.ExplicitServer(discipline=Rosenbrock()) + discipline.attach_to_server(server) + server.add_insecure_port(PORT) + server.start() + + rosenbrock_disc = PhiloteDiscipline( + channel=grpc.insecure_channel(CHANNEL), dimension=3 + ) + + out = rosenbrock_disc.execute({"x": array([1.0, 1.0, 1.0])}) + + server.stop(0) + + self.assertEqual(out["f"][0], 0.0) + if __name__ == "__main__": unittest.main(verbosity=2) From a3d31854c6abac450044e5979b73334f0094c5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gallard?= Date: Tue, 15 Sep 2026 18:11:03 +0200 Subject: [PATCH 05/16] Add gemseo in main doc pages --- docs/src/pages/index.tsx | 9 +- .../gemseo/gemseo-openaerostruct.md | 208 ++++++++++++++ .../version-0.8.0/gemseo/gemseo.md | 263 ++++++++++++++++++ .../version-0.8.0-sidebars.json | 8 + 4 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md create mode 100644 docs/versioned_docs/version-0.8.0/gemseo/gemseo.md diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 2c21b42..9f46318 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -249,11 +249,17 @@ const features: FeatureItem[] = [ "First-class support for both explicit components and residual-based implicit disciplines, with analytic partial derivatives.", }, { - label: "INTEROP", + label: "INTEROPERABILITY : OpenMDAO", title: "OpenMDAO Bindings", description: "Drop-in RemoteExplicitComponent and RemoteImplicitComponent for OpenMDAO models, plus a wrapper to host OpenMDAO groups as Philote servers.", }, + { + label: "INTEROPERABILITY : GEMSEO", + title: "GEMSEO Bindings", + description: + "Drop-in GEMSEO Disciplines for Philote Disciplines, plus a wrapper to host GEMSEO Disciplines or processes as Philote servers.", + }, { label: "DISTRIBUTED", title: "Network Native", @@ -357,6 +363,7 @@ export default function Home(): ReactNode { + diff --git a/docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md b/docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md new file mode 100644 index 0000000..c9a8549 --- /dev/null +++ b/docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 2 +title: "Coupling OpenAeroStruct and GEMSEO" +--- + +# Coupling OpenAeroStruct and GEMSEO through Philote-MDO + +This tutorial builds on [Using GEMSEO with Philote-MDO](./gemseo.md) and +walks through +[`examples/openaerostruct_to_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/openaerostruct_to_gemseo.py), +a more realistic case: instead of a toy analytic function, the remote +discipline is an [OpenAeroStruct](https://github.com/mdolab/OpenAeroStruct) +aerostructural analysis -- a coupled VLM aerodynamic model and a +finite-element structural model, solved together with an OpenMDAO +nonlinear solver -- and it is optimized with a gradient-based algorithm. + +This example additionally requires OpenMDAO, OpenAeroStruct and the +[`philote-examples`](https://pypi.org/project/philote-examples/) package +(`pip install openaerostruct philote-examples`), which provides the +`OasAerostructDiscipline` used below. + +## Why this matters: hiding an OpenMDAO MDA behind a Philote-MDO discipline + +OpenAeroStruct is written for [OpenMDAO](https://openmdao.org): the +`philote-examples` package provides `OasAerostructDiscipline`, which +builds an OpenMDAO `Group` combining `AerostructGeometry` and +`AerostructPoint` for a wing, resolves the aero-structural coupling +internally with `NonlinearBlockGS`, and wraps the whole `om.Problem` as a +**single Philote-MDO explicit discipline**. + +This is the key interoperability point: from the Philote-MDO (and +therefore GEMSEO) side, this coupled, iterative OpenMDAO analysis is +completely invisible. `PhiloteDiscipline` only ever sees a stateless +function `inputs -> outputs` plus a Jacobian -- exactly like the +`Paraboloid` discipline of the [previous tutorial](./gemseo.md). GEMSEO +does not need OpenMDAO, OpenAeroStruct, or any of their dependencies +installed: it only talks gRPC to a server that happens to run them. + +```mermaid +flowchart LR + subgraph GEMSEO_process["GEMSEO process"] + PD["PhiloteDiscipline"] + SC["MDOScenario
(SLSQP)"] + SC --> PD + end + subgraph Server_process["Server process"] + OAS["OasAerostructDiscipline
(Philote explicit discipline)"] + subgraph OM["OpenMDAO Problem"] + VLM["VLM aerodynamics"] + FEM["FEM structure"] + VLM <-->|"NonlinearBlockGS"| FEM + end + OAS --> OM + end + PD <-- "gRPC: alpha -> CL, CD, ...
+ Jacobian" --> OAS +``` + +## A gradient-based scenario is limited to what the server differentiates + +`OasAerostructDiscipline` only declares two partial derivatives on the +server side (in its `_build_discipline` method): + +```python +self.declare_subproblem_partial("CD", "alpha") +self.declare_subproblem_partial("CL", "alpha") +``` + +Only declared partials are computed and sent to the client: a +gradient-based GEMSEO scenario can therefore only use `alpha` as a design +variable, and `CL`/`CD` as objective/constraint candidates -- using any +other input as a design variable, or any other output as an objective or +constraint, would make GEMSEO request a Jacobian entry the server never +computes. + +This shapes the scenario: **trim the wing** by finding the angle of +attack `alpha` that **minimizes drag** `CD` while holding the **lift +coefficient** `CL` at a target value, a classic aerostructural design +problem. + +## Walkthrough + +### Fixing the non-design inputs + +The Philote-MDO protocol only transfers variable names, shapes and units +over gRPC -- not GEMSEO's default values. Since `alpha` is the only +design variable, every other input of the discipline (flight conditions +and mission parameters) must be given a value explicitly, or GEMSEO would +have no default to execute the discipline with. This is done once, by +updating the connected discipline's `default_input_data`: + +```python +FIXED_INPUTS = { + "v": array([248.136]), + "Mach_number": array([0.84]), + "re": array([1e6]), + "rho": array([0.38]), + "CT": array([grav_constant * 17.0e-6]), + "R": array([11.165e6]), + "W0": array([0.4 * 3e5]), + "speed_of_sound": array([295.4]), + "load_factor": array([1.0]), + "empty_cg": np.zeros(3), +} + + +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + discipline = pmdo.ExplicitServer(discipline=OasAerostructDiscipline()) + discipline.attach_to_server(server) + oas_discipline = PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) + oas_discipline.default_input_data.update(FIXED_INPUTS) + return oas_discipline +``` + +:::note +This is not specific to GEMSEO: any Philote-MDO client that does not set +a value for a non-design input runs into the same issue. The +[Sellar example](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py), +which drives remote *GEMSEO* disciplines from *OpenMDAO*, sets its fixed +parameters explicitly with `prob.set_val(...)` for the exact same reason. +::: + +### Serving the discipline + +As in the previous tutorial, serving the discipline only requires +starting a gRPC server and attaching an `ExplicitServer` wrapping it: + +```python +def create_server() -> grpc.Server: + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server +``` + +### Building and running the scenario + +The design space has a single variable, `alpha` (in degrees); the +scenario minimizes `CD` under the equality constraint `CL = 0.5`, using +the gradient-based `SLSQP` algorithm -- made possible because the server +provides the `CL`/`CD` derivatives with respect to `alpha` analytically: + +```python +if __name__ == "__main__": + server = create_server() + + try: + oas_disc = create_discipline(server) + + design_space = create_design_space() + # alpha is in degrees, as declared by OasAerostructDiscipline. + design_space.add_variable( + "alpha", lower_bound=-10.0, upper_bound=15.0, value=5.0 + ) + + scenario = create_scenario( + [oas_disc], + objective_name="CD", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + # Trim the wing: CL = 0.5 + scenario.add_constraint("CL", constraint_type="eq", value=0.5) + scenario.execute(algo_settings_model=SLSQP_Settings(max_iter=20)) + + out = oas_disc.local_data + print( + "Trimmed solution:", + {name: out[name] for name in ("alpha", "CL", "CD", "failure")}, + ) + execute_post(scenario, OptHistoryView_Settings(save=True, show=False)) + + finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) +``` + +Running the script converges in a handful of iterations and prints: + +```text +Trimmed solution: { + 'alpha': array([4.07490213]), + 'CL': array([0.5]), + 'CD': array([0.03553854]), + 'failure': array([-0.91249284]), +} +``` + +`CL` reaches the target `0.5` exactly (the equality constraint fully +determines `alpha` here, since there is a single design variable), and +`failure` stays negative, meaning the structure remains within its +allowable stress margin at the trimmed condition. + +## The full script + +See the actual, always up-to-date source at +[`examples/openaerostruct_to_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/openaerostruct_to_gemseo.py) +in the repository. + +## Where to go next + +[`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py) +shows the mirror image of this tutorial: instead of GEMSEO calling an +OpenMDAO/OpenAeroStruct discipline, GEMSEO's own Sellar disciplines are +served over gRPC with `GEMSEOtoPhiloteDiscipline` and optimized from an +OpenMDAO model with SLSQP -- the same coupling pattern used here (an +internal Gauss-Seidel solver resolving an algebraic loop behind the +scenes), but with the two frameworks swapped. diff --git a/docs/versioned_docs/version-0.8.0/gemseo/gemseo.md b/docs/versioned_docs/version-0.8.0/gemseo/gemseo.md new file mode 100644 index 0000000..be03dcc --- /dev/null +++ b/docs/versioned_docs/version-0.8.0/gemseo/gemseo.md @@ -0,0 +1,263 @@ +--- +sidebar_position: 1 +title: "Using GEMSEO with Philote-MDO" +--- + +# Using GEMSEO with Philote-MDO + +## What problem does this solve? + +[Philote-MDO](https://github.com/MDO-Standards/Philote-Python) is a gRPC +protocol for exchanging MDO disciplines between tools. A discipline is +served by one process (possibly on another machine, in another language, +or built with another MDO framework) and called by a client process as if +it were local: the client sends input values, and the server streams back +the output values and, if available, the Jacobian. + +`philote_mdo.gemseo` implements this protocol on the GEMSEO side, in +**both** directions: + +- `PhiloteDiscipline` is a GEMSEO `Discipline` that connects to *any* + Philote-MDO server (be it a `philote-mdo` discipline, an OpenAeroStruct + model, or an OpenMDAO component served through + `philote_mdo.openmdao.RemoteExplicitComponent`) and exposes it as a + normal discipline, usable in an `MDOScenario` or an `MDA` like any + other. +- `GEMSEOtoPhiloteDiscipline` does the opposite: it wraps a GEMSEO + `Discipline` so that it can be served over gRPC and called from any + Philote-MDO client, in particular an OpenMDAO model. + +```mermaid +flowchart LR + subgraph GEMSEO_process["GEMSEO process"] + PD["PhiloteDiscipline"] + end + subgraph Server_process["Remote server process"] + S["Philote-MDO server
(any framework)"] + end + PD <-- "gRPC: values + Jacobian" --> S + + subgraph GEMSEO_process2["GEMSEO process"] + D["A GEMSEO Discipline"] + GTP["GEMSEOtoPhiloteDiscipline"] + D --> GTP + end + subgraph Client_process["Remote client process"] + C["Philote-MDO client
(e.g. OpenMDAO)"] + end + GTP <-- "gRPC: values + Jacobian" --> C +``` + +Both directions carry the discipline's Jacobian, not just its output +values, so a remote discipline can be used in a gradient-based +optimization exactly like a local one. + +This page walks through the first, and simplest, direction -- +`PhiloteDiscipline` consuming a remote discipline -- using the complete +example [`examples/paraboloid_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/paraboloid_gemseo.py). +The [next tutorial](./gemseo-openaerostruct.md) covers a realistic, +coupled OpenMDAO analysis and the opposite direction is demonstrated in +[`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py). + +## Installation + +```bash +pip install philote-mdo gemseo +``` + +`philote_mdo.gemseo` (which provides `PhiloteDiscipline` and +`GEMSEOtoPhiloteDiscipline`) is part of `philote-mdo` itself; GEMSEO is +not a hard dependency of the package, so it must be installed separately +(or via the `philote-mdo[gemseo]` extra). + +## Walkthrough: optimizing a remote Paraboloid discipline + +The example serves `philote_mdo.examples.Paraboloid`, a toy discipline +computing `f_xy(x, y)`, over gRPC, and uses it as the objective of a +GEMSEO optimization scenario that also has a constraint computed locally. +The full script is reproduced at the end of this page; the sections below +explain it piece by piece. + +### Serving the discipline + +Any Philote-MDO discipline (here, the built-in `Paraboloid` example) is +served by wrapping it in a `philote_mdo.general.ExplicitServer` and +attaching that server to a started gRPC server: + +```python +def create_server() -> grpc.Server: + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server +``` + +### Connecting a GEMSEO client to it + +`PhiloteDiscipline` is the GEMSEO-side client. Connecting it to the +channel above is enough: at construction, it queries the server for the +discipline's inputs, outputs and available partial derivatives, and +builds its GEMSEO grammars from that metadata -- no manual declaration is +needed. + +```python +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + discipline = ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) +``` + +From this point on, `paraboloid_disc` behaves like any other GEMSEO +`Discipline`: `paraboloid_disc.execute(...)` calls the remote +`ComputeFunction` RPC, and `paraboloid_disc.linearize(...)` calls the +remote `ComputeGradient` RPC. + +### Adding a local constraint + +Not every discipline of a scenario has to be remote. Here, the +constraint `g = x + y` is computed locally through GEMSEO's +`AutoPyDiscipline`, which turns a plain Python function into a +discipline by inspecting its signature and its `return` statement: + +```python +def compute_constraint(x: ndarray, y: ndarray) -> ndarray: + # AutoPyDiscipline infers the output name from this assignment, so the + # variable must be named "g" and returned as-is (not e.g. "return x + y"). + g = x + y + return g +``` + +:::warning +`AutoPyDiscipline` infers the output name(s) by parsing the literal +`return` statement of the function, so it must be `return g` with `g` +assigned beforehand, not an inlined expression like `return x + y`. +::: + +### Running the scenario + +The remote discipline and the local constraint discipline are combined in +a standard `MDOScenario`, built with `create_scenario`: + +```python +server = create_server() + +try: + paraboloid_disc = create_discipline(server) + design_space = create_design_space() + design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) + design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) + + scenario = create_scenario( + [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], + objective_name="f_xy", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + + # 0 <= g <= 10 + scenario.add_constraint("g", constraint_type="ineq", positive=True) + scenario.add_constraint("g", constraint_type="ineq", value=10.0) + scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) + + print("Optimal design found:", design_space.get_current_value(as_dict=True)) +finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) +``` + +Running the script prints the optimal design found by COBYQA: + +```text +Optimal design found: {'x': array([7.]), 'y': array([-7.])} +``` + +which matches `f_xy(7, -7) = -27`, the known minimum of the paraboloid +under `0 <= x + y <= 10` (here reached at the boundary `g = 0`). + +## The full script + +```python title="examples/paraboloid_gemseo.py" +from __future__ import annotations + +from concurrent import futures + +import grpc +from gemseo import create_design_space +from gemseo import create_scenario +from gemseo.disciplines.auto_py import AutoPyDiscipline +from gemseo.settings.formulations import DisciplinaryOpt_Settings +from gemseo.settings.opt import COBYQA_Settings +from numpy import ndarray + +from philote_mdo.examples import Paraboloid +from philote_mdo.gemseo import PhiloteDiscipline +from philote_mdo.general import ExplicitServer + +HOST = "localhost" +PORT = 50051 + + +def create_discipline(server: grpc.Server) -> PhiloteDiscipline: + discipline = ExplicitServer(discipline=Paraboloid()) + discipline.attach_to_server(server) + return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) + + +def create_server() -> grpc.Server: + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server.add_insecure_port(f"[::]:{PORT}") + server.start() + return server + + +def compute_constraint(x: ndarray, y: ndarray) -> ndarray: + # AutoPyDiscipline infers the output name from this assignment, so the + # variable must be named "g" and returned as-is (not e.g. "return x + y"). + g = x + y + return g + + +if __name__ == "__main__": + server = create_server() + + try: + paraboloid_disc = create_discipline(server) + design_space = create_design_space() + design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) + design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) + + scenario = create_scenario( + [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], + objective_name="f_xy", + design_space=design_space, + formulation_settings_model=DisciplinaryOpt_Settings(), + ) + + # 0 <= g <= 10 + scenario.add_constraint("g", constraint_type="ineq", positive=True) + scenario.add_constraint("g", constraint_type="ineq", value=10.0) + scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) + + print("Optimal design found:", design_space.get_current_value(as_dict=True)) + finally: + # Stop the server explicitly: + # its worker threads would otherwise keep the process alive. + server.stop(0) +``` + +See the actual, always up-to-date source at +[`examples/paraboloid_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/paraboloid_gemseo.py) +in the repository. + +## Where to go next + +- [Coupling OpenAeroStruct and GEMSEO](./gemseo-openaerostruct.md) applies + the same `PhiloteDiscipline` pattern to a realistic, internally-coupled + OpenMDAO analysis, and shows how the discipline's non-design inputs + must be set explicitly since Philote-MDO does not transfer GEMSEO's + default values. +- [`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py) + demonstrates the opposite direction with `GEMSEOtoPhiloteDiscipline`: + GEMSEO's Sellar disciplines are served over gRPC and optimized from an + OpenMDAO model. diff --git a/docs/versioned_sidebars/version-0.8.0-sidebars.json b/docs/versioned_sidebars/version-0.8.0-sidebars.json index 6eec8d4..1fca0b6 100644 --- a/docs/versioned_sidebars/version-0.8.0-sidebars.json +++ b/docs/versioned_sidebars/version-0.8.0-sidebars.json @@ -25,6 +25,14 @@ "openmdao/openmdao-groups" ] }, + { + "type": "category", + "label": "Working with GEMSEO", + "items": [ + "gemseo/gemseo", + "gemseo/gemseo-openaerostruct" + ] + }, { "type": "category", "label": "About", From c5d1465e87741150435e81228da27daad7780c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gallard?= Date: Thu, 17 Sep 2026 16:03:20 +0200 Subject: [PATCH 06/16] Enable @docusaurus/theme-mermaid so the GEMSEO tutorial flowcharts render The GEMSEO tutorials use ```mermaid fences, but the theme was never actually wired in: package.json didn't list the dependency and docusaurus.config.ts had no markdown.mermaid / themes entry, so the diagrams were not compiled and rendered as plain code blocks instead. Verified by building the site and loading both tutorial pages in a browser: the flowcharts now render as SVG diagrams. Note: docs/package-lock.json still needs to be regenerated with real npm (this environment only has bun) before `npm ci` in CI will pick up the new dependency. Co-Authored-By: Claude Sonnet 5 --- docs/docusaurus.config.ts | 5 +++++ docs/package.json | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 89eb25d..587a113 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -27,6 +27,11 @@ const config: Config = { locales: ["en"], }, + markdown: { + mermaid: true, + }, + themes: ["@docusaurus/theme-mermaid"], + stylesheets: [ { href: "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css", diff --git a/docs/package.json b/docs/package.json index 77fdde2..03d7252 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,6 +17,7 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", From 100bd80e13829a428619fb755732b220f006c266 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Thu, 17 Sep 2026 21:34:59 -0400 Subject: [PATCH 07/16] docs: keep the GEMSEO pages out of the 0.8.0 snapshot The 0.8.0 versioned docs describe the released package, which has no philote_mdo.gemseo module. The GEMSEO pages stay under docs/docs/ (the Next version) and will be snapshotted by the release workflow with the first release that ships them. Signed-off-by: Christopher Lupp --- .../gemseo/gemseo-openaerostruct.md | 208 -------------- .../version-0.8.0/gemseo/gemseo.md | 263 ------------------ .../version-0.8.0-sidebars.json | 8 - 3 files changed, 479 deletions(-) delete mode 100644 docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md delete mode 100644 docs/versioned_docs/version-0.8.0/gemseo/gemseo.md diff --git a/docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md b/docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md deleted file mode 100644 index c9a8549..0000000 --- a/docs/versioned_docs/version-0.8.0/gemseo/gemseo-openaerostruct.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -sidebar_position: 2 -title: "Coupling OpenAeroStruct and GEMSEO" ---- - -# Coupling OpenAeroStruct and GEMSEO through Philote-MDO - -This tutorial builds on [Using GEMSEO with Philote-MDO](./gemseo.md) and -walks through -[`examples/openaerostruct_to_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/openaerostruct_to_gemseo.py), -a more realistic case: instead of a toy analytic function, the remote -discipline is an [OpenAeroStruct](https://github.com/mdolab/OpenAeroStruct) -aerostructural analysis -- a coupled VLM aerodynamic model and a -finite-element structural model, solved together with an OpenMDAO -nonlinear solver -- and it is optimized with a gradient-based algorithm. - -This example additionally requires OpenMDAO, OpenAeroStruct and the -[`philote-examples`](https://pypi.org/project/philote-examples/) package -(`pip install openaerostruct philote-examples`), which provides the -`OasAerostructDiscipline` used below. - -## Why this matters: hiding an OpenMDAO MDA behind a Philote-MDO discipline - -OpenAeroStruct is written for [OpenMDAO](https://openmdao.org): the -`philote-examples` package provides `OasAerostructDiscipline`, which -builds an OpenMDAO `Group` combining `AerostructGeometry` and -`AerostructPoint` for a wing, resolves the aero-structural coupling -internally with `NonlinearBlockGS`, and wraps the whole `om.Problem` as a -**single Philote-MDO explicit discipline**. - -This is the key interoperability point: from the Philote-MDO (and -therefore GEMSEO) side, this coupled, iterative OpenMDAO analysis is -completely invisible. `PhiloteDiscipline` only ever sees a stateless -function `inputs -> outputs` plus a Jacobian -- exactly like the -`Paraboloid` discipline of the [previous tutorial](./gemseo.md). GEMSEO -does not need OpenMDAO, OpenAeroStruct, or any of their dependencies -installed: it only talks gRPC to a server that happens to run them. - -```mermaid -flowchart LR - subgraph GEMSEO_process["GEMSEO process"] - PD["PhiloteDiscipline"] - SC["MDOScenario
(SLSQP)"] - SC --> PD - end - subgraph Server_process["Server process"] - OAS["OasAerostructDiscipline
(Philote explicit discipline)"] - subgraph OM["OpenMDAO Problem"] - VLM["VLM aerodynamics"] - FEM["FEM structure"] - VLM <-->|"NonlinearBlockGS"| FEM - end - OAS --> OM - end - PD <-- "gRPC: alpha -> CL, CD, ...
+ Jacobian" --> OAS -``` - -## A gradient-based scenario is limited to what the server differentiates - -`OasAerostructDiscipline` only declares two partial derivatives on the -server side (in its `_build_discipline` method): - -```python -self.declare_subproblem_partial("CD", "alpha") -self.declare_subproblem_partial("CL", "alpha") -``` - -Only declared partials are computed and sent to the client: a -gradient-based GEMSEO scenario can therefore only use `alpha` as a design -variable, and `CL`/`CD` as objective/constraint candidates -- using any -other input as a design variable, or any other output as an objective or -constraint, would make GEMSEO request a Jacobian entry the server never -computes. - -This shapes the scenario: **trim the wing** by finding the angle of -attack `alpha` that **minimizes drag** `CD` while holding the **lift -coefficient** `CL` at a target value, a classic aerostructural design -problem. - -## Walkthrough - -### Fixing the non-design inputs - -The Philote-MDO protocol only transfers variable names, shapes and units -over gRPC -- not GEMSEO's default values. Since `alpha` is the only -design variable, every other input of the discipline (flight conditions -and mission parameters) must be given a value explicitly, or GEMSEO would -have no default to execute the discipline with. This is done once, by -updating the connected discipline's `default_input_data`: - -```python -FIXED_INPUTS = { - "v": array([248.136]), - "Mach_number": array([0.84]), - "re": array([1e6]), - "rho": array([0.38]), - "CT": array([grav_constant * 17.0e-6]), - "R": array([11.165e6]), - "W0": array([0.4 * 3e5]), - "speed_of_sound": array([295.4]), - "load_factor": array([1.0]), - "empty_cg": np.zeros(3), -} - - -def create_discipline(server: grpc.Server) -> PhiloteDiscipline: - discipline = pmdo.ExplicitServer(discipline=OasAerostructDiscipline()) - discipline.attach_to_server(server) - oas_discipline = PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) - oas_discipline.default_input_data.update(FIXED_INPUTS) - return oas_discipline -``` - -:::note -This is not specific to GEMSEO: any Philote-MDO client that does not set -a value for a non-design input runs into the same issue. The -[Sellar example](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py), -which drives remote *GEMSEO* disciplines from *OpenMDAO*, sets its fixed -parameters explicitly with `prob.set_val(...)` for the exact same reason. -::: - -### Serving the discipline - -As in the previous tutorial, serving the discipline only requires -starting a gRPC server and attaching an `ExplicitServer` wrapping it: - -```python -def create_server() -> grpc.Server: - server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) - server.add_insecure_port(f"[::]:{PORT}") - server.start() - return server -``` - -### Building and running the scenario - -The design space has a single variable, `alpha` (in degrees); the -scenario minimizes `CD` under the equality constraint `CL = 0.5`, using -the gradient-based `SLSQP` algorithm -- made possible because the server -provides the `CL`/`CD` derivatives with respect to `alpha` analytically: - -```python -if __name__ == "__main__": - server = create_server() - - try: - oas_disc = create_discipline(server) - - design_space = create_design_space() - # alpha is in degrees, as declared by OasAerostructDiscipline. - design_space.add_variable( - "alpha", lower_bound=-10.0, upper_bound=15.0, value=5.0 - ) - - scenario = create_scenario( - [oas_disc], - objective_name="CD", - design_space=design_space, - formulation_settings_model=DisciplinaryOpt_Settings(), - ) - # Trim the wing: CL = 0.5 - scenario.add_constraint("CL", constraint_type="eq", value=0.5) - scenario.execute(algo_settings_model=SLSQP_Settings(max_iter=20)) - - out = oas_disc.local_data - print( - "Trimmed solution:", - {name: out[name] for name in ("alpha", "CL", "CD", "failure")}, - ) - execute_post(scenario, OptHistoryView_Settings(save=True, show=False)) - - finally: - # Stop the server explicitly: - # its worker threads would otherwise keep the process alive. - server.stop(0) -``` - -Running the script converges in a handful of iterations and prints: - -```text -Trimmed solution: { - 'alpha': array([4.07490213]), - 'CL': array([0.5]), - 'CD': array([0.03553854]), - 'failure': array([-0.91249284]), -} -``` - -`CL` reaches the target `0.5` exactly (the equality constraint fully -determines `alpha` here, since there is a single design variable), and -`failure` stays negative, meaning the structure remains within its -allowable stress margin at the trimmed condition. - -## The full script - -See the actual, always up-to-date source at -[`examples/openaerostruct_to_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/openaerostruct_to_gemseo.py) -in the repository. - -## Where to go next - -[`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py) -shows the mirror image of this tutorial: instead of GEMSEO calling an -OpenMDAO/OpenAeroStruct discipline, GEMSEO's own Sellar disciplines are -served over gRPC with `GEMSEOtoPhiloteDiscipline` and optimized from an -OpenMDAO model with SLSQP -- the same coupling pattern used here (an -internal Gauss-Seidel solver resolving an algebraic loop behind the -scenes), but with the two frameworks swapped. diff --git a/docs/versioned_docs/version-0.8.0/gemseo/gemseo.md b/docs/versioned_docs/version-0.8.0/gemseo/gemseo.md deleted file mode 100644 index be03dcc..0000000 --- a/docs/versioned_docs/version-0.8.0/gemseo/gemseo.md +++ /dev/null @@ -1,263 +0,0 @@ ---- -sidebar_position: 1 -title: "Using GEMSEO with Philote-MDO" ---- - -# Using GEMSEO with Philote-MDO - -## What problem does this solve? - -[Philote-MDO](https://github.com/MDO-Standards/Philote-Python) is a gRPC -protocol for exchanging MDO disciplines between tools. A discipline is -served by one process (possibly on another machine, in another language, -or built with another MDO framework) and called by a client process as if -it were local: the client sends input values, and the server streams back -the output values and, if available, the Jacobian. - -`philote_mdo.gemseo` implements this protocol on the GEMSEO side, in -**both** directions: - -- `PhiloteDiscipline` is a GEMSEO `Discipline` that connects to *any* - Philote-MDO server (be it a `philote-mdo` discipline, an OpenAeroStruct - model, or an OpenMDAO component served through - `philote_mdo.openmdao.RemoteExplicitComponent`) and exposes it as a - normal discipline, usable in an `MDOScenario` or an `MDA` like any - other. -- `GEMSEOtoPhiloteDiscipline` does the opposite: it wraps a GEMSEO - `Discipline` so that it can be served over gRPC and called from any - Philote-MDO client, in particular an OpenMDAO model. - -```mermaid -flowchart LR - subgraph GEMSEO_process["GEMSEO process"] - PD["PhiloteDiscipline"] - end - subgraph Server_process["Remote server process"] - S["Philote-MDO server
(any framework)"] - end - PD <-- "gRPC: values + Jacobian" --> S - - subgraph GEMSEO_process2["GEMSEO process"] - D["A GEMSEO Discipline"] - GTP["GEMSEOtoPhiloteDiscipline"] - D --> GTP - end - subgraph Client_process["Remote client process"] - C["Philote-MDO client
(e.g. OpenMDAO)"] - end - GTP <-- "gRPC: values + Jacobian" --> C -``` - -Both directions carry the discipline's Jacobian, not just its output -values, so a remote discipline can be used in a gradient-based -optimization exactly like a local one. - -This page walks through the first, and simplest, direction -- -`PhiloteDiscipline` consuming a remote discipline -- using the complete -example [`examples/paraboloid_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/paraboloid_gemseo.py). -The [next tutorial](./gemseo-openaerostruct.md) covers a realistic, -coupled OpenMDAO analysis and the opposite direction is demonstrated in -[`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py). - -## Installation - -```bash -pip install philote-mdo gemseo -``` - -`philote_mdo.gemseo` (which provides `PhiloteDiscipline` and -`GEMSEOtoPhiloteDiscipline`) is part of `philote-mdo` itself; GEMSEO is -not a hard dependency of the package, so it must be installed separately -(or via the `philote-mdo[gemseo]` extra). - -## Walkthrough: optimizing a remote Paraboloid discipline - -The example serves `philote_mdo.examples.Paraboloid`, a toy discipline -computing `f_xy(x, y)`, over gRPC, and uses it as the objective of a -GEMSEO optimization scenario that also has a constraint computed locally. -The full script is reproduced at the end of this page; the sections below -explain it piece by piece. - -### Serving the discipline - -Any Philote-MDO discipline (here, the built-in `Paraboloid` example) is -served by wrapping it in a `philote_mdo.general.ExplicitServer` and -attaching that server to a started gRPC server: - -```python -def create_server() -> grpc.Server: - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - server.add_insecure_port(f"[::]:{PORT}") - server.start() - return server -``` - -### Connecting a GEMSEO client to it - -`PhiloteDiscipline` is the GEMSEO-side client. Connecting it to the -channel above is enough: at construction, it queries the server for the -discipline's inputs, outputs and available partial derivatives, and -builds its GEMSEO grammars from that metadata -- no manual declaration is -needed. - -```python -def create_discipline(server: grpc.Server) -> PhiloteDiscipline: - discipline = ExplicitServer(discipline=Paraboloid()) - discipline.attach_to_server(server) - return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) -``` - -From this point on, `paraboloid_disc` behaves like any other GEMSEO -`Discipline`: `paraboloid_disc.execute(...)` calls the remote -`ComputeFunction` RPC, and `paraboloid_disc.linearize(...)` calls the -remote `ComputeGradient` RPC. - -### Adding a local constraint - -Not every discipline of a scenario has to be remote. Here, the -constraint `g = x + y` is computed locally through GEMSEO's -`AutoPyDiscipline`, which turns a plain Python function into a -discipline by inspecting its signature and its `return` statement: - -```python -def compute_constraint(x: ndarray, y: ndarray) -> ndarray: - # AutoPyDiscipline infers the output name from this assignment, so the - # variable must be named "g" and returned as-is (not e.g. "return x + y"). - g = x + y - return g -``` - -:::warning -`AutoPyDiscipline` infers the output name(s) by parsing the literal -`return` statement of the function, so it must be `return g` with `g` -assigned beforehand, not an inlined expression like `return x + y`. -::: - -### Running the scenario - -The remote discipline and the local constraint discipline are combined in -a standard `MDOScenario`, built with `create_scenario`: - -```python -server = create_server() - -try: - paraboloid_disc = create_discipline(server) - design_space = create_design_space() - design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) - design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) - - scenario = create_scenario( - [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], - objective_name="f_xy", - design_space=design_space, - formulation_settings_model=DisciplinaryOpt_Settings(), - ) - - # 0 <= g <= 10 - scenario.add_constraint("g", constraint_type="ineq", positive=True) - scenario.add_constraint("g", constraint_type="ineq", value=10.0) - scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) - - print("Optimal design found:", design_space.get_current_value(as_dict=True)) -finally: - # Stop the server explicitly: - # its worker threads would otherwise keep the process alive. - server.stop(0) -``` - -Running the script prints the optimal design found by COBYQA: - -```text -Optimal design found: {'x': array([7.]), 'y': array([-7.])} -``` - -which matches `f_xy(7, -7) = -27`, the known minimum of the paraboloid -under `0 <= x + y <= 10` (here reached at the boundary `g = 0`). - -## The full script - -```python title="examples/paraboloid_gemseo.py" -from __future__ import annotations - -from concurrent import futures - -import grpc -from gemseo import create_design_space -from gemseo import create_scenario -from gemseo.disciplines.auto_py import AutoPyDiscipline -from gemseo.settings.formulations import DisciplinaryOpt_Settings -from gemseo.settings.opt import COBYQA_Settings -from numpy import ndarray - -from philote_mdo.examples import Paraboloid -from philote_mdo.gemseo import PhiloteDiscipline -from philote_mdo.general import ExplicitServer - -HOST = "localhost" -PORT = 50051 - - -def create_discipline(server: grpc.Server) -> PhiloteDiscipline: - discipline = ExplicitServer(discipline=Paraboloid()) - discipline.attach_to_server(server) - return PhiloteDiscipline(channel=grpc.insecure_channel(f"{HOST}:{PORT}")) - - -def create_server() -> grpc.Server: - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - server.add_insecure_port(f"[::]:{PORT}") - server.start() - return server - - -def compute_constraint(x: ndarray, y: ndarray) -> ndarray: - # AutoPyDiscipline infers the output name from this assignment, so the - # variable must be named "g" and returned as-is (not e.g. "return x + y"). - g = x + y - return g - - -if __name__ == "__main__": - server = create_server() - - try: - paraboloid_disc = create_discipline(server) - design_space = create_design_space() - design_space.add_variable("x", lower_bound=-50, upper_bound=50, value=3.0) - design_space.add_variable("y", lower_bound=-50, upper_bound=50, value=-4.0) - - scenario = create_scenario( - [paraboloid_disc, AutoPyDiscipline(py_func=compute_constraint)], - objective_name="f_xy", - design_space=design_space, - formulation_settings_model=DisciplinaryOpt_Settings(), - ) - - # 0 <= g <= 10 - scenario.add_constraint("g", constraint_type="ineq", positive=True) - scenario.add_constraint("g", constraint_type="ineq", value=10.0) - scenario.execute(algo_settings_model=COBYQA_Settings(max_iter=100)) - - print("Optimal design found:", design_space.get_current_value(as_dict=True)) - finally: - # Stop the server explicitly: - # its worker threads would otherwise keep the process alive. - server.stop(0) -``` - -See the actual, always up-to-date source at -[`examples/paraboloid_gemseo.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/paraboloid_gemseo.py) -in the repository. - -## Where to go next - -- [Coupling OpenAeroStruct and GEMSEO](./gemseo-openaerostruct.md) applies - the same `PhiloteDiscipline` pattern to a realistic, internally-coupled - OpenMDAO analysis, and shows how the discipline's non-design inputs - must be set explicitly since Philote-MDO does not transfer GEMSEO's - default values. -- [`examples/sellar_gemseo_to_openmdao.py`](https://github.com/MDO-Standards/Philote-Python/blob/main/examples/sellar_gemseo_to_openmdao.py) - demonstrates the opposite direction with `GEMSEOtoPhiloteDiscipline`: - GEMSEO's Sellar disciplines are served over gRPC and optimized from an - OpenMDAO model. diff --git a/docs/versioned_sidebars/version-0.8.0-sidebars.json b/docs/versioned_sidebars/version-0.8.0-sidebars.json index 1fca0b6..6eec8d4 100644 --- a/docs/versioned_sidebars/version-0.8.0-sidebars.json +++ b/docs/versioned_sidebars/version-0.8.0-sidebars.json @@ -25,14 +25,6 @@ "openmdao/openmdao-groups" ] }, - { - "type": "category", - "label": "Working with GEMSEO", - "items": [ - "gemseo/gemseo", - "gemseo/gemseo-openaerostruct" - ] - }, { "type": "category", "label": "About", From 105673d74a05654edede32f534ea3d245da34878 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Thu, 17 Sep 2026 21:56:05 -0400 Subject: [PATCH 08/16] docs: sync package-lock.json with the mermaid theme dependency package.json gained @docusaurus/theme-mermaid without a matching lock update, so the documentation workflow's npm ci refused to install. Regenerated with npm 10 (the version CI's Node 20 ships) in lockfile-only mode: this adds mermaid and its dependencies, and bumps katex from 0.16.45 to 0.16.47 because mermaid requires ^0.16.47. Signed-off-by: Christopher Lupp --- docs/package-lock.json | 1429 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 1285 insertions(+), 144 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index d5bce68..c05c360 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", @@ -262,6 +263,19 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", + "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1992,6 +2006,51 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz", + "integrity": "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.1.2", + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.2.tgz", + "integrity": "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.2.tgz", + "integrity": "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.2.tgz", + "integrity": "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==", + "license": "Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3932,6 +3991,34 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", @@ -4098,6 +4185,23 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", + "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^2.0.1", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -4651,6 +4755,18 @@ "react": ">=16" } }, + "node_modules/@mermaid-js/parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.0.tgz", + "integrity": "sha512-K8BeapFUfrfxbRAUQAG5oBOCwo1+bNWzaVnPxTCutkfrTBn6T/j91FIhqxWJ32SUeQ9T3iy1zcmPZ5ROZEvDrg==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -5250,6 +5366,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -5318,6 +5687,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/gtag.js": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", @@ -5560,6 +5935,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5596,6 +5978,16 @@ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6696,6 +7088,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chevrotain": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", + "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.1.2", + "@chevrotain/gast": "11.1.2", + "@chevrotain/regexp-to-ast": "11.1.2", + "@chevrotain/types": "11.1.2", + "@chevrotain/utils": "11.1.2", + "lodash-es": "4.17.23" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -7193,6 +7599,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -7495,187 +7910,713 @@ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "license": "MIT", "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.10.0.tgz", + "integrity": "sha512-+JWEEjjoqkPY7iGHps3SaCT2w67Zpaj9zHvhCJ2iPBavKHwgtOOD2YEbZSCU8VO2uVGpKdYjVz+j6bhkNSjZ0g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.34.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", + "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">=12" } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "engines": { + "node": ">=12" } }, - "node_modules/cssdb": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.10.0.tgz", - "integrity": "sha512-+JWEEjjoqkPY7iGHps3SaCT2w67Zpaj9zHvhCJ2iPBavKHwgtOOD2YEbZSCU8VO2uVGpKdYjVz+j6bhkNSjZ0g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "MIT-0" - }, - "node_modules/cssesc": { + "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" + "d3-path": "^3.1.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" + "d3-array": "2 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" + "d3-time": "1 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "license": "MIT", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "css-tree": "~2.2.0" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", "license": "MIT" }, "node_modules/debounce": { @@ -7839,6 +8780,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7977,6 +8927,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", + "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -8063,6 +9022,12 @@ "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", "license": "ISC" }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -8173,6 +9138,18 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -9209,6 +10186,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -9920,6 +10903,16 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9968,6 +10961,15 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10474,9 +11476,9 @@ } }, "node_modules/katex": { - "version": "0.16.45", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", - "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -10507,6 +11509,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10550,6 +11557,12 @@ "shell-quote": "^1.8.4" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10625,6 +11638,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10717,6 +11736,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11215,6 +12246,40 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.0.0.tgz", + "integrity": "sha512-/wQXC9iBxoGV8p3erbvaXs9h77VyLDBH6GdayVjj3hEcSQhFU4N1WUhUppotCEqlIxI2pRMwjwBSwTB1MfZBgQ==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^2.0.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "chevrotain": "~11.1.2", + "cytoscape": "^3.34.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.21", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.47", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13656,6 +14721,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -13783,6 +14854,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -13881,6 +14958,22 @@ "node": ">=16.0.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", @@ -16338,6 +17431,24 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -16391,6 +17502,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -17223,6 +18340,12 @@ "postcss": "^8.4.31" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17423,6 +18546,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -17498,6 +18630,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", From 6722a20dbc4981ffaa912f971c24c92b9d9ef380 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 20:59:47 -0400 Subject: [PATCH 09/16] docs: require philote-examples 0.5.1 for the OpenAeroStruct example philote-examples 0.5.1 is the first release whose OasAerostructDiscipline declares the CD and CL partials with respect to alpha. With earlier versions the SLSQP scenario fails on its first gradient request. Signed-off-by: Christopher Lupp --- docs/docs/gemseo/gemseo-openaerostruct.md | 9 ++++++--- examples/openaerostruct_to_gemseo.py | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/docs/gemseo/gemseo-openaerostruct.md b/docs/docs/gemseo/gemseo-openaerostruct.md index c9a8549..92c2db7 100644 --- a/docs/docs/gemseo/gemseo-openaerostruct.md +++ b/docs/docs/gemseo/gemseo-openaerostruct.md @@ -15,9 +15,12 @@ finite-element structural model, solved together with an OpenMDAO nonlinear solver -- and it is optimized with a gradient-based algorithm. This example additionally requires OpenMDAO, OpenAeroStruct and the -[`philote-examples`](https://pypi.org/project/philote-examples/) package -(`pip install openaerostruct philote-examples`), which provides the -`OasAerostructDiscipline` used below. +[`philote-examples`](https://pypi.org/project/philote-examples/) package, +version 0.5.1 or later +(`pip install openaerostruct "philote-examples>=0.5.1"`), which provides +the `OasAerostructDiscipline` used below. Earlier versions of +`philote-examples` do not declare the partial derivatives this tutorial +relies on. ## Why this matters: hiding an OpenMDAO MDA behind a Philote-MDO discipline diff --git a/examples/openaerostruct_to_gemseo.py b/examples/openaerostruct_to_gemseo.py index 187d5c0..28a7d57 100644 --- a/examples/openaerostruct_to_gemseo.py +++ b/examples/openaerostruct_to_gemseo.py @@ -36,8 +36,10 @@ constraint, for a gradient-based scenario. The other flight-condition and mission parameters are kept fixed at their default values. -This requires GEMSEO, OpenMDAO, OpenAeroStruct and ``philote-examples`` to -be installed (``pip install gemseo openaerostruct philote-examples``). +This requires GEMSEO, OpenMDAO, OpenAeroStruct and ``philote-examples`` +0.5.1 or later, the first version whose ``OasAerostructDiscipline`` +declares these partial derivatives, to be installed +(``pip install gemseo openaerostruct "philote-examples>=0.5.1"``). """ from __future__ import annotations From ec9254a91e7b173a2da6349c5502c13e7e12481b Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:01:57 -0400 Subject: [PATCH 10/16] docs: correct the PhiloteDiscipline description in the GEMSEO intro PhiloteDiscipline is built on ExplicitClient, so it connects to explicit Philote-MDO servers only; against an implicit server it fails at execute with "Method not found". The intro also named RemoteExplicitComponent, the OpenMDAO client, as the way to serve an OpenMDAO model; serving is done with OpenMdaoSubProblem. Signed-off-by: Christopher Lupp --- docs/docs/gemseo/gemseo.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/gemseo/gemseo.md b/docs/docs/gemseo/gemseo.md index be03dcc..112d865 100644 --- a/docs/docs/gemseo/gemseo.md +++ b/docs/docs/gemseo/gemseo.md @@ -18,9 +18,9 @@ the output values and, if available, the Jacobian. **both** directions: - `PhiloteDiscipline` is a GEMSEO `Discipline` that connects to *any* - Philote-MDO server (be it a `philote-mdo` discipline, an OpenAeroStruct - model, or an OpenMDAO component served through - `philote_mdo.openmdao.RemoteExplicitComponent`) and exposes it as a + (explicit) Philote-MDO server (be it a `philote-mdo` discipline, an + OpenAeroStruct model, or an OpenMDAO group served through + `philote_mdo.openmdao.OpenMdaoSubProblem`) and exposes it as a normal discipline, usable in an `MDOScenario` or an `MDA` like any other. - `GEMSEOtoPhiloteDiscipline` does the opposite: it wraps a GEMSEO From 2764659f849fa6f69521a8841dc304774850fbb1 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:08:12 -0400 Subject: [PATCH 11/16] docs: list GEMSEO as a test dependency The GEMSEO tests import gemseo at module level, so without it installed they fail to import. List it next to OpenMDAO in the README and the installation page. Signed-off-by: Christopher Lupp --- README.md | 1 + docs/docs/getting-started/installation.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index a8e8fce..b38e7c3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ installed automatically during the installation process: To run the unit and integration tests, you will need: - openmdao (can be found [here](https://github.com/OpenMDAO/OpenMDAO) or installed via pip) +- gemseo (can be found [here](https://gitlab.com/gemseo/dev/gemseo) or installed via pip) ## Installation diff --git a/docs/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md index 9c62167..31a2db8 100644 --- a/docs/docs/getting-started/installation.md +++ b/docs/docs/getting-started/installation.md @@ -23,6 +23,7 @@ Additionally, the following dependencies are required by Philote-MDO and will be To run the unit and integration tests, you will need: - [OpenMDAO](https://github.com/OpenMDAO/OpenMDAO) (also installable via pip) +- [GEMSEO](https://gitlab.com/gemseo/dev/gemseo) (also installable via pip) ## Compiling Definitions and Installation From 766ab3905be8c4786bf758e8ac9e74cc26671eeb Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:08:12 -0400 Subject: [PATCH 12/16] Rename gemseo_discipine to gemseo_discipline Fix the misspelled GEMSEOtoPhiloteDiscipline attribute and constructor argument before they are released as public API. Signed-off-by: Christopher Lupp --- philote_mdo/gemseo/gemseo_to_philote.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/philote_mdo/gemseo/gemseo_to_philote.py b/philote_mdo/gemseo/gemseo_to_philote.py index 8e33c39..d670e39 100644 --- a/philote_mdo/gemseo/gemseo_to_philote.py +++ b/philote_mdo/gemseo/gemseo_to_philote.py @@ -55,24 +55,24 @@ class GEMSEOtoPhiloteDiscipline( (see :meth:`~gemseo.core.discipline.discipline.Discipline.linearize`). """ - gemseo_discipine: Discipline + gemseo_discipline: Discipline """The GEMSEO discipline to be wrapped.""" default_data_size: int """The default size used to declare a variable that has no default value.""" - def __init__(self, gemseo_discipine: Discipline, default_data_size: int = 1): + def __init__(self, gemseo_discipline: Discipline, default_data_size: int = 1): """Initialize the GEMSEO discipline. Args: - gemseo_discipine: The GEMSEO discipline to be wrapped. + gemseo_discipline: The GEMSEO discipline to be wrapped. default_data_size: The default data size to be declared for an input or output variable when the wrapped GEMSEO discipline does not define a default value (as a :class:`~numpy.ndarray`) for it in its grammars. """ super().__init__() - self.gemseo_discipine = gemseo_discipine + self.gemseo_discipline = gemseo_discipline self.default_data_size = default_data_size def setup(self): @@ -87,7 +87,7 @@ def setup(self): when it is a :class:`~numpy.ndarray`, and from :attr:`.default_data_size` otherwise. """ - disc = self.gemseo_discipine + disc = self.gemseo_discipline for input_name in disc.input_grammar.names: data = disc.default_input_data.get(input_name) size = self.default_data_size @@ -108,8 +108,8 @@ def setup_partials(self): wrapped GEMSEO discipline are declared as partial derivatives to be computed by :meth:`.compute_partials`. """ - for output_name in self.gemseo_discipine.output_grammar.names: - for input_name in self.gemseo_discipine.input_grammar.names: + for output_name in self.gemseo_discipline.output_grammar.names: + for input_name in self.gemseo_discipline.input_grammar.names: self.declare_partials(output_name, input_name) def compute(self, inputs, outputs): @@ -121,8 +121,8 @@ def compute(self, inputs, outputs): computed by the wrapped GEMSEO discipline, indexed by output name. """ - out = self.gemseo_discipine.execute(inputs) - outputs.update({k: out[k] for k in self.gemseo_discipine.output_grammar.names}) + out = self.gemseo_discipline.execute(inputs) + outputs.update({k: out[k] for k in self.gemseo_discipline.output_grammar.names}) def compute_partials(self, inputs, partials): """Linearize the wrapped GEMSEO discipline. @@ -133,9 +133,9 @@ def compute_partials(self, inputs, partials): computed by the wrapped GEMSEO discipline, indexed by ``(output_name, input_name)`` pairs. """ - jac = self.gemseo_discipine.linearize(inputs, compute_all_jacobians=True) - output_names = self.gemseo_discipine.output_grammar.names - input_names = self.gemseo_discipine.input_grammar.names + jac = self.gemseo_discipline.linearize(inputs, compute_all_jacobians=True) + output_names = self.gemseo_discipline.output_grammar.names + input_names = self.gemseo_discipline.input_grammar.names for out_k, jac_in in jac.items(): if out_k not in output_names: From ed64e7fddb9847988dae5fb0a084e0b73c151361 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:14:45 -0400 Subject: [PATCH 13/16] PhiloteDiscipline: use the server's name, reject unsupported variables The constructor documented that an empty name would use the remote discipline's name, but passed it straight to GEMSEO, so every remote discipline was named PhiloteDiscipline. Query GetInfo and use the server-reported name when none is given; GEMSEO still falls back to the class name when the server reports none (the default). Dynamic-shape and discrete server variables were not handled. Dynamic shapes were never sent, so the first execute failed with an error asking for SetVariableShapes, which a GEMSEO user cannot call. Discrete variables were left out of the grammars and silently kept their server-side defaults. Reject both at construction with a NotImplementedError naming the variables, until they are supported. Signed-off-by: Christopher Lupp --- philote_mdo/gemseo/philote_to_gemseo.py | 29 +++++++- tests/test_philote_to_gemseo.py | 95 +++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/philote_mdo/gemseo/philote_to_gemseo.py b/philote_mdo/gemseo/philote_to_gemseo.py index 7b643b8..189a93a 100644 --- a/philote_mdo/gemseo/philote_to_gemseo.py +++ b/philote_mdo/gemseo/philote_to_gemseo.py @@ -59,12 +59,15 @@ def __init__(self, channel, name="", **options): channel: The gRPC channel to the Philote discipline server, e.g. created with ``grpc.insecure_channel("localhost:50051")``. name: The name of the discipline. - If empty, use the name of the remote discipline class. + If empty, use the name reported by the server, + or the class name if the server reports none. **options: The discipline options to send to the server, if any. Raises: ValueError: When ``channel`` is empty or ``None``. + NotImplementedError: When the server declares dynamic-shape + or discrete variables, which are not supported yet. """ if not channel: msg = "No channel provided, the Philote client will not be able to connect." @@ -76,8 +79,10 @@ def __init__(self, channel, name="", **options): # generic Philote client self._client = pm.ExplicitClient(channel=channel) - # call the init function of the explicit component - super().__init__(name=name) + # call the init function of the explicit component; an empty name + # makes GEMSEO fall back to the class name + self._client.get_discipline_info() + super().__init__(name=name or self._client._name) self._client.send_stream_options() if options: @@ -150,7 +155,25 @@ def _initialize_grammars(self): from the remote discipline server by :meth:`~philote_mdo.general.discipline_client.DisciplineClient.get_variable_definitions`. It does not perform any RPC call itself. + + Raises: + NotImplementedError: When the server declares dynamic-shape or + discrete variables. Their shapes would have to be sent to + the server, and discrete variables would be missing from the + grammars and silently left at their server-side defaults. """ + unsupported = [ + f"{var.name} (dynamic shape)" + for var in self._client._var_meta + if var.dynamic_shape + ] + [f"{var.name} (discrete)" for var in self._client._discrete_var_meta] + if unsupported: + msg = ( + "PhiloteDiscipline does not support these server variables yet: " + + ", ".join(unsupported) + ) + raise NotImplementedError(msg) + input_names = [] output_names = [] for var in self._client._var_meta: diff --git a/tests/test_philote_to_gemseo.py b/tests/test_philote_to_gemseo.py index 72e9f77..f8419db 100644 --- a/tests/test_philote_to_gemseo.py +++ b/tests/test_philote_to_gemseo.py @@ -28,6 +28,29 @@ CHANNEL = "localhost:50051" +class DynamicShapeDiscipline(pmdo.ExplicitDiscipline): + """A discipline whose input and output shapes are set by the client.""" + + def setup(self): + self.add_input("x", dynamic_shape=True) + self.add_output("y", dynamic_shape=True) + + def compute(self, inputs, outputs): + outputs["y"] = 2.0 * inputs["x"] + + +class DiscreteInputDiscipline(pmdo.ExplicitDiscipline): + """A discipline with a discrete input next to a continuous one.""" + + def setup(self): + self.add_input("x", shape=(1,)) + self.add_discrete_input("mode", default="a") + self.add_output("y", shape=(1,)) + + def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): + outputs["y"] = 2.0 * inputs["x"] + + class PhiloteToGEMSEOTests(unittest.TestCase): """ Integration tests for PhiloteDiscipline, which wraps a remote Philote @@ -104,6 +127,78 @@ def test_rosenbrock_with_options(self): self.assertEqual(out["f"][0], 0.0) + def _serve(self, discipline): + """ + Serves a Philote discipline for the duration of the test. + """ + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + pmdo.ExplicitServer(discipline=discipline).attach_to_server(server) + server.add_insecure_port(PORT) + server.start() + self.addCleanup(server.stop, 0) + + def test_name_defaults_to_server_reported_name(self): + """ + Without an explicit name, the discipline takes the name reported by + the server. + """ + paraboloid = Paraboloid() + paraboloid._name = "paraboloid" + self._serve(paraboloid) + + disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + self.assertEqual(disc.name, "paraboloid") + + def test_name_falls_back_to_class_name(self): + """ + When the server reports no name, GEMSEO's class-name default is used. + """ + self._serve(Paraboloid()) + + disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + self.assertEqual(disc.name, "PhiloteDiscipline") + + def test_explicit_name_overrides_server_name(self): + """ + A name passed to the constructor takes precedence over the server's. + """ + paraboloid = Paraboloid() + paraboloid._name = "paraboloid" + self._serve(paraboloid) + + disc = PhiloteDiscipline( + channel=grpc.insecure_channel(CHANNEL), name="remote_paraboloid" + ) + + self.assertEqual(disc.name, "remote_paraboloid") + + def test_dynamic_shape_variables_raise(self): + """ + Dynamic-shape server variables are rejected at construction, naming + the offending variables. + """ + self._serve(DynamicShapeDiscipline()) + + with self.assertRaises(NotImplementedError) as ctx: + PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + self.assertIn("x (dynamic shape)", str(ctx.exception)) + self.assertIn("y (dynamic shape)", str(ctx.exception)) + + def test_discrete_variables_raise(self): + """ + Discrete server variables are rejected at construction rather than + silently left at their server-side defaults. + """ + self._serve(DiscreteInputDiscipline()) + + with self.assertRaises(NotImplementedError) as ctx: + PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) + + self.assertIn("mode (discrete)", str(ctx.exception)) + if __name__ == "__main__": unittest.main(verbosity=2) From e8c7fb0d381bdda11df209f40c3bf96675e3d4c8 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:14:45 -0400 Subject: [PATCH 14/16] Check Sellar MDA outputs against the canonical result test_sellar_mda_compute only asserted that obj and con1 were present, so any values passed. Evaluate at the canonical Sellar starting point (x = 1, z = [5, 2]) and compare against the canonical result there. Signed-off-by: Christopher Lupp --- tests/test_openmdao_to_gemseo.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_openmdao_to_gemseo.py b/tests/test_openmdao_to_gemseo.py index f6d3628..1d8e3bd 100644 --- a/tests/test_openmdao_to_gemseo.py +++ b/tests/test_openmdao_to_gemseo.py @@ -19,6 +19,7 @@ import unittest import grpc from numpy import array +from numpy.testing import assert_allclose import philote_mdo.general as pmdo import philote_mdo.openmdao as pmdo_om from philote_mdo.examples.sellar import SellarMDA @@ -65,12 +66,14 @@ def test_sellar_mda_compute(self): sellar_mda = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL)) - out = sellar_mda.execute({"x": array([1.0]), "z": array([2.0, 3.0])}) + # canonical Sellar starting point + out = sellar_mda.execute({"x": array([1.0]), "z": array([5.0, 2.0])}) server.stop(0) - self.assertIn("obj", out) - self.assertIn("con1", out) + # canonical Sellar result at that point + assert_allclose(out["obj"], 28.58830817, rtol=1e-8) + assert_allclose(out["con1"], -22.42830237, rtol=1e-8) def test_sellar_mda_linearize(self): """ From 4f88d2d7321cb23b01ac1d2db2b3e0f893f2610b Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:22:11 -0400 Subject: [PATCH 15/16] docs: list GEMSEO among the clients in the quickstart Signed-off-by: Christopher Lupp --- docs/docs/getting-started/quickstart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/getting-started/quickstart.md b/docs/docs/getting-started/quickstart.md index aae3fb7..72fcc83 100644 --- a/docs/docs/getting-started/quickstart.md +++ b/docs/docs/getting-started/quickstart.md @@ -119,9 +119,9 @@ server.wait_for_termination() ## Calling the Discipline Using a Client -Now that a server is running, it can be queried using a client. Philote-Python offers a number of clients for this purpose, ranging from the general implementation to OpenMDAO and CSDL components. However, under the hood, the OpenMDAO and CSDL components use the general client implementation. +Now that a server is running, it can be queried using a client. Philote-Python offers a number of clients for this purpose, ranging from the general implementation to OpenMDAO and CSDL components and GEMSEO disciplines. However, under the hood, these all use the general client implementation. -This example will use the general explicit client implementation. Despite it being fully functional and able to run in scientific workflows, it probably is not a realistic MDO workflow. The [OpenMDAO clients](../openmdao/openmdao-clients.md) tutorial demonstrates calling Philote disciplines from the OpenMDAO framework. +This example will use the general explicit client implementation. Despite it being fully functional and able to run in scientific workflows, it probably is not a realistic MDO workflow. The [OpenMDAO clients](../openmdao/openmdao-clients.md) and [GEMSEO](../gemseo/gemseo.md) tutorials demonstrate calling Philote disciplines from the OpenMDAO and GEMSEO frameworks. First, the explicit client must be imported and initialized using a gRPC channel. From 3472f27f806a70ddd02772ffbc0873f301431a78 Mon Sep 17 00:00:00 2001 From: Christopher Lupp Date: Fri, 18 Sep 2026 21:24:52 -0400 Subject: [PATCH 16/16] Drop support for Python 3.9 Python 3.9 reached end of life in October 2025, and GEMSEO 6.3 requires 3.10, so the 3.9 CI job resolved GEMSEO 6.2 while every other job tested 6.3. Require Python >=3.10, remove 3.9 from the CI matrix and the classifiers, and update the agent guidelines and the landing page. Signed-off-by: Christopher Lupp --- .github/workflows/tests.yaml | 2 +- CHANGELOG.md | 5 +++++ CLAUDE.md | 4 ++-- docs/src/pages/index.tsx | 2 +- pyproject.toml | 3 +-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 60e67b9..ee30bb8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@main diff --git a/CHANGELOG.md b/CHANGELOG.md index 484dd98..51639ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Documentation & Infrastructure +- Dropped support for Python 3.9, which reached end of life in October + 2025. `requires-python` is now `>=3.10`, and CI no longer tests 3.9. + GEMSEO 6.3 requires Python 3.10, so the 3.9 job was the only one + testing an older GEMSEO (6.2); the latest `grpcio`, `protobuf` and + OpenMDAO also require 3.10 or later. - Updated the documentation site's dependencies to clear known npm advisories, taking the audit from 42 findings (2 critical, 26 high) to 17 (all high). `npm audit fix` resolved the `webpack-dev-server`, `sockjs`, diff --git a/CLAUDE.md b/CLAUDE.md index f219efe..8ffd17e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ Philote-Python is the reference Python implementation of the Philote-MDO standar ## Build System ### Requirements -- Python 3.9 - 3.12 +- Python 3.10 - 3.12 - `numpy`, `scipy`, `grpcio`, `protobuf` (installed automatically) - `openmdao` (only for the OpenMDAO bindings and integration tests) @@ -51,7 +51,7 @@ docs/ # Docusaurus documentation site (see docs/README.md) ## CI/CD ### Tests workflow (`.github/workflows/tests.yaml`) -Runs `pytest` against Python 3.9 / 3.10 / 3.11 / 3.12 on every push to `main`, `develop`, `release/*`, `support/*`, and on PRs into `main` / `develop`. +Runs `pytest` against Python 3.10 / 3.11 / 3.12 on every push to `main`, `develop`, `release/*`, `support/*`, and on PRs into `main` / `develop`. ### Documentation workflow (`.github/workflows/documentation.yaml`) Builds the Docusaurus site under `docs/` and deploys it to GitHub Pages whenever `docs/**` changes on `develop`. Uses the GitHub Pages artifact pipeline (`actions/configure-pages` + `upload-pages-artifact` + `deploy-pages`). diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 9f46318..6521bb8 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -360,7 +360,7 @@ export default function Home(): ReactNode {
- + diff --git a/pyproject.toml b/pyproject.toml index cf4e65e..7ece48f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,12 +11,11 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "grpcio~=1.66.0", "protobuf~=5.29.0",