Skip to content
Draft
40 changes: 26 additions & 14 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
name: Deploy docs to GitHub Pages
name: Documentation

on:
push:
branches:
- main

pull_request:
branches:
- devel
- main
workflow_dispatch:

defaults:
Expand All @@ -13,28 +16,23 @@ defaults:

permissions:
contents: read
pages: write
id-token: write
packages: read

concurrency:
group: "pages"
cancel-in-progress: false
group: "docs-${{ github.ref }}"
cancel-in-progress: true

jobs:
build-and-deploy:
build-docs:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.11"

- name: Install prerequisites (Ubuntu)
uses: ./.github/actions/install/ubuntu-latest
Expand All @@ -52,18 +50,32 @@ jobs:
uses: ./.github/actions/compile

- name: Build Sphinx docs
env:
STRUPHY_DOCS_SKIP_TUTORIALS: "1"
run: |
cd doc
make html

- name: Setup Pages
uses: actions/configure-pages@v5

- name: Upload built docs
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: doc/_build/html/

deploy-docs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build-docs
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Setup Pages
uses: actions/configure-pages@v5

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
8 changes: 7 additions & 1 deletion .github/workflows/reusable-unit-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ jobs:
with:
env-name: /struphy_fortran_/env_fortran_

- name: Test plasma-plots integration
if: matrix.shard == 'shard-2'
run: |
source /struphy_fortran_/env_fortran_/bin/activate
python -m pytest "$STRUPHY_PATH/post_processing/tests/test_plasma_plots_integration.py" -v

- name: Run unit tests
env:
TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-unit-${{ matrix.shard }}
Expand All @@ -143,4 +149,4 @@ jobs:
$UNINSTALL_MPI
git status || true
echo "Running $PYTEST_CMD $TEST_FILES"
$PYTEST_CMD $TEST_FILES
$PYTEST_CMD $TEST_FILES
24 changes: 16 additions & 8 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@
"sphinx_design",
"sphinx_copybutton",
"rst_docstring", # Custom extension to use __doc_rst__
"sphinx_collections",
]

_skip_tutorials = os.environ.get("STRUPHY_DOCS_SKIP_TUTORIALS") == "1"
if not _skip_tutorials:
extensions.append("sphinx_collections")
else:
tags.add("skip_tutorials") # noqa: F821 - injected by Sphinx


def _struphy_is_compiled():
"""Whether `struphy compile` has been run (state.yml exists in the installed package)."""
Expand All @@ -75,14 +80,15 @@ def _struphy_is_compiled():
napoleon_attr_annotations = False

# Set path to outside sources for sphinxcontrib-collections.
collections = {
"notebooks": {
"driver": "copy_folder",
"source": "../tutorials/",
"target": "tutorials/",
"ignore": ["*.py", ".sh"],
if not _skip_tutorials:
collections = {
"notebooks": {
"driver": "copy_folder",
"source": "../tutorials/",
"target": "tutorials/",
"ignore": ["*.py", ".sh"],
}
}
}

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
Expand All @@ -91,6 +97,8 @@ def _struphy_is_compiled():
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
if _skip_tutorials:
exclude_patterns.extend(["sections/tutorials.rst", "sections/dev_guide.rst"])

# mock imports
# autodoc_mock_imports = ["mpi4py"]
Expand Down
9 changes: 6 additions & 3 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,22 @@ The code is freely available under an `MIT license <https://github.com/struphy-h
sections/api_guide
sections/models
sections/propagators
sections/tutorials
sections/numerics
sections/domains
sections/fluid-equils
sections/kinetic-equils
sections/perturbations
sections/dev_guide
sections/dev_reference

.. only:: not skip_tutorials

.. toctree::
:hidden:

sections/tutorials
sections/dev_guide






2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ phys = [
"desc-opt<=0.17.1",
]
pproc = [
"plasma-plots>=0.1.0",
"plasma-plots>=0.1.1",
]
dev = [
"struphy[mpi]",
Expand Down
132 changes: 132 additions & 0 deletions src/struphy/post_processing/tests/test_plasma_plots_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Small real simulation exercising optional plasma-plots and the output CLI."""

import os
import shutil
import subprocess
import sys
from pathlib import Path

import numpy as np
import pytest
import xarray as xr

from struphy import DerhamOptions, EnvironmentOptions, Output, Simulation, Time, grids, perturbations
from struphy.models import Maxwell


def run_command(*args):
"""Include CLI diagnostics in pytest's failure report."""
result = subprocess.run(
list(map(str, args)),
capture_output=True,
text=True,
env={**os.environ, "MPLBACKEND": "Agg"},
timeout=120,
)
assert result.returncode == 0, (
f"Command failed ({result.returncode}): {result.args}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
return result.stdout


@pytest.fixture(scope="module")
def raw_run(tmp_path_factory):
root = tmp_path_factory.mktemp("maxwell")
model = Maxwell()
model.em_fields.e_field.save_data = True
model.em_fields.b_field.save_data = True
model.em_fields.e_field.add_perturbation(
perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(0.1,), comp=0, given_in_basis="1")
)
sim = Simulation(
model=model,
env=EnvironmentOptions(out_folders=str(root), sim_folder="raw", save_step=1),
time_opts=Time(dt=0.025, Tend=0.2),
grid=grids.TensorProductGrid(num_elements=(1, 1, 8)),
derham_opts=DerhamOptions(degree=(1, 1, 2)),
)
out = sim.run()
assert (out.path_out / "run_metadata.json").is_file()
assert not out.is_processed
assert out.scalars.sizes["t"] >= 8
assert np.isfinite(out.scalars.to_array()).all()
out.close()
return out.path_out


@pytest.fixture
def run_path(raw_run, tmp_path):
# Each interface must create its own products from the original raw output.
return Path(shutil.copytree(raw_run, tmp_path / "run"))


def assert_image(path):
assert path.is_file() and path.stat().st_size > 100
assert path.read_bytes().startswith(b"\x89PNG\r\n\x1a\n")


def test_python_api(run_path):
# No explicit plasma_plots import: Output must register its accessors.
with Output(run_path) as out:
out.pproc(physical=True)
assert out.is_processed
assert "em_fields/e_field" in out.keys()
field = out.evaluate("em_fields/e_field", representation="1")
assert isinstance(field, xr.DataArray)
assert np.isfinite(field).all()
signal = field.isel(component=0, eta1=0, eta2=0)
assert float(signal.std("t").max()) > 0

spectrum = out.analysis.time_fft(signal)
assert "omega" in spectrum.dims
assert np.isfinite(spectrum.power).all()
np.testing.assert_allclose(spectrum.power.sum("omega"), (signal**2).mean("t"), atol=1e-12)
filtered = signal.plasma.analysis.filter_time()
assert filtered.filtered.dims == signal.dims
assert bool(filtered.spectrum.has_peak.all())
assert np.isfinite(filtered.filtered).all()

energies = run_path / "energies.png"
out.plot.energies(parts=["electric_energy", "magnetic_energy"], total="total_energy").save(energies)
profile = run_path / "electric-field.png"
signal.plasma.plot.lineout(x="eta3", t=-1).save(profile)
assert_image(energies)
assert_image(profile)


def test_cli(run_path):
cli = Path(sys.executable).parent / "struphy"

def command(*args):
return run_command(cli, "output", *args)

command("pproc", run_path, "--physical")
with Output(run_path) as out:
assert out.is_processed
assert np.isfinite(out.evaluate("em_fields/e_field_xyz")).all()
assert "em_fields/e_field" in command("keys", run_path)
assert "em_fields/e_field" in command("info", run_path)
report_dir = run_path / "cli-report"
command("report", run_path, "--format", "html", "--directory", report_dir)
reports = list(report_dir.glob("*.html"))
assert reports and "total_energy" in reports[0].read_text()

def plasma_command(*args):
return run_command(sys.executable, "-m", "plasma_plots", *args)

assert "em_fields/e_field" in plasma_command("info", run_path)
energy_plot = run_path / "cli-energies.png"
plasma_command(
"plot",
run_path,
".",
"energies",
'parts=["electric_energy","magnetic_energy"]',
"total=total_energy",
"-o",
energy_plot,
)
spectrum_plot = run_path / "cli-spectrum.png"
plasma_command("plot", run_path, "em_fields/e_field", "power_spectrum", "component=0", "-o", spectrum_plot)
assert_image(energy_plot)
assert_image(spectrum_plot)
Loading