diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 5f6c9d9..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
@@ -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..2a7fe83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,6 +54,21 @@ 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.
+ Discrete variables are carried in both directions. The discrete
+ variables of a remote Philote discipline are part of the GEMSEO
+ grammars next to the continuous ones, bound to no type since a discrete
+ variable may carry any JSON-compatible value. Conversely, a variable
+ of a wrapped GEMSEO discipline is served as a discrete Philote variable
+ when its grammar's data converter does not report it as continuous,
+ which includes the integer-valued ones, since a continuous Philote
+ variable is an array of doubles. Discrete variables are excluded from
+ the Jacobian, including when it is requested in full with
+ `compute_all_jacobians=True`.
+- Added examples and tutorials demonstrating interoperability between
+ GEMSEO, OpenMDAO and OpenAeroStruct through Philote-MDO.
### Bug Fixes
@@ -121,9 +136,15 @@ 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
+- 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/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/gemseo/gemseo-openaerostruct.md b/docs/docs/gemseo/gemseo-openaerostruct.md
new file mode 100644
index 0000000..92c2db7
--- /dev/null
+++ b/docs/docs/gemseo/gemseo-openaerostruct.md
@@ -0,0 +1,211 @@
+---
+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,
+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
+
+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..00a8e45
--- /dev/null
+++ b/docs/docs/gemseo/gemseo.md
@@ -0,0 +1,339 @@
+---
+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*
+ (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
+ `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. Both also carry the discipline's
+discrete variables next to its continuous ones -- see
+[Discrete variables](#discrete-variables) below.
+
+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.
+
+## Discrete variables
+
+Philote-MDO distinguishes **continuous** variables, which travel over the
+wire as arrays of doubles, from **discrete** variables, which carry any
+JSON-compatible value: a string, a boolean, an integer, a list, or a
+nested structure. `philote_mdo.gemseo` supports them in both directions,
+and nothing has to be configured to enable it.
+
+### Serving a GEMSEO discipline that has discrete variables
+
+`GEMSEOtoPhiloteDiscipline` classifies every name of the wrapped
+discipline's grammars by asking the grammar's data converter whether that
+name holds continuous data:
+
+```python
+discipline.input_grammar.data_converter.is_continuous(name)
+```
+
+A name reported as continuous is declared as a continuous Philote
+variable; every other name is declared as a discrete one. A grammar
+element typed `str`, `bool`, `int`, `dict` or `list` is therefore served
+as a discrete variable automatically:
+
+```python
+class ScalingDiscipline(Discipline):
+ default_grammar_type = GrammarType.SIMPLE
+
+ def __init__(self):
+ super().__init__()
+ self.input_grammar.update_from_names(["x"]) # continuous
+ self.input_grammar.update_from_types({"mode": str}) # discrete
+ self.output_grammar.update_from_names(["y"]) # continuous
+ self.output_grammar.update_from_types({"used_mode": str}) # discrete
+```
+
+:::note
+`is_continuous` is the right question to ask, rather than `is_numeric`,
+because an integer is numeric but a continuous Philote variable is an
+array of doubles. An integer-valued variable therefore belongs to the
+discrete side of the protocol, as it does in OpenMDAO.
+
+A `SimpleGrammar` cannot see the dtype of an element it types as
+`ndarray`, so an integer-valued *array* still travels as a continuous
+variable. Use a `JSONGrammar` or a `PydanticGrammar` if that distinction
+matters.
+:::
+
+### Consuming a remote discipline that has discrete variables
+
+`PhiloteDiscipline` puts the server's discrete variables in its GEMSEO
+grammars next to the continuous ones, bound to no type, since a discrete
+variable may carry any value. They are then read and written like any
+other variable:
+
+```python
+remote = PhiloteDiscipline(channel=grpc.insecure_channel("localhost:50051"))
+
+out = remote.execute({"x": array([1.0, 2.0]), "mode": "triple"})
+
+print(out["y"], out["used_mode"])
+```
+
+:::note
+Discrete variables are never differentiated: no partial derivative is
+declared for them, and they are excluded from the Jacobian, including
+when it is requested in full with `compute_all_jacobians=True`.
+:::
+
+:::warning
+Philote-MDO does not transfer default values, so a discrete input has no
+default on the client side. It must be part of the input data passed to
+`execute()`, just like a continuous one.
+:::
+
+## 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/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
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.
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-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",
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",
diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx
index 2c21b42..6521bb8 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",
@@ -354,9 +360,10 @@ export default function Home(): ReactNode {
diff --git a/examples/openaerostruct_to_gemseo.py b/examples/openaerostruct_to_gemseo.py
new file mode 100644
index 0000000..28a7d57
--- /dev/null
+++ b/examples/openaerostruct_to_gemseo.py
@@ -0,0 +1,152 @@
+# 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``
+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 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..06186ab
--- /dev/null
+++ b/philote_mdo/gemseo/gemseo_to_philote.py
@@ -0,0 +1,272 @@
+# 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 typing import Any
+
+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: # pragma: no cover
+ from collections.abc import Mapping
+
+ from gemseo.core.discipline.discipline import Discipline
+ from gemseo.core.grammars.base_grammar import BaseGrammar
+
+
+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)
+
+ The input and output variables of the wrapped discipline that hold
+ continuous data, as reported by the ``is_continuous`` method of their
+ grammar's data converter, are exposed as flat (1D) continuous Philote
+ variables. Every other variable is exposed as a Philote discrete
+ variable, which carries any JSON-compatible value.
+
+ This is the split of the Philote-MDO protocol itself: a continuous
+ variable travels as an array of doubles, while a discrete variable
+ travels as a ``google.protobuf.Value``. An integer-valued variable
+ therefore belongs to the discrete side, as it does in OpenMDAO.
+
+ 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`).
+ The discrete variables are left out of it.
+ """
+
+ 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_discipline: Discipline, default_data_size: int = 1):
+ """Initialize the GEMSEO discipline.
+
+ Args:
+ 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_discipline = gemseo_discipline
+ self.default_data_size = default_data_size
+ # The names of the variables of the wrapped discipline, split by
+ # setup() into the ones Philote carries as continuous arrays and the
+ # ones it carries as discrete values.
+ self._input_names = ()
+ self._output_names = ()
+ self._discrete_input_names = ()
+ self._discrete_output_names = ()
+
+ 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 that holds continuous data 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.
+
+ Every other name is declared as a discrete Philote variable, with
+ the default value of the wrapped discipline, if any.
+ """
+ disc = self.gemseo_discipline
+ self._input_names, self._discrete_input_names = self._split_names(
+ disc.input_grammar
+ )
+ self._output_names, self._discrete_output_names = self._split_names(
+ disc.output_grammar
+ )
+
+ defaults = disc.default_input_data
+ for input_name in self._input_names:
+ self.add_input(input_name, shape=(self._get_size(defaults, input_name),))
+ for input_name in self._discrete_input_names:
+ self.add_discrete_input(input_name, default=defaults.get(input_name))
+
+ defaults = disc.default_output_data
+ for output_name in self._output_names:
+ self.add_output(output_name, shape=(self._get_size(defaults, output_name),))
+ for output_name in self._discrete_output_names:
+ self.add_discrete_output(output_name, default=defaults.get(output_name))
+
+ @staticmethod
+ def _split_names(grammar: BaseGrammar) -> tuple[tuple[str, ...], tuple[str, ...]]:
+ """Split the names of a grammar into continuous and discrete ones.
+
+ Args:
+ grammar: The input or output grammar of the wrapped discipline.
+
+ Returns:
+ The names bound to continuous data,
+ which Philote carries as arrays of doubles,
+ and the other names,
+ which Philote carries as discrete variables.
+ """
+ is_continuous = grammar.data_converter.is_continuous
+ continuous_names = []
+ discrete_names = []
+ for name in grammar.names:
+ if is_continuous(name):
+ continuous_names.append(name)
+ else:
+ discrete_names.append(name)
+ return tuple(continuous_names), tuple(discrete_names)
+
+ def _get_size(self, defaults: Mapping[str, Any], name: str) -> int:
+ """Return the size to declare for a continuous Philote variable.
+
+ Args:
+ defaults: The default input or output data of the wrapped
+ discipline.
+ name: The name of the variable.
+
+ Returns:
+ The size of the default value when it is a NumPy array,
+ and :attr:`.default_data_size` otherwise.
+ """
+ value = defaults.get(name)
+ if isinstance(value, ndarray):
+ return value.size
+ return self.default_data_size
+
+ def setup_partials(self):
+ """Declare the Jacobian of every output with respect to every input.
+
+ No sparsity pattern is assumed: all the continuous input-output
+ pairs of the wrapped GEMSEO discipline are declared as partial
+ derivatives to be computed by :meth:`.compute_partials`. The
+ discrete variables are not differentiable, so they take no part in
+ the Jacobian.
+ """
+ for output_name in self._output_names:
+ for input_name in self._input_names:
+ self.declare_partials(output_name, input_name)
+
+ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
+ """Execute the wrapped GEMSEO discipline.
+
+ Args:
+ inputs: The continuous input values, indexed by input name.
+ outputs: The dictionary to be filled with the continuous output
+ values computed by the wrapped GEMSEO discipline, indexed by
+ output name.
+ discrete_inputs: The discrete input values, indexed by input
+ name, if any.
+ discrete_outputs: The dictionary to be filled with the discrete
+ output values computed by the wrapped GEMSEO discipline,
+ indexed by output name, if the discipline has any.
+ """
+ out = self.gemseo_discipline.execute(
+ self._merge_inputs(inputs, discrete_inputs)
+ )
+ for output_name in self._output_names:
+ outputs[output_name] = out[output_name]
+
+ if discrete_outputs is not None:
+ for output_name in self._discrete_output_names:
+ discrete_outputs[output_name] = out[output_name]
+
+ def compute_partials(self, inputs, partials, discrete_inputs=None):
+ """Linearize the wrapped GEMSEO discipline.
+
+ The discrete inputs are passed to the wrapped discipline along with
+ the continuous ones, so that it can use them to compute the
+ Jacobian, but the discipline is not differentiated with respect to
+ them.
+
+ Args:
+ inputs: The continuous 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.
+ discrete_inputs: The discrete input values, indexed by input
+ name, if any.
+ """
+ if not self._partials_meta:
+ # The discipline has no continuous input or no continuous output,
+ # so setup_partials() declared nothing to differentiate.
+ return
+
+ disc = self.gemseo_discipline
+ # Restrict the differentiation to the continuous variables, which is
+ # exactly the subset that setup_partials() declared.
+ disc.add_differentiated_inputs(self._input_names)
+ disc.add_differentiated_outputs(self._output_names)
+ jac = disc.linearize(self._merge_inputs(inputs, discrete_inputs))
+
+ for out_k, jac_in in jac.items():
+ if out_k not in self._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 self._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
+
+ @staticmethod
+ def _merge_inputs(inputs, discrete_inputs):
+ """Merge the continuous and the discrete inputs into GEMSEO input data.
+
+ Args:
+ inputs: The continuous input values, indexed by input name.
+ discrete_inputs: The discrete input values, indexed by input
+ name, if any.
+
+ Returns:
+ The input data of the wrapped GEMSEO discipline.
+ """
+ if not discrete_inputs:
+ return inputs
+ return {**inputs, **discrete_inputs}
diff --git a/philote_mdo/gemseo/philote_to_gemseo.py b/philote_mdo/gemseo/philote_to_gemseo.py
new file mode 100644
index 0000000..7585980
--- /dev/null
+++ b/philote_mdo/gemseo/philote_to_gemseo.py
@@ -0,0 +1,294 @@
+# 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 Any
+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: # pragma: no cover
+ from collections.abc import Iterable
+ from collections.abc import Mapping
+
+
+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.
+
+ The discrete variables of the remote discipline are part of the
+ grammars, next to the continuous ones, and are read from and written to
+ the local data like any other variable. Since a Philote discrete
+ variable may carry any JSON-compatible value, its grammar element is
+ bound to no type. Discrete variables are never differentiated: they are
+ excluded from the Jacobian, including when it is requested in full with
+ ``compute_all_jacobians=True``.
+ """
+
+ 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 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
+ variables, which are not supported yet.
+ """
+ if not channel:
+ msg = "No channel provided, the Philote client will not be able to connect."
+ raise ValueError(msg)
+ # The shape of each continuous 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 = {}
+ # The names of the discrete input and output variables, filled in by
+ # _initialize_grammars(); discrete inputs travel on their own side of
+ # the wire protocol and so must be split off the GEMSEO input data.
+ self._discrete_input_names = set()
+ self._discrete_output_names = set()
+ # generic Philote client
+ self._client = pm.ExplicitClient(channel=channel)
+
+ # 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:
+ 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. Continuous and discrete inputs are sent separately, and the
+ discrete outputs returned by the server, if any, are merged back into
+ the output data.
+
+ Args:
+ input_data: The input data, without namespace prefixes.
+
+ Returns:
+ The output data computed by the remote discipline.
+ """
+ inputs, discrete_inputs = self._split_input_data(input_data)
+ outputs = self._client.run_compute(inputs, discrete_inputs=discrete_inputs)
+ # run_compute returns (outputs, discrete_outputs) when the server
+ # sends back discrete output data, and a plain dictionary otherwise.
+ if isinstance(outputs, tuple):
+ outputs, discrete_outputs = outputs
+ outputs.update(discrete_outputs)
+ return outputs
+
+ def _split_input_data(self, input_data: Mapping[str, Any]) -> tuple[dict, dict]:
+ """Split the GEMSEO input data into continuous and discrete inputs.
+
+ The GEMSEO input grammar holds the continuous and the discrete inputs
+ side by side, while the Philote wire protocol carries them in two
+ distinct kinds of message.
+
+ Args:
+ input_data: The input data, without namespace prefixes.
+
+ Returns:
+ The continuous input values and the discrete input values,
+ both indexed by variable name.
+ """
+ inputs = {}
+ discrete_inputs = {}
+ for name, value in input_data.items():
+ if name in self._discrete_input_names:
+ discrete_inputs[name] = value
+ else:
+ inputs[name] = value
+ return inputs, discrete_inputs
+
+ 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.
+ """
+ inputs, discrete_inputs = self._split_input_data(self.get_input_data())
+ jac_flat = self._client.run_compute_partials(
+ inputs, discrete_inputs=discrete_inputs
+ )
+ 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 _get_differentiated_io(
+ self,
+ compute_all_jacobians: bool = False,
+ ) -> tuple[tuple[str, ...], tuple[str, ...]]:
+ """Return the inputs and outputs used in the differentiation.
+
+ The discrete variables are filtered out, as they cannot be
+ differentiated. GEMSEO already does this when the differentiated
+ variables are selected with :meth:`.add_differentiated_inputs` and
+ :meth:`.add_differentiated_outputs`, but not when the whole Jacobian
+ is requested.
+
+ Args:
+ compute_all_jacobians: Whether to compute the Jacobians of all the
+ outputs with respect to all the inputs.
+
+ Returns:
+ The names of the differentiated inputs
+ and the names of the differentiated outputs.
+ """
+ input_names, output_names = super()._get_differentiated_io(
+ compute_all_jacobians
+ )
+ if not compute_all_jacobians:
+ return input_names, output_names
+
+ return (
+ tuple(
+ name for name in input_names if name not in self._discrete_input_names
+ ),
+ tuple(
+ name
+ for name in output_names
+ if name not in self._discrete_output_names
+ ),
+ )
+
+ 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.
+
+ Continuous variables are bound to :class:`~numpy.ndarray`, while
+ discrete variables are bound to no type at all, since a Philote
+ discrete variable may hold any JSON-compatible value.
+
+ Raises:
+ NotImplementedError: When the server declares dynamic-shape
+ variables, whose shapes would have to be sent to the server.
+ """
+ unsupported = [
+ f"{var.name} (dynamic shape)"
+ for var in self._client._var_meta
+ if var.dynamic_shape
+ ]
+ 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:
+ 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)
+
+ for var in self._client._discrete_var_meta:
+ if var.type == data.kDiscreteInput:
+ self._discrete_input_names.add(var.name)
+
+ if var.type == data.kDiscreteOutput:
+ self._discrete_output_names.add(var.name)
+
+ self.input_grammar.update_from_names(input_names)
+ self.output_grammar.update_from_names(output_names)
+ # A None type means that the grammar accepts any value for that name,
+ # which is what a Philote discrete variable may carry.
+ self.input_grammar.update_from_types(
+ dict.fromkeys(self._discrete_input_names)
+ )
+ self.output_grammar.update_from_types(
+ dict.fromkeys(self._discrete_output_names)
+ )
diff --git a/pyproject.toml b/pyproject.toml
index 0b45c86..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",
@@ -31,6 +30,7 @@ Documentation = "https://mdo-standards.github.io/Philote-Python/"
[project.optional-dependencies]
openmdao = ["openmdao>=3.0"]
+gemseo = ["gemseo>=6.0,<7.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..cf99353
--- /dev/null
+++ b/tests/test_gemseo_to_philote.py
@@ -0,0 +1,348 @@
+# 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
+from unittest.mock import patch
+import grpc
+from numpy import array
+from numpy import eye
+from numpy import ndarray
+from numpy.testing import assert_allclose
+from scipy.sparse import issparse
+from gemseo.core.discipline.discipline import Discipline
+from gemseo.core.grammars.factory import GrammarType
+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 ScalingDiscipline(Discipline):
+ """A GEMSEO discipline with a string input and a string output.
+
+ The string input ``mode`` scales the array output ``y``, so that both
+ the outputs and the Jacobian depend on it, and the string output
+ ``used_mode`` echoes it back.
+ """
+
+ default_grammar_type = GrammarType.SIMPLE
+
+ def __init__(self):
+ super().__init__()
+ self.input_grammar.update_from_names(["x"])
+ self.input_grammar.update_from_types({"mode": str})
+ self.output_grammar.update_from_names(["y"])
+ self.output_grammar.update_from_types({"used_mode": str})
+ self.default_input_data = {"x": array([1.0, 1.0]), "mode": "double"}
+ self.default_output_data = {"y": array([0.0, 0.0]), "used_mode": "double"}
+
+ @staticmethod
+ def _get_factor(mode):
+ """Return the factor scaling the output.
+
+ Args:
+ mode: The value of the string input.
+
+ Returns:
+ The factor by which the input array is scaled.
+ """
+ return 2.0 if mode == "double" else 3.0
+
+ def _run(self, input_data):
+ factor = self._get_factor(input_data["mode"])
+ return {"y": factor * input_data["x"], "used_mode": input_data["mode"]}
+
+ def _compute_jacobian(self, input_names=(), output_names=()):
+ self._init_jacobian(input_names, output_names)
+ self.jac["y"]["x"] = self._get_factor(self.io.data["mode"]) * eye(2)
+
+
+class IntegerDiscipline(Discipline):
+ """A GEMSEO discipline with an integer input and an integer output.
+
+ A Philote continuous variable travels as an array of doubles, so an
+ integer variable belongs to the discrete side of the protocol.
+ """
+
+ default_grammar_type = GrammarType.SIMPLE
+
+ def __init__(self):
+ super().__init__()
+ self.input_grammar.update_from_names(["x"])
+ self.input_grammar.update_from_types({"n": int})
+ self.output_grammar.update_from_names(["y"])
+ self.output_grammar.update_from_types({"count": int})
+ self.default_input_data = {"x": array([1.0]), "n": 2}
+
+ def _run(self, input_data):
+ return {"y": input_data["n"] * input_data["x"], "count": input_data["n"]}
+
+
+class DiscreteOnlyDiscipline(Discipline):
+ """A GEMSEO discipline whose variables are all discrete."""
+
+ default_grammar_type = GrammarType.SIMPLE
+
+ def __init__(self):
+ super().__init__()
+ self.input_grammar.update_from_types({"mode": str})
+ self.output_grammar.update_from_types({"used_mode": str})
+ self.default_input_data = {"mode": "double"}
+
+ def _run(self, input_data):
+ return {"used_mode": input_data["mode"]}
+
+
+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,
+ )
+
+ 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)
+
+
+class DiscreteVariableTests(unittest.TestCase):
+ """
+ Tests for the handling of the variables of a wrapped GEMSEO discipline
+ that do not hold numeric data, which are exposed as Philote discrete
+ variables.
+ """
+
+ def test_setup_splits_numeric_and_discrete_variables(self):
+ """
+ The variables whose grammar data converter reports them as numeric
+ are declared as continuous Philote variables, and the others as
+ discrete ones.
+ """
+ wrapper = GEMSEOtoPhiloteDiscipline(ScalingDiscipline())
+ wrapper.setup()
+
+ self.assertEqual([m.name for m in wrapper._var_meta], ["x", "y"])
+ self.assertEqual(
+ [m.name for m in wrapper._discrete_var_meta], ["mode", "used_mode"]
+ )
+
+ def test_setup_partials_skips_discrete_variables(self):
+ """
+ Only the variables holding continuous data take part in the
+ Jacobian, so no partial derivative is declared for a discrete one.
+ """
+ wrapper = GEMSEOtoPhiloteDiscipline(ScalingDiscipline())
+ wrapper.setup()
+ wrapper.setup_partials()
+
+ self.assertEqual(
+ [(m.name, m.subname) for m in wrapper._partials_meta], [("y", "x")]
+ )
+
+ def test_discrete_variables_round_trip(self):
+ """
+ A discrete input travels to the wrapped GEMSEO discipline and a
+ discrete output travels back, through a remote PhiloteDiscipline.
+ """
+ remote = self._serve_scaling_discipline()
+
+ self.assertEqual(
+ {n: remote.input_grammar[n] for n in remote.input_grammar},
+ {"x": ndarray, "mode": None},
+ )
+ self.assertEqual(
+ {n: remote.output_grammar[n] for n in remote.output_grammar},
+ {"y": ndarray, "used_mode": None},
+ )
+
+ out = remote.execute({"x": array([1.0, 2.0]), "mode": "triple"})
+
+ assert_allclose(out["y"], array([3.0, 6.0]))
+ self.assertEqual(out["used_mode"], "triple")
+
+ def test_discrete_input_used_by_compute_partials(self):
+ """
+ The discrete inputs are passed to the wrapped discipline when it is
+ linearized, and are themselves left out of the Jacobian.
+ """
+ remote = self._serve_scaling_discipline()
+
+ jac = remote.linearize(
+ {"x": array([1.0, 2.0]), "mode": "triple"}, compute_all_jacobians=True
+ )
+
+ self.assertEqual(list(jac), ["y"])
+ self.assertEqual(list(jac["y"]), ["x"])
+ assert_allclose(jac["y"]["x"], 3.0 * eye(2))
+
+ def _serve_scaling_discipline(self):
+ """
+ Serves a ScalingDiscipline for the duration of the test.
+
+ Returns:
+ A PhiloteDiscipline client connected to the served discipline.
+ """
+ server = grpc.server(futures.ThreadPoolExecutor(max_workers=1))
+ pmdo.ExplicitServer(
+ discipline=GEMSEOtoPhiloteDiscipline(ScalingDiscipline())
+ ).attach_to_server(server)
+ server.add_insecure_port(PORT)
+ server.start()
+ self.addCleanup(server.stop, 0)
+ return PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL))
+
+ def test_integer_variables_are_discrete(self):
+ """
+ A Philote continuous variable is an array of doubles, so an
+ integer-valued variable is served as a discrete one and takes no
+ part in the Jacobian.
+ """
+ wrapper = GEMSEOtoPhiloteDiscipline(IntegerDiscipline())
+ wrapper.setup()
+ wrapper.setup_partials()
+
+ self.assertEqual([m.name for m in wrapper._var_meta], ["x", "y"])
+ self.assertEqual(
+ [m.name for m in wrapper._discrete_var_meta], ["n", "count"]
+ )
+ self.assertEqual(
+ [(m.name, m.subname) for m in wrapper._partials_meta], [("y", "x")]
+ )
+
+ def test_compute_partials_without_declared_partials(self):
+ """
+ When no variable holds continuous data, setup_partials() declares
+ nothing and compute_partials() does not linearize the wrapped
+ discipline at all.
+ """
+ discipline = DiscreteOnlyDiscipline()
+ wrapper = GEMSEOtoPhiloteDiscipline(discipline)
+ wrapper.setup()
+ wrapper.setup_partials()
+
+ partials = {}
+ with patch.object(discipline, "linearize") as linearize:
+ wrapper.compute_partials({}, partials, {"mode": "double"})
+
+ linearize.assert_not_called()
+ self.assertEqual(partials, {})
+
+
+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..1d8e3bd
--- /dev/null
+++ b/tests/test_openmdao_to_gemseo.py
@@ -0,0 +1,99 @@
+# 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
+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
+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))
+
+ # canonical Sellar starting point
+ out = sellar_mda.execute({"x": array([1.0]), "z": array([5.0, 2.0])})
+
+ server.stop(0)
+
+ # 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):
+ """
+ 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..7f14a9c
--- /dev/null
+++ b/tests/test_philote_to_gemseo.py
@@ -0,0 +1,269 @@
+# 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
+from numpy import eye
+from numpy import ndarray
+from numpy.testing import assert_allclose
+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"
+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 DiscreteDiscipline(pmdo.ExplicitDiscipline):
+ """A discipline with a discrete input and a discrete output.
+
+ The discrete input ``factor`` scales the continuous output ``y``, so
+ that both the outputs and the Jacobian depend on it, and the discrete
+ output ``tags`` echoes a non-scalar discrete value back to the client.
+ """
+
+ def setup(self):
+ self.add_input("x", shape=(2,))
+ self.add_discrete_input("factor", default=2.0)
+ self.add_output("y", shape=(2,))
+ self.add_discrete_output("tags")
+
+ def setup_partials(self):
+ self.declare_partials("y", "x")
+
+ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
+ outputs["y"] = discrete_inputs["factor"] * inputs["x"]
+ discrete_outputs["tags"] = ["scaled", discrete_inputs["factor"]]
+
+ def compute_partials(self, inputs, partials, discrete_inputs=None):
+ partials["y", "x"] = discrete_inputs["factor"] * eye(2)
+
+
+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)
+
+ 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)
+
+ 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_in_grammars(self):
+ """
+ Discrete server variables are added to the grammars next to the
+ continuous ones, and bound to no type, since they may carry any
+ JSON-compatible value.
+ """
+ self._serve(DiscreteDiscipline())
+
+ disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL))
+
+ self.assertEqual({n: disc.input_grammar[n] for n in disc.input_grammar},
+ {"x": ndarray, "factor": None})
+ self.assertEqual({n: disc.output_grammar[n] for n in disc.output_grammar},
+ {"y": ndarray, "tags": None})
+
+ def test_discrete_variables_compute(self):
+ """
+ Discrete inputs are sent to the server and discrete outputs are
+ returned in the local data, next to the continuous ones.
+ """
+ self._serve(DiscreteDiscipline())
+
+ disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL))
+
+ out = disc.execute({"x": array([1.0, 2.0]), "factor": 3.0})
+
+ assert_allclose(out["y"], array([3.0, 6.0]))
+ self.assertEqual(out["tags"], ["scaled", 3.0])
+
+ def test_discrete_input_changes_output(self):
+ """
+ A different discrete input value yields a different output, which
+ shows the value is actually sent rather than left at the
+ server-side default.
+ """
+ self._serve(DiscreteDiscipline())
+
+ disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL))
+
+ out = disc.execute({"x": array([1.0, 2.0]), "factor": 10.0})
+
+ assert_allclose(out["y"], array([10.0, 20.0]))
+
+ def test_discrete_variables_linearize(self):
+ """
+ Discrete variables are excluded from the full Jacobian, and the
+ discrete inputs are sent along with the continuous ones so that the
+ server can use them to compute the partials.
+ """
+ self._serve(DiscreteDiscipline())
+
+ disc = PhiloteDiscipline(channel=grpc.insecure_channel(CHANNEL))
+
+ jac = disc.linearize(
+ {"x": array([1.0, 2.0]), "factor": 3.0}, compute_all_jacobians=True
+ )
+
+ self.assertEqual(list(jac), ["y"])
+ self.assertEqual(list(jac["y"]), ["x"])
+ assert_allclose(jac["y"]["x"], 3.0 * eye(2))
+
+
+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__":