From 4267653dda5c60c73a2b3ba295e02c8dd394125b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 19 Oct 2026 15:39:17 +0200 Subject: [PATCH 1/2] Register CurlCurl, ToyDrift, IncompressibleNavierStokesSPH in struphy.api.models; fix model and propagator templates - api/models/__init__.py: add the three missing models; new test checks that its __all__ matches struphy.models (closes #793) - models/base.py: docstring now says one model per module and where to register it, instead of fluid.py/kinetic.py/... (closes #794) - propagators/base.py: template option is opt1: OptsTemplate = "implicit" instead of a tuple annotated as str (closes #795) Co-Authored-By: Claude Opus 5.5 --- src/struphy/api/models/__init__.py | 6 ++++++ src/struphy/api/tests/test_public_api.py | 18 ++++++++++++++++++ src/struphy/models/base.py | 5 +++-- src/struphy/propagators/base.py | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/struphy/api/models/__init__.py b/src/struphy/api/models/__init__.py index 966384b90..9a120106c 100644 --- a/src/struphy/api/models/__init__.py +++ b/src/struphy/api/models/__init__.py @@ -1,5 +1,6 @@ from struphy.models.cold_plasma import ColdPlasma from struphy.models.cold_plasma_vlasov import ColdPlasmaVlasov +from struphy.models.curl_curl import CurlCurl from struphy.models.deterministic_particle_diffusion import ( DeterministicParticleDiffusion, ) @@ -9,6 +10,7 @@ from struphy.models.guiding_center import GuidingCenter from struphy.models.gyrokinetic_poisson import GyrokineticPoisson from struphy.models.hasegawa_wakatani import HasegawaWakatani +from struphy.models.incompressible_navier_stokes_sph import IncompressibleNavierStokesSPH from struphy.models.linear_extended_mh_duniform import LinearExtendedMHDuniform from struphy.models.linear_mhd import LinearMHD from struphy.models.linear_mhd_driftkinetic_cc import LinearMHDDriftkineticCC @@ -25,6 +27,7 @@ from struphy.models.shear_alfven import ShearAlfven from struphy.models.shear_alfven_curl_curl import ShearAlfvenCurlCurl from struphy.models.shear_alfven_curl_curl_with_proj import ShearAlfvenCurlCurlwithProj +from struphy.models.toy_drift import ToyDrift from struphy.models.two_fluid_quasi_neutral_toy import TwoFluidQuasiNeutralToy from struphy.models.variational_barotropic_fluid import VariationalBarotropicFluid from struphy.models.variational_compressible_fluid import VariationalCompressibleFluid @@ -49,11 +52,13 @@ __all__ = [ "ColdPlasma", "ColdPlasmaVlasov", + "CurlCurl", "DeterministicParticleDiffusion", "DriftKineticElectrostaticAdiabatic", "GuidingCenter", "GyrokineticPoisson", "HasegawaWakatani", + "IncompressibleNavierStokesSPH", "LinearExtendedMHDuniform", "LinearMHD", "LinearMHDDriftkineticCC", @@ -68,6 +73,7 @@ "ShearAlfven", "ShearAlfvenCurlCurl", "ShearAlfvenCurlCurlwithProj", + "ToyDrift", "TwoFluidQuasiNeutralToy", "VariationalBarotropicFluid", "VariationalCompressibleFluid", diff --git a/src/struphy/api/tests/test_public_api.py b/src/struphy/api/tests/test_public_api.py index e9211ac27..4f9cdd1b4 100644 --- a/src/struphy/api/tests/test_public_api.py +++ b/src/struphy/api/tests/test_public_api.py @@ -31,3 +31,21 @@ def test_post_processing_exports(): capture_output=True, text=True, ) + + +def test_api_models_match_models(): + # Parse instead of import: importing struphy.api.models loads every model. + import ast + from pathlib import Path + + import struphy.models + + api_init = Path(struphy.models.__file__).parents[1] / "api" / "models" / "__init__.py" + tree = ast.parse(api_init.read_text()) + (api_all,) = [ + ast.literal_eval(node.value) + for node in tree.body + if isinstance(node, ast.Assign) and any(getattr(t, "id", None) == "__all__" for t in node.targets) + ] + assert sorted(api_all) == sorted(struphy.models.__all__) + assert sorted(struphy.models.__all__) == sorted(struphy.models._LAZY_IMPORTS) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 0abc45730..406a72493 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -81,8 +81,9 @@ class StruphyModel(metaclass=StruphyModelMeta): Notes ----- - All Struphy models must be subclasses of ``StruphyModel`` and should be added to ``struphy/models/`` - in one of the modules: ``fluid.py``, ``kinetic.py``, ``hybrid.py``, or ``toy.py``. + All Struphy models must be subclasses of ``StruphyModel``. Each model lives in its own module + ``struphy/models/.py`` (snake case of the class name) and is registered in + ``struphy/models/__init__.py`` and ``struphy/api/models/__init__.py``. Time integration is performed by calling the ``integrate()`` method with a time step and splitting algorithm (Lie-Trotter or Strang). The model manages the execution of all propagators diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index 4353c0b08..aa7e399d7 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -66,7 +66,7 @@ class Options(OptionsBase): # specific literals OptsTemplate = Literal["implicit", "explicit"] # propagator options - opt1: str = ("implicit",) + opt1: OptsTemplate = "implicit" def __post_init__(self): # checks From 546516868737436accc45f64d968979a1176b647 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 19 Oct 2026 15:42:17 +0200 Subject: [PATCH 2/2] Run api/tests in CI, stricter api models test, mention build-init-files Review follow-ups: api/tests/ was in no CI shard; added to shard-2 of both workflows and to struphy test unit. The test now also checks that each exported model is imported from its defining module. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/reusable-scheduled.yml | 2 +- .github/workflows/reusable-unit-testing.yml | 2 +- src/struphy/api/tests/__init__.py | 0 src/struphy/api/tests/test_public_api.py | 14 +++++++++++--- src/struphy/console/test.py | 1 + src/struphy/models/base.py | 5 +++-- 6 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 src/struphy/api/tests/__init__.py diff --git a/.github/workflows/reusable-scheduled.yml b/.github/workflows/reusable-scheduled.yml index e4652bd23..68f7f8f82 100644 --- a/.github/workflows/reusable-scheduled.yml +++ b/.github/workflows/reusable-scheduled.yml @@ -107,7 +107,7 @@ jobs: TEST_FILES="bsplines/tests/ console/tests/ fields_background/tests/ geometry/tests/ initial/tests/ kinetic_background/tests/ linear_algebra/tests/" ;; shard-2) - TEST_FILES="propagators/tests/ ode/tests/ polar/tests/ post_processing/tests/ dispersion_relations/tests/ io/tests/ simulation/tests/ models/tests/test_gpu_LinearVlasovAmpereOneSpecies.py models/tests/test_gpu_VlasovAmpereOneSpecies.py" + TEST_FILES="api/tests/ propagators/tests/ ode/tests/ polar/tests/ post_processing/tests/ dispersion_relations/tests/ io/tests/ simulation/tests/ models/tests/test_gpu_LinearVlasovAmpereOneSpecies.py models/tests/test_gpu_VlasovAmpereOneSpecies.py" ;; shard-3) TEST_FILES="feec/tests/" diff --git a/.github/workflows/reusable-unit-testing.yml b/.github/workflows/reusable-unit-testing.yml index 33c3b7a35..3864101f6 100644 --- a/.github/workflows/reusable-unit-testing.yml +++ b/.github/workflows/reusable-unit-testing.yml @@ -39,7 +39,7 @@ jobs: - shard: shard-1 test_files: bsplines/tests/ console/tests/ fields_background/tests/ geometry/tests/ initial/tests/ kinetic_background/tests/ linear_algebra/tests/ - shard: shard-2 - test_files: propagators/tests/ ode/tests/ polar/tests/ post_processing/tests/ dispersion_relations/tests/ io/tests/ simulation/tests/ models/tests/test_gpu_LinearVlasovAmpereOneSpecies.py models/tests/test_gpu_VlasovAmpereOneSpecies.py + test_files: api/tests/ propagators/tests/ ode/tests/ polar/tests/ post_processing/tests/ dispersion_relations/tests/ io/tests/ simulation/tests/ models/tests/test_gpu_LinearVlasovAmpereOneSpecies.py models/tests/test_gpu_VlasovAmpereOneSpecies.py - shard: shard-3 test_files: feec/tests/ - shard: shard-4 diff --git a/src/struphy/api/tests/__init__.py b/src/struphy/api/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/struphy/api/tests/test_public_api.py b/src/struphy/api/tests/test_public_api.py index 4f9cdd1b4..33bbe7e39 100644 --- a/src/struphy/api/tests/test_public_api.py +++ b/src/struphy/api/tests/test_public_api.py @@ -1,7 +1,13 @@ """Regression coverage for the public lazy imports.""" +import ast import subprocess import sys +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.mpi_skip def test_post_processing_exports(): @@ -35,9 +41,6 @@ def test_post_processing_exports(): def test_api_models_match_models(): # Parse instead of import: importing struphy.api.models loads every model. - import ast - from pathlib import Path - import struphy.models api_init = Path(struphy.models.__file__).parents[1] / "api" / "models" / "__init__.py" @@ -47,5 +50,10 @@ def test_api_models_match_models(): for node in tree.body if isinstance(node, ast.Assign) and any(getattr(t, "id", None) == "__all__" for t in node.targets) ] + api_imports = { + alias.name: node.module for node in tree.body if isinstance(node, ast.ImportFrom) for alias in node.names + } assert sorted(api_all) == sorted(struphy.models.__all__) assert sorted(struphy.models.__all__) == sorted(struphy.models._LAZY_IMPORTS) + # every exported name is imported from the module that defines it + assert api_imports == struphy.models._LAZY_IMPORTS diff --git a/src/struphy/console/test.py b/src/struphy/console/test.py index 0af89df1f..264373cf3 100644 --- a/src/struphy/console/test.py +++ b/src/struphy/console/test.py @@ -44,6 +44,7 @@ def struphy_test( if "unit" in group: list_of_tests = [ + f"{LIBPATH}/api/tests/", f"{LIBPATH}/bsplines/tests/", f"{LIBPATH}/console/tests/", f"{LIBPATH}/dispersion_relations/tests/", diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 406a72493..7d969f23d 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -82,8 +82,9 @@ class StruphyModel(metaclass=StruphyModelMeta): Notes ----- All Struphy models must be subclasses of ``StruphyModel``. Each model lives in its own module - ``struphy/models/.py`` (snake case of the class name) and is registered in - ``struphy/models/__init__.py`` and ``struphy/api/models/__init__.py``. + ``struphy/models/.py`` (snake case of the class name). After adding a model, regenerate + ``struphy/models/__init__.py`` with ``struphy build-init-files`` and add the model by hand to + ``struphy/api/models/__init__.py``. Time integration is performed by calling the ``integrate()`` method with a time step and splitting algorithm (Lie-Trotter or Strang). The model manages the execution of all propagators