From 4eaac80eda15c5335403ecb469109ad6af9dc47d Mon Sep 17 00:00:00 2001 From: Philip Korsager Nickel Date: Fri, 28 Nov 2025 16:35:34 +0100 Subject: [PATCH 1/2] lots of fixes and so --- .github/workflows/docs.yml | 71 ++++++ .gitignore | 3 +- CODE_REVIEW.md | 37 ---- Experiments/01-kernels/compute_all.py | 56 +++-- Experiments/01-kernels/plot_kernels.py | 93 ++++---- .../02-decomposition/plot_decompositions.py | 21 +- .../03-communication/compute_communication.py | 54 +++-- .../03-communication/plot_communication.py | 39 ++-- .../04-validation/compute_validation.py | 4 +- Experiments/04-validation/plot_validation.py | 88 ++++---- Experiments/05-scaling/compute_scaling.py | 168 ++++++++++++++ Experiments/05-scaling/submit_sweep.sh | 15 -- Experiments/05-scaling/sweep.py | 21 -- Experiments/05-scaling/template_config.yaml | 72 ++++++ data/01-kernels/kernel_benchmark.parquet | Bin 0 -> 102969 bytes data/01-kernels/kernel_convergence.parquet | Bin 0 -> 98796 bytes data/communication/communication_np4.parquet | Bin 0 -> 61729 bytes data/validation/N16_np8_cubic_custom.h5 | Bin 0 -> 1066608 bytes data/validation/N16_np8_cubic_numpy.h5 | Bin 0 -> 1066608 bytes data/validation/N16_np8_sliced_custom.h5 | Bin 0 -> 1066608 bytes data/validation/N16_np8_sliced_numpy.h5 | Bin 0 -> 1066608 bytes data/validation/N32_np8_cubic_custom.h5 | Bin 0 -> 1066608 bytes data/validation/N32_np8_cubic_numpy.h5 | Bin 0 -> 1066608 bytes data/validation/N32_np8_sliced_custom.h5 | Bin 0 -> 1066608 bytes data/validation/N32_np8_sliced_numpy.h5 | Bin 0 -> 1066608 bytes data/validation/N48_np8_cubic_custom.h5 | Bin 0 -> 1066608 bytes data/validation/N48_np8_cubic_numpy.h5 | Bin 0 -> 1066608 bytes data/validation/N48_np8_sliced_custom.h5 | Bin 0 -> 1066608 bytes data/validation/N48_np8_sliced_numpy.h5 | Bin 0 -> 1066608 bytes docs/source/api_reference.rst | 30 ++- docs/source/conf.py | 9 +- docs/source/sg_execution_times.rst | 24 +- main.py | 177 ++++++++++++++- pyproject.toml | 5 + setup_mlflow.py | 4 +- src/Poisson/__init__.py | 20 +- src/Poisson/datastructures.py | 19 +- src/Poisson/helpers/__init__.py | 5 + src/Poisson/{ => helpers}/runner.py | 16 +- src/Poisson/{ => helpers}/runner_helper.py | 15 +- src/Poisson/kernels.py | 18 +- src/Poisson/mpi/__init__.py | 12 + src/Poisson/{ => mpi}/communicators.py | 64 ++++-- src/Poisson/{ => mpi}/decomposition.py | 86 ++++---- src/Poisson/problems.py | 8 +- src/Poisson/{jacobi.py => solver.py} | 171 ++++++++++++--- src/utils/__init__.py | 9 +- src/utils/datatools.py | 204 ----------------- src/utils/generate_pack.py | 58 +++++ src/utils/hpc.py | 168 ++++++++++++++ src/utils/mlflow_io.py | 207 ++++++++++++++++++ src/utils/upload_logs.py | 80 +++++++ tests/test_communicators.py | 130 ----------- tests/test_decomposition.py | 53 +++-- tests/test_kernels.py | 59 +---- tests/test_mpi_integration.py | 185 ++++------------ tests/test_problems.py | 141 ------------ tests/test_solver.py | 143 ++++-------- 58 files changed, 1714 insertions(+), 1148 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 CODE_REVIEW.md create mode 100644 Experiments/05-scaling/compute_scaling.py delete mode 100644 Experiments/05-scaling/submit_sweep.sh delete mode 100644 Experiments/05-scaling/sweep.py create mode 100644 Experiments/05-scaling/template_config.yaml create mode 100644 data/01-kernels/kernel_benchmark.parquet create mode 100644 data/01-kernels/kernel_convergence.parquet create mode 100644 data/communication/communication_np4.parquet create mode 100644 data/validation/N16_np8_cubic_custom.h5 create mode 100644 data/validation/N16_np8_cubic_numpy.h5 create mode 100644 data/validation/N16_np8_sliced_custom.h5 create mode 100644 data/validation/N16_np8_sliced_numpy.h5 create mode 100644 data/validation/N32_np8_cubic_custom.h5 create mode 100644 data/validation/N32_np8_cubic_numpy.h5 create mode 100644 data/validation/N32_np8_sliced_custom.h5 create mode 100644 data/validation/N32_np8_sliced_numpy.h5 create mode 100644 data/validation/N48_np8_cubic_custom.h5 create mode 100644 data/validation/N48_np8_cubic_numpy.h5 create mode 100644 data/validation/N48_np8_sliced_custom.h5 create mode 100644 data/validation/N48_np8_sliced_numpy.h5 create mode 100644 src/Poisson/helpers/__init__.py rename src/Poisson/{ => helpers}/runner.py (79%) rename src/Poisson/{ => helpers}/runner_helper.py (82%) create mode 100644 src/Poisson/mpi/__init__.py rename src/Poisson/{ => mpi}/communicators.py (70%) rename src/Poisson/{ => mpi}/decomposition.py (86%) rename src/Poisson/{jacobi.py => solver.py} (61%) delete mode 100644 src/utils/datatools.py create mode 100644 src/utils/generate_pack.py create mode 100644 src/utils/hpc.py create mode 100644 src/utils/mlflow_io.py create mode 100644 src/utils/upload_logs.py delete mode 100644 tests/test_communicators.py delete mode 100644 tests/test_problems.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..8ecd9e3 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,71 @@ +name: Docs + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y texlive-latex-extra texlive-fonts-recommended texlive-science dvipng cm-super libosmesa6-dev libgl1-mesa-dev + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies + run: uv sync + + - name: Fetch MLflow artifacts + env: + DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} + run: uv run python main.py --fetch + + - name: Build Sphinx docs + env: + PYVISTA_OFF_SCREEN: "true" + PYTHONPATH: "${{ github.workspace }}" + run: uv run python main.py --docs + + - name: Upload artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: docs/build/html + + deploy: + if: github.ref == 'refs/heads/main' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index f8715dd..276a422 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ docs/reports/TexReport/ # Generated data (keep README.md) -data/* +#data/* # Uv stuff uv.lock @@ -18,6 +18,7 @@ docs/source/generated/ docs/source/example_gallery/ docs/source/_autosummary/ docs/source/gen_modules/backreferences/ +sg_execution_times* # macOS .DS_Store diff --git a/CODE_REVIEW.md b/CODE_REVIEW.md deleted file mode 100644 index 2a2cfe4..0000000 --- a/CODE_REVIEW.md +++ /dev/null @@ -1,37 +0,0 @@ -# Code Review - COMPLETED - -All phases completed successfully. 68 tests passing. - -## Summary of Changes - -### Phase 0: Pytest Suite -- `tests/test_kernels.py` - Kernel correctness & O(h²) convergence -- `tests/test_decomposition.py` - Domain decomposition logic -- `tests/test_communicators.py` - Halo exchange (NumpyHaloExchange, CustomHaloExchange) -- `tests/test_solver.py` - JacobiPoisson solver convergence & accuracy (single-rank) -- `tests/test_problems.py` - Grid creation & sinusoidal problem setup -- `tests/test_mpi_integration.py` - End-to-end MPI tests via subprocess runner (1-8 ranks) - -### Phase 1: Bug Fixes & Enhancements -- Fixed warmup parameter mismatch in `kernels.py` -- Scaling experiment converted to placeholder -- Added configurable `axis` parameter to sliced decomposition - -### Phase 2: Communicators Refactor -- Renamed: `NumpyCommunicator` → `NumpyHaloExchange` -- Renamed: `DatatypeCommunicator` → `CustomHaloExchange` -- Extracted `_get_neighbor_ranks()` helper (~60 lines reduced) -- Created `_BaseHaloExchange` base class - -### Phase 3: Experiments Refactor -- `plot_decompositions.py`: 134 → 84 lines (extracted `visualize_decomposition`) -- `compute_all.py`: 175 → 106 lines (extracted `run_kernel`, `kernel_to_df`) - -### Phase 4: MPI Correctness -- Fixed hardcoded byte stride → `MPI.DOUBLE.Get_size()` -- Added offset validation assertions in `CustomHaloExchange` - -### Phase 5: Documentation & Terminology -- Standardized on "halo" terminology (replaced "ghost") -- Renamed method: `exchange_ghosts` → `exchange_halos` -- Renamed field: `ghost_cells_total` → `halo_cells_total` diff --git a/Experiments/01-kernels/compute_all.py b/Experiments/01-kernels/compute_all.py index 28f3198..90095ec 100644 --- a/Experiments/01-kernels/compute_all.py +++ b/Experiments/01-kernels/compute_all.py @@ -5,9 +5,9 @@ 1. Convergence validation with analytical solution 2. Fixed iteration performance benchmark """ + import numpy as np import pandas as pd -from pathlib import Path from dataclasses import asdict from scipy.ndimage import laplace @@ -29,10 +29,10 @@ def run_kernel(kernel, f, max_iter, track_algebraic=False): if track_algebraic: kernel.timeseries.physical_errors = [] - h2 = kernel.parameters.h ** 2 + h2 = kernel.parameters.h**2 for _ in range(max_iter): - residual = kernel.step(u_old, u, f) + kernel.step(u_old, u, f) if track_algebraic: Au = -laplace(u) / h2 @@ -47,10 +47,10 @@ def run_kernel(kernel, f, max_iter, track_algebraic=False): def kernel_to_df(kernel, kernel_name, N, omega, **extra): """Convert kernel timeseries to DataFrame.""" df = pd.DataFrame(asdict(kernel.timeseries)) - df['iteration'] = range(len(df)) - df['kernel'] = kernel_name - df['N'] = N - df['omega'] = omega + df["iteration"] = range(len(df)) + df["kernel"] = kernel_name + df["N"] = N + df["omega"] = omega for k, v in extra.items(): df[k] = v return df @@ -67,14 +67,18 @@ def kernel_to_df(kernel, kernel_name, N, omega, **extra): f = problems.sinusoidal_source_term(N) numpy_kernel = NumPyKernel(N=N, omega=omega, tolerance=0.0, max_iter=max_iter) - numba_kernel = NumbaKernel(N=N, omega=omega, tolerance=0.0, max_iter=max_iter, numba_threads=4) + numba_kernel = NumbaKernel( + N=N, omega=omega, tolerance=0.0, max_iter=max_iter, numba_threads=4 + ) numba_kernel.warmup() - for name, kernel in [('numpy', numpy_kernel), ('numba', numba_kernel)]: + for name, kernel in [("numpy", numpy_kernel), ("numba", numba_kernel)]: run_kernel(kernel, f, max_iter, track_algebraic=True) all_dfs.append(kernel_to_df(kernel, name, N, omega, tolerance=0.0)) -pd.concat(all_dfs, ignore_index=True).to_parquet(data_dir / "kernel_convergence.parquet", index=False) +pd.concat(all_dfs, ignore_index=True).to_parquet( + data_dir / "kernel_convergence.parquet", index=False +) # Experiment 2: Fixed Iteration Benchmark @@ -90,16 +94,38 @@ def kernel_to_df(kernel, kernel_name, N, omega, **extra): kernel = NumPyKernel(N=N, omega=omega, tolerance=0.0, max_iter=max_iter) f = np.ones((N, N, N), dtype=np.float64) run_kernel(kernel, f, max_iter) - all_dfs.append(kernel_to_df(kernel, 'numpy', N, omega, max_iter=max_iter, use_numba=False, num_threads=0)) + all_dfs.append( + kernel_to_df( + kernel, "numpy", N, omega, max_iter=max_iter, use_numba=False, num_threads=0 + ) + ) # Numba with thread scaling for num_threads in thread_counts: for idx, N in enumerate(problem_sizes): - kernel = NumbaKernel(N=N, omega=omega, tolerance=0.0, max_iter=max_iter, numba_threads=num_threads) + kernel = NumbaKernel( + N=N, + omega=omega, + tolerance=0.0, + max_iter=max_iter, + numba_threads=num_threads, + ) if idx == 0: kernel.warmup() f = np.ones((N, N, N), dtype=np.float64) run_kernel(kernel, f, max_iter) - all_dfs.append(kernel_to_df(kernel, 'numba', N, omega, max_iter=max_iter, use_numba=True, num_threads=num_threads)) - -pd.concat(all_dfs, ignore_index=True).to_parquet(data_dir / "kernel_benchmark.parquet", index=False) + all_dfs.append( + kernel_to_df( + kernel, + "numba", + N, + omega, + max_iter=max_iter, + use_numba=True, + num_threads=num_threads, + ) + ) + +pd.concat(all_dfs, ignore_index=True).to_parquet( + data_dir / "kernel_benchmark.parquet", index=False +) diff --git a/Experiments/01-kernels/plot_kernels.py b/Experiments/01-kernels/plot_kernels.py index 57f816c..3dd1ecc 100644 --- a/Experiments/01-kernels/plot_kernels.py +++ b/Experiments/01-kernels/plot_kernels.py @@ -4,6 +4,7 @@ Comprehensive analysis and visualization of NumPy vs Numba kernel benchmarks. """ + import pandas as pd import matplotlib.pyplot as plt import seaborn as sns @@ -23,8 +24,12 @@ fig_dir.mkdir(parents=True, exist_ok=True) # Check if data exists -if not data_dir.exists(): - raise FileNotFoundError(f"Data not found: {data_dir}. Run compute_kernels.py first.") +if not list(data_dir.glob("*.parquet")): + print(f"Data not found: {data_dir}. Run compute_kernels.py first.") + # Graceful exit for docs build + import sys + + sys.exit(0) # %% # Plot 1: Convergence Validation @@ -37,21 +42,21 @@ # Create faceted plot: one subplot per problem size g = sns.relplot( data=df_conv, - x='iteration', - y='physical_errors', - col='N', - hue='kernel', + x="iteration", + y="physical_errors", + col="N", + hue="kernel", style="kernel", - kind='line', + kind="line", dashes=True, markers=False, - facet_kws={'sharey': True, 'sharex': False} + facet_kws={"sharey": True, "sharex": False}, ) - g.set(xscale='log', yscale='log') - g.set_axis_labels('Iteration', r'Algebraic Residual $||Au - f||_\infty$') - g.set_titles(col_template='N={col_name}') - g.fig.suptitle(r'Kernel Convergence Validation', y=1.02) + g.set(xscale="log", yscale="log") + g.set_axis_labels("Iteration", r"Algebraic Residual $||Au - f||_\infty$") + g.set_titles(col_template="N={col_name}") + g.fig.suptitle(r"Kernel Convergence Validation", y=1.02) # Save figure g.savefig(fig_dir / "01_convergence_validation.pdf") @@ -64,13 +69,14 @@ df = pd.read_parquet(benchmark_file) # Convert to milliseconds -df['time_ms'] = df['compute_times'] * 1000 +df["time_ms"] = df["compute_times"] * 1000 # Prepare configuration labels -df['config'] = df.apply( - lambda row: 'NumPy' if row['kernel'] == 'numpy' +df["config"] = df.apply( + lambda row: "NumPy" + if row["kernel"] == "numpy" else f"Numba ({int(row['num_threads'])} threads)", - axis=1 + axis=1, ) # %% @@ -81,21 +87,21 @@ fig, ax = plt.subplots() sns.lineplot( data=df, - x='N', - y='time_ms', - hue='config', - style='config', + x="N", + y="time_ms", + hue="config", + style="config", markers=True, dashes=False, - errorbar='ci', # Show confidence intervals - ax=ax + errorbar="ci", # Show confidence intervals + ax=ax, ) -ax.set_xscale('log') -ax.set_yscale('log') -ax.set_xlabel('Problem Size (N)') -ax.set_ylabel('Time per Iteration (ms)') -ax.set_title('Kernel Performance Comparison') +ax.set_xscale("log") +ax.set_yscale("log") +ax.set_xlabel("Problem Size (N)") +ax.set_ylabel("Time per Iteration (ms)") +ax.set_title("Kernel Performance Comparison") fig.savefig(fig_dir / "02_performance.pdf") @@ -104,30 +110,33 @@ # ------------------------- # Compute numpy baseline for each N and iteration -df_numpy = df[df['kernel'] == 'numpy'][['N', 'iteration', 'compute_times']].rename( - columns={'compute_times': 'numpy_time'} +df_numpy = df[df["kernel"] == "numpy"][["N", "iteration", "compute_times"]].rename( + columns={"compute_times": "numpy_time"} +) +df_speedup = df[df["kernel"] == "numba"].merge( + df_numpy, on=["N", "iteration"], how="left" +) +df_speedup["speedup"] = df_speedup["numpy_time"] / df_speedup["compute_times"] +df_speedup["thread_label"] = ( + df_speedup["num_threads"].astype(int).astype(str) + " threads" ) -df_speedup = df[df['kernel'] == 'numba'].merge(df_numpy, on=['N', 'iteration'], how='left') -df_speedup['speedup'] = df_speedup['numpy_time'] / df_speedup['compute_times'] -df_speedup['thread_label'] = df_speedup['num_threads'].astype(int).astype(str) + ' threads' # Create speedup plot - seaborn will compute mean and error bars fig, ax = plt.subplots() sns.lineplot( data=df_speedup, - x='N', - y='speedup', - hue='thread_label', - style='thread_label', + x="N", + y="speedup", + hue="thread_label", + style="thread_label", markers=True, dashes=False, - errorbar='ci', - ax=ax + errorbar="ci", + ax=ax, ) -ax.set_xlabel('Problem Size (N)') -ax.set_ylabel('Speedup vs NumPy') -ax.set_title('Fixed Iteration Speedup (200 iterations)') +ax.set_xlabel("Problem Size (N)") +ax.set_ylabel("Speedup vs NumPy") +ax.set_title("Fixed Iteration Speedup (200 iterations)") fig.savefig(fig_dir / "03_speedup_fixed_iter.pdf") - diff --git a/Experiments/02-decomposition/plot_decompositions.py b/Experiments/02-decomposition/plot_decompositions.py index dec50b5..4015a50 100644 --- a/Experiments/02-decomposition/plot_decompositions.py +++ b/Experiments/02-decomposition/plot_decompositions.py @@ -4,6 +4,7 @@ Visualize how domain partitioning works for sliced vs cubic decompositions. """ + import matplotlib.pyplot as plt import pyvista as pv from pyvista import themes @@ -15,7 +16,7 @@ # ----- pv.set_plot_theme(themes.ParaViewTheme()) -pv.global_theme.anti_aliasing = 'ssaa' +pv.global_theme.anti_aliasing = "ssaa" pv.global_theme.smooth_shading = True pv.global_theme.multi_samples = 16 @@ -32,7 +33,7 @@ # ------------------------------ # 1D decomposition along Z-axis - each rank owns horizontal slices. -decomp = DomainDecomposition(N=N, size=4, strategy='sliced') +decomp = DomainDecomposition(N=N, size=4, strategy="sliced") plotter = pv.Plotter(window_size=[1500, 1500], off_screen=True) for rank in range(4): @@ -41,11 +42,14 @@ z1, y1, x1 = info.global_end box = pv.Box(bounds=[x0, x1, y0, y1, z0, z1]) color = cmap(rank / 4)[:3] - plotter.add_mesh(box, opacity=0.4, color=color, show_edges=True, - edge_color='black', line_width=8) + plotter.add_mesh( + box, opacity=0.4, color=color, show_edges=True, edge_color="black", line_width=8 + ) plotter.add_axes() -plotter.screenshot(fig_dir / "01a_sliced_decomposition.png", transparent_background=True) +plotter.screenshot( + fig_dir / "01a_sliced_decomposition.png", transparent_background=True +) plotter.show() # %% @@ -53,7 +57,7 @@ # ----------------------------- # 3D Cartesian decomposition - domain split across all dimensions. -decomp = DomainDecomposition(N=N, size=8, strategy='cubic') +decomp = DomainDecomposition(N=N, size=8, strategy="cubic") plotter = pv.Plotter(window_size=[1500, 1500], off_screen=True) for rank in range(8): @@ -62,8 +66,9 @@ z1, y1, x1 = info.global_end box = pv.Box(bounds=[x0, x1, y0, y1, z0, z1]) color = cmap(rank / 8)[:3] - plotter.add_mesh(box, opacity=0.4, color=color, show_edges=True, - edge_color='black', line_width=8) + plotter.add_mesh( + box, opacity=0.4, color=color, show_edges=True, edge_color="black", line_width=8 + ) plotter.add_axes() plotter.screenshot(fig_dir / "01b_cubic_decomposition.png", transparent_background=True) diff --git a/Experiments/03-communication/compute_communication.py b/Experiments/03-communication/compute_communication.py index 182f862..9db1272 100644 --- a/Experiments/03-communication/compute_communication.py +++ b/Experiments/03-communication/compute_communication.py @@ -7,8 +7,8 @@ Uses per-iteration timeseries data for statistical analysis. """ + import subprocess -import sys from Poisson import get_project_root @@ -17,6 +17,7 @@ def main(): """Entry point - spawns MPI if needed.""" try: from mpi4py import MPI + if MPI.COMM_WORLD.Get_size() > 1: _run_benchmark() return @@ -24,7 +25,12 @@ def main(): pass # Spawn MPI - script = get_project_root() / "Experiments" / "03-communication" / "compute_communication.py" + script = ( + get_project_root() + / "Experiments" + / "03-communication" + / "compute_communication.py" + ) subprocess.run(["mpiexec", "-n", "4", "uv", "run", "python", str(script)]) @@ -32,7 +38,13 @@ def _run_benchmark(): """MPI worker - collects per-iteration timings.""" import pandas as pd from mpi4py import MPI - from Poisson import JacobiPoisson, DomainDecomposition, NumpyHaloExchange, CustomHaloExchange, get_project_root + from Poisson import ( + JacobiPoisson, + DomainDecomposition, + NumpyHaloExchange, + CustomHaloExchange, + get_project_root, + ) comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -70,10 +82,17 @@ def _run_benchmark(): print(f" {label}...", end=" ", flush=True) # Create and run solver - decomp = DomainDecomposition(N=N, size=size, strategy='sliced', axis=axis) - halo = CustomHaloExchange() if comm_type == "custom" else NumpyHaloExchange() - solver = JacobiPoisson(N=N, decomposition=decomp, communicator=halo, - max_iter=WARMUP + ITERATIONS, tolerance=0) + decomp = DomainDecomposition(N=N, size=size, strategy="sliced", axis=axis) + halo = ( + CustomHaloExchange() if comm_type == "custom" else NumpyHaloExchange() + ) + solver = JacobiPoisson( + N=N, + decomposition=decomp, + communicator=halo, + max_iter=WARMUP + ITERATIONS, + tolerance=0, + ) solver.solve() # Get max halo time across ranks per iteration (skip warmup) @@ -82,13 +101,20 @@ def _run_benchmark(): if rank == 0: local_N = N // size # Local subdomain size along decomposed axis - print(f"mean={sum(max_times)/len(max_times)*1e6:.1f} μs/iter") - dfs.append(pd.DataFrame({ - "N": N, "local_N": local_N, "axis": axis, - "communicator": comm_type, "label": label, - "iteration": range(len(max_times)), - "halo_time_us": [t * 1e6 for t in max_times], - })) + print(f"mean={sum(max_times) / len(max_times) * 1e6:.1f} μs/iter") + dfs.append( + pd.DataFrame( + { + "N": N, + "local_N": local_N, + "axis": axis, + "communicator": comm_type, + "label": label, + "iteration": range(len(max_times)), + "halo_time_us": [t * 1e6 for t in max_times], + } + ) + ) if rank == 0: df = pd.concat(dfs, ignore_index=True) diff --git a/Experiments/03-communication/plot_communication.py b/Experiments/03-communication/plot_communication.py index 53e647c..b1bb287 100644 --- a/Experiments/03-communication/plot_communication.py +++ b/Experiments/03-communication/plot_communication.py @@ -7,6 +7,7 @@ Uses per-iteration timeseries data for tight confidence intervals. """ + import matplotlib.pyplot as plt import pandas as pd import seaborn as sns @@ -18,7 +19,7 @@ # ----- sns.set_style("whitegrid") -plt.rcParams['figure.dpi'] = 100 +plt.rcParams["figure.dpi"] = 100 repo_root = get_project_root() data_dir = repo_root / "data" / "communication" @@ -31,7 +32,9 @@ parquet_files = list(data_dir.glob("communication_*.parquet")) if not parquet_files: - raise FileNotFoundError(f"No data found in {data_dir}. Run compute_communication.py first.") + raise FileNotFoundError( + f"No data found in {data_dir}. Run compute_communication.py first." + ) df = pd.concat([pd.read_parquet(f) for f in parquet_files], ignore_index=True) @@ -55,28 +58,30 @@ sns.lineplot( data=df, - x='local_N', - y='halo_time_us', - hue='label', - style='label', + x="local_N", + y="halo_time_us", + hue="label", + style="label", markers=True, dashes=False, palette=palette, ax=ax, - errorbar=('ci', 95), + errorbar=("ci", 95), markersize=8, linewidth=2, ) -ax.set_xlabel('Local Subdomain Size (N / nprocs)', fontsize=12) -ax.set_ylabel('Halo Exchange Time (μs)', fontsize=12) -ax.set_title('Halo Exchange Performance: Contiguous vs Non-Contiguous Memory', fontsize=13) -ax.legend(title='Configuration', fontsize=9, loc='upper left') +ax.set_xlabel("Local Subdomain Size (N / nprocs)", fontsize=12) +ax.set_ylabel("Halo Exchange Time (μs)", fontsize=12) +ax.set_title( + "Halo Exchange Performance: Contiguous vs Non-Contiguous Memory", fontsize=13 +) +ax.legend(title="Configuration", fontsize=9, loc="upper left") ax.grid(True, alpha=0.3) plt.tight_layout() output_file = fig_dir / "communication_comparison.pdf" -plt.savefig(output_file, bbox_inches='tight') +plt.savefig(output_file, bbox_inches="tight") print(f"Saved: {output_file}") plt.show() @@ -87,7 +92,9 @@ print("\n" + "=" * 70) print("Summary: Mean halo time (μs) with 95% CI by local subdomain size") print("=" * 70) -summary = df.groupby(['local_N', 'label'])['halo_time_us'].agg(['mean', 'std', 'count']) -summary['ci95'] = 1.96 * summary['std'] / (summary['count'] ** 0.5) -summary['display'] = summary.apply(lambda r: f"{r['mean']:.1f} ± {r['ci95']:.1f}", axis=1) -print(summary['display'].unstack().to_string()) +summary = df.groupby(["local_N", "label"])["halo_time_us"].agg(["mean", "std", "count"]) +summary["ci95"] = 1.96 * summary["std"] / (summary["count"] ** 0.5) +summary["display"] = summary.apply( + lambda r: f"{r['mean']:.1f} ± {r['ci95']:.1f}", axis=1 +) +print(summary["display"].unstack().to_string()) diff --git a/Experiments/04-validation/compute_validation.py b/Experiments/04-validation/compute_validation.py index 100e9d6..5360d08 100644 --- a/Experiments/04-validation/compute_validation.py +++ b/Experiments/04-validation/compute_validation.py @@ -43,8 +43,8 @@ ) if "error" in result: - print(f"ERROR") + print("ERROR") else: - print(f"done") + print("done") print(f"\nSaved results to: {data_dir}") diff --git a/Experiments/04-validation/plot_validation.py b/Experiments/04-validation/plot_validation.py index 8799b3c..ea30fea 100644 --- a/Experiments/04-validation/plot_validation.py +++ b/Experiments/04-validation/plot_validation.py @@ -39,28 +39,35 @@ # Load validation data from HDF5 files h5_files = list(data_dir.glob("*.h5")) if not h5_files: - raise FileNotFoundError(f"No data found in {data_dir}. Run compute_validation.py first.") + raise FileNotFoundError( + f"No data found in {data_dir}. Run compute_validation.py first." + ) -df = pd.concat([pd.read_hdf(f, key='results') for f in h5_files], ignore_index=True) +df = pd.concat([pd.read_hdf(f, key="results") for f in h5_files], ignore_index=True) print(f"\nLoaded {len(df)} validation results") print(f"Strategies: {df['decomposition'].unique()}") print(f"Problem sizes: {sorted(df['N'].unique())}") # Create labels for plotting -df['Strategy'] = df['decomposition'].str.capitalize() -df['Communicator'] = df['communicator'].str.replace('haloexchange', '').str.replace('custom', 'Custom').str.replace('numpy', 'NumPy') -df['Method'] = df['Strategy'] + ' + ' + df['Communicator'] +df["Strategy"] = df["decomposition"].str.capitalize() +df["Communicator"] = ( + df["communicator"] + .str.replace("haloexchange", "") + .str.replace("custom", "Custom") + .str.replace("numpy", "NumPy") +) +df["Method"] = df["Strategy"] + " + " + df["Communicator"] # Use lineplot (single rank count = 8) fig, ax = plt.subplots(figsize=(8, 6)) sns.lineplot( data=df, - x='N', - y='final_error', - hue='Method', - style='Method', + x="N", + y="final_error", + hue="Method", + style="Method", markers=True, dashes=True, ax=ax, @@ -68,14 +75,14 @@ # Add O(N^-2) reference line N_ref = [16, 64] -ax.plot(N_ref, [0.02, 0.02 * (16/64)**2], 'k:', alpha=0.5, label=r'$O(N^{-2})$') +ax.plot(N_ref, [0.02, 0.02 * (16 / 64) ** 2], "k:", alpha=0.5, label=r"$O(N^{-2})$") -ax.set_xscale('log') -ax.set_yscale('log') +ax.set_xscale("log") +ax.set_yscale("log") ax.grid(True, alpha=0.3) -ax.set_xlabel('Grid Size N') -ax.set_ylabel('L2 Error') -ax.set_title('Spatial Convergence: Solver Validation') +ax.set_xlabel("Grid Size N") +ax.set_ylabel("L2 Error") +ax.set_title("Spatial Convergence: Solver Validation") ax.legend() fig.tight_layout() @@ -95,12 +102,12 @@ x = np.linspace(0, 2, N) y = np.linspace(0, 2, N) z = np.linspace(0, 2, N) -X, Y, Z = np.meshgrid(x, y, z, indexing='ij') +X, Y, Z = np.meshgrid(x, y, z, indexing="ij") u_analytical = np.sin(np.pi * X) * np.sin(np.pi * Y) * np.sin(np.pi * Z) # Create structured grid grid = pv.StructuredGrid(X, Y, Z) -grid['solution'] = u_analytical.flatten(order='F') +grid["solution"] = u_analytical.flatten(order="F") # Create orthogonal slices at domain center @@ -112,44 +119,44 @@ # Add orthogonal slices plotter.add_mesh( slices, - scalars='solution', - cmap='coolwarm', + scalars="solution", + cmap="coolwarm", show_edges=True, - edge_color='black', + edge_color="black", line_width=0.5, show_scalar_bar=True, scalar_bar_args={ - 'title': 'u(x,y,z)', - 'position_x': 0.85, - 'position_y': 0.05, - 'title_font_size': 20, - 'label_font_size': 16, - 'fmt': '%.2f', - 'n_labels': 7 - } + "title": "u(x,y,z)", + "position_x": 0.85, + "position_y": 0.05, + "title_font_size": 20, + "label_font_size": 16, + "fmt": "%.2f", + "n_labels": 7, + }, ) # Add coordinate axes plotter.add_axes( interactive=False, line_width=5, - x_color='red', - y_color='green', - z_color='blue', - xlabel='X', - ylabel='Y', - zlabel='Z' + x_color="red", + y_color="green", + z_color="blue", + xlabel="X", + ylabel="Y", + zlabel="Z", ) # Add bounds with labels plotter.show_bounds( - grid='back', - location='outer', - xtitle='X', - ytitle='Y', - ztitle='Z', + grid="back", + location="outer", + xtitle="X", + ytitle="Y", + ztitle="Z", font_size=12, - all_edges=True + all_edges=True, ) # Show the plot (Sphinx-Gallery scraper will capture it) @@ -158,4 +165,3 @@ plotter.screenshot(output_file, transparent_background=True) print(f"Saved: {output_file}") plotter.show() - diff --git a/Experiments/05-scaling/compute_scaling.py b/Experiments/05-scaling/compute_scaling.py new file mode 100644 index 0000000..c92a7c6 --- /dev/null +++ b/Experiments/05-scaling/compute_scaling.py @@ -0,0 +1,168 @@ +""" +Poisson Scaling Experiment +========================== + +MPI Jacobi solver for 3D Poisson equation with MLflow logging. + +Usage: + mpiexec -n 4 uv run python compute_scaling.py --N 64 + mpiexec -n 8 uv run python compute_scaling.py --N 128 --tol 1e-8 --numba +""" + +import argparse +import os +import sys + +from mpi4py import MPI + +from Poisson import ( + JacobiPoisson, + DomainDecomposition, + NumpyHaloExchange, + CustomHaloExchange, + get_project_root, +) + +# Parse command line arguments +parser = argparse.ArgumentParser( + description="MPI Jacobi solver for 3D Poisson equation" +) +parser.add_argument("--N", type=int, default=64, help="Grid size N³ (default: 64)") +parser.add_argument( + "--tol", type=float, default=1e-6, help="Convergence tolerance (default: 1e-6)" +) +parser.add_argument( + "--max-iter", type=int, default=50000, help="Max iterations (default: 50000)" +) +parser.add_argument( + "--omega", type=float, default=0.8, help="Relaxation parameter (default: 0.8)" +) +parser.add_argument( + "--strategy", + choices=["sliced", "cubic"], + default="sliced", + help="Decomposition strategy", +) +parser.add_argument( + "--communicator", + choices=["numpy", "custom"], + default="numpy", + help="Halo exchange communicator", +) +parser.add_argument("--numba", action="store_true", help="Use Numba kernel") +parser.add_argument("--job-name", type=str, default=None, help="LSF Job Name for log retrieval") +parser.add_argument("--log-dir", type=str, default="logs", help="Directory containing LSF logs") +parser.add_argument("--experiment-name", type=str, default=None, help="MLflow experiment name") +args = parser.parse_args() + +N = args.N +comm = MPI.COMM_WORLD +n_ranks = comm.Get_size() +rank = comm.Get_rank() + +# Setup directories +project_root = get_project_root() +data_dir = project_root / "data" / "scaling" +data_dir.mkdir(parents=True, exist_ok=True) + +# Create solver +decomp = DomainDecomposition(N=N, size=n_ranks, strategy=args.strategy) +halo = CustomHaloExchange() if args.communicator == "custom" else NumpyHaloExchange() +solver = JacobiPoisson( + N=N, + omega=args.omega, + tolerance=args.tol, + max_iter=args.max_iter, + use_numba=args.numba, + decomposition=decomp, + communicator=halo, +) + +if rank == 0: + print( + f"Solver configured: N={N}³, ranks={n_ranks}, strategy={args.strategy}, comm={args.communicator}" + ) + print(f" Kernel: {'Numba' if args.numba else 'NumPy'}, omega={args.omega}") + +# MLflow setup with nested runs (auto-detect HPC via LSF env vars) +if args.experiment_name: + experiment_name = args.experiment_name +else: + is_hpc = "LSB_JOBID" in os.environ + experiment_name = "HPC-Poisson-Scaling" if is_hpc else "Poisson-Scaling" + +parent_run = f"N{N}" +run_name = f"N{N}_p{n_ranks}_{args.strategy}" +solver.mlflow_start(experiment_name, run_name, parent_run_name=parent_run) + +# Save Run ID for external log uploader +if rank == 0 and args.job_name: + try: + run_id = mlflow.active_run().info.run_id + log_path = project_root / args.log_dir + log_path.mkdir(parents=True, exist_ok=True) + run_id_file = log_path / f"{args.job_name}.runid" + with open(run_id_file, "w") as f: + f.write(run_id) + except Exception as e: + print(f"Warning: Could not save run ID to file: {e}") + +# Warmup Numba if needed +if args.numba: + solver.warmup() + +# Solve with timing +t0 = MPI.Wtime() +solver.solve() +wall_time = MPI.Wtime() - t0 + +# Store timing metrics +if rank == 0: + solver.results.wall_time = wall_time + solver.results.total_compute_time = sum(solver.timeseries.compute_times) + solver.results.total_halo_time = sum(solver.timeseries.halo_exchange_times) + solver.results.total_mpi_comm_time = sum(solver.timeseries.mpi_comm_times) + +# Compute L2 error +solver.compute_l2_error() + +# Save solution +output_file = data_dir / f"poisson_N{N}_p{n_ranks}.h5" +solver.save_hdf5(output_file) +solver.mlflow_log_artifact(str(output_file)) + +if rank == 0: + print(f"\nResults saved to: {output_file}") + + # Flush streams to ensure logs on disk are up to date for the external uploader + sys.stdout.flush() + sys.stderr.flush() + +# End MLflow run +solver.mlflow_end() + +# Summary +if rank == 0: + print("\nSolution Status:") + print(f" Converged: {solver.results.converged}") + print(f" Iterations: {solver.results.iterations}") + print(f" L2 error: {solver.results.final_error:.6e}") + print(f" Wall time: {wall_time:.2f} seconds") + + # Timing breakdown + total = ( + solver.results.total_compute_time + + solver.results.total_halo_time + + solver.results.total_mpi_comm_time + ) + if total > 0: + print("\nTiming breakdown:") + print( + f" Compute: {solver.results.total_compute_time:.3f}s ({100 * solver.results.total_compute_time / total:.1f}%)" + ) + print( + f" Halo exchange:{solver.results.total_halo_time:.3f}s ({100 * solver.results.total_halo_time / total:.1f}%)" + ) + print( + f" MPI allreduce:{solver.results.total_mpi_comm_time:.3f}s ({100 * solver.results.total_mpi_comm_time / total:.1f}%)" + ) diff --git a/Experiments/05-scaling/submit_sweep.sh b/Experiments/05-scaling/submit_sweep.sh deleted file mode 100644 index aeb1b9c..0000000 --- a/Experiments/05-scaling/submit_sweep.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -#BSUB -J submit_sequential -#BSUB -o Experiments/05-scaling/logs/sweep_%J.out -#BSUB -e Experiments/05-scaling/logs/sweep_%J.err -#BSUB -n 8 -#BSUB -W 00:10 -#BSUB -q hpcintro -#BSUB -R "span[hosts=1]" -#BSUB -R "rusage[mem=1GB]" - -module load python3/3.11.1 -module load mpi/5.0.8-gcc-13.4.0-binutils-2.44 - -uv sync -uv run python Experiments/05-scaling/run_sweep.py diff --git a/Experiments/05-scaling/sweep.py b/Experiments/05-scaling/sweep.py deleted file mode 100644 index 3920b64..0000000 --- a/Experiments/05-scaling/sweep.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Scaling Sweep: Strong and Weak Scaling Experiments -=================================================== - -Placeholder for future implementation. -""" - - -def strong_scaling(N, rank_counts, strategy, **kwargs): - """Strong scaling experiment (not yet implemented).""" - raise NotImplementedError("Strong scaling experiment not yet implemented") - - -def weak_scaling(N_per_rank, rank_counts, strategy, **kwargs): - """Weak scaling experiment (not yet implemented).""" - raise NotImplementedError("Weak scaling experiment not yet implemented") - - -if __name__ == "__main__": - print("Scaling experiments not yet implemented.") - print("See CODE_REVIEW.md Phase 1.3 for status.") diff --git a/Experiments/05-scaling/template_config.yaml b/Experiments/05-scaling/template_config.yaml new file mode 100644 index 0000000..ad277ad --- /dev/null +++ b/Experiments/05-scaling/template_config.yaml @@ -0,0 +1,72 @@ +# ============================================================================ +# LSF Scaling Experiment Configuration Template +# ============================================================================ + +type: strong # Options: strong, weak +script: Experiments/05-scaling/compute_scaling.py + +# ============================================================================ +# Global Parameters (Apply to all groups) +# ============================================================================ +# Arguments passed to the python script +parameters: + tol: 1e-6 + max-iter: 50000 + numba: true # Always use numba + +# ============================================================================ +# Global LSF Settings (Apply to all groups) +# ============================================================================ +lsf: + queue: hpc # -q: Queue name + walltime: "00:10" # -W: Walltime (HH:MM) + + # Default resources + resources: + - "rusage[mem=4GB]" + + # Additional options + exclusive: false # -x: Exclusive node access + +# ============================================================================ +# Experiment Groups +# ============================================================================ +groups: + # -------------------------------------------------------------------------- + # Group 1: Single Node (Shared Memory) + # -------------------------------------------------------------------------- + # Runs on a single host. Useful for small scale testing and debugging. + - name: single_node + lsf: + resources: + - "rusage[mem=4GB]" + - "span[hosts=1]" # FORCE execution on a single node + + # Sweep parameters generate one job per combination + sweep: + N: [64, 128] + ranks: [1, 2, 4, 8, 16, 32] + communicator: [numpy, custom] + strategy: [sliced, cubic] + + # -------------------------------------------------------------------------- + # Group 2: Multi Node (Distributed Memory) + # -------------------------------------------------------------------------- + # Runs across multiple nodes. + # 'ptile' defines how many MPI ranks to place per node. + - name: multi_node + lsf: + walltime: "00:10" + resources: + - "rusage[mem=8GB]" + - "span[ptile=24]" # Place 24 ranks per node (e.g. full node usage) + + # MPI options for process binding and mapping + # Example: 2 processes per CPU socket (package) + mpi_options: "--map-by ppr:2:package --bind-to core --report-bindings" + + sweep: + N: [256] + ranks: [48, 96] # Must be multiples of ptile (24) for efficient packing + communicator: [custom, numpy] + strategy: [cubic, sliced] diff --git a/data/01-kernels/kernel_benchmark.parquet b/data/01-kernels/kernel_benchmark.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b69abfbf92ec739b4bccf46be5035243d039b4b3 GIT binary patch literal 102969 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 literal 0 HcmV?d00001 diff --git a/data/01-kernels/kernel_convergence.parquet b/data/01-kernels/kernel_convergence.parquet new file mode 100644 index 0000000000000000000000000000000000000000..aadb61e9a1fd163255c6b714bfc5162d1785d71a GIT binary patch literal 98796 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.E7kK)(x@cZ*D)a(UA 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

b~-I4ZLap5 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

)I>* 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 z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly cK!5-N0t5&UAV7cs0RjXF5FkK+z%?oG9|?#Vn*aa+ literal 0 HcmV?d00001 diff --git a/data/validation/N16_np8_cubic_numpy.h5 b/data/validation/N16_np8_cubic_numpy.h5 new file mode 100644 index 0000000000000000000000000000000000000000..7089d6ec1da58769fa8807ffcdf00bb594b2e15f GIT binary patch literal 1066608 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 z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N Y0t5&UAV7cs0RjXF5FkK+zzr$zAA1%Vb^rhX literal 0 HcmV?d00001 diff --git a/data/validation/N16_np8_sliced_custom.h5 b/data/validation/N16_np8_sliced_custom.h5 new file mode 100644 index 0000000000000000000000000000000000000000..4c74556a828c9c6cfbb967c5bb7dff4a0848b180 GIT binary patch literal 1066608 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 z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly lK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWS2=@E=DQ8pi+t literal 0 HcmV?d00001 diff --git a/data/validation/N32_np8_cubic_numpy.h5 b/data/validation/N32_np8_cubic_numpy.h5 new file mode 100644 index 0000000000000000000000000000000000000000..b6853fdadb213c8be2ba17961eee8c88c83603c4 GIT binary patch literal 1066608 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 z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs e0RjXF5FkK+009C72oNAZfB*pk1PI)a0{;OrF&N_j literal 0 HcmV?d00001 diff --git a/data/validation/N32_np8_sliced_numpy.h5 b/data/validation/N32_np8_sliced_numpy.h5 new file mode 100644 index 0000000000000000000000000000000000000000..bd9712858a35febf0e7af56396e77315b1ee1724 GIT binary patch literal 1066608 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 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk f1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{rWE)OiSQeR literal 0 HcmV?d00001 diff --git a/data/validation/N48_np8_sliced_numpy.h5 b/data/validation/N48_np8_sliced_numpy.h5 new file mode 100644 index 0000000000000000000000000000000000000000..72ef8505b46474d86ca297fd7a99719535d34e10 GIT binary patch literal 1066608 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 zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF d5FkK+009C72oNAZfB*pk1PBlyK;VWH_z#Ny8Oi_v literal 0 HcmV?d00001 diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst index 0ca65c9..e32bf9b 100644 --- a/docs/source/api_reference.rst +++ b/docs/source/api_reference.rst @@ -108,8 +108,20 @@ Simplified Datastructures - :class:`LocalFields` - Local domain arrays with halo zones (u1, u2, f) - :class:`LocalSeries` - Per-iteration timing arrays (compute_times, halo_exchange_times, residual_history) -Parallel I/O Strategy -^^^^^^^^^^^^^^^^^^^^^^ +Experiment Tracking & I/O +------------------------- + +The framework integrates with **MLflow** for experiment tracking and **HDF5** for data storage. + +**MLflow Tracking:** + +The solver automatically logs: +- **Parameters**: Grid size, tolerance, decomposition strategy, etc. +- **Metrics**: Convergence status, iterations, final error, wall time. +- **Time Series**: Complete history of residuals and performance metrics (compute time, comm time) per step. +- **Artifacts**: The HDF5 result file is uploaded to the MLflow run. + +**Parallel HDF5 Strategy:** **HDF5 collective writes** eliminate the gather-to-rank-0 bottleneck: @@ -188,11 +200,25 @@ Communication Strategies NumpyHaloExchange CustomHaloExchange +Experiment Utilities +==================== + +.. currentmodule:: utils + +.. autosummary:: + :toctree: generated + + mlflow_io + +The :mod:`utils.mlflow_io` module provides tools for fetching and managing experiment data from MLflow. + .. _data-structures: Data Structures =============== +.. currentmodule:: Poisson + Global Configuration & Metrics ------------------------------ diff --git a/docs/source/conf.py b/docs/source/conf.py index 8c65635..d2e33be 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,9 @@ # -- Project information ----------------------------------------------------- project = "MPI 3D Poisson Solver" -copyright = "2025, Alexander Elbæk Nielsen, Junriu Li, Philip Korsager Nickel, DTU Compute" +copyright = ( + "2025, Alexander Elbæk Nielsen, Junriu Li, Philip Korsager Nickel, DTU Compute" +) author = "Alexander Elbæk Nielsen, Junriu Li, Philip Korsager Nickel" # -- General configuration --------------------------------------------------- @@ -76,7 +78,10 @@ "remove_config_comments": True, # Clean up notebook outputs "abort_on_example_error": False, # Continue if examples fail "plot_gallery": True, - "image_scrapers": ("matplotlib", "pyvista"), # Capture both matplotlib and pyvista plots + "image_scrapers": ( + "matplotlib", + "pyvista", + ), # Capture both matplotlib and pyvista plots "capture_repr": ("_repr_html_", "__repr__"), # Capture output representations "matplotlib_animations": True, # Support matplotlib animations # Remove Jupyter cell markers (# %%) from rendered output diff --git a/docs/source/sg_execution_times.rst b/docs/source/sg_execution_times.rst index 0d42d20..273760b 100644 --- a/docs/source/sg_execution_times.rst +++ b/docs/source/sg_execution_times.rst @@ -6,7 +6,7 @@ Computation times ================= -**00:05.720** total execution time for 8 files **from all galleries**: +**00:01.892** total execution time for 8 files **from all galleries**: .. container:: @@ -32,27 +32,27 @@ Computation times * - Example - Time - Mem (MB) - * - :ref:`sphx_glr_example_gallery_01-kernels_plot_kernels.py` (``../../Experiments/01-kernels/plot_kernels.py``) - - 00:02.639 - - 0.0 - * - :ref:`sphx_glr_example_gallery_04-validation_plot_validation.py` (``../../Experiments/04-validation/plot_validation.py``) - - 00:01.353 - - 0.0 - * - :ref:`sphx_glr_example_gallery_02-decomposition_plot_decompositions.py` (``../../Experiments/02-decomposition/plot_decompositions.py``) - - 00:01.044 - - 0.0 * - :ref:`sphx_glr_example_gallery_03-communication_plot_communication.py` (``../../Experiments/03-communication/plot_communication.py``) - - 00:00.685 + - 00:01.892 - 0.0 * - :ref:`sphx_glr_example_gallery_01-kernels_compute_all.py` (``../../Experiments/01-kernels/compute_all.py``) - 00:00.000 - 0.0 + * - :ref:`sphx_glr_example_gallery_01-kernels_plot_kernels.py` (``../../Experiments/01-kernels/plot_kernels.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_example_gallery_02-decomposition_plot_decompositions.py` (``../../Experiments/02-decomposition/plot_decompositions.py``) + - 00:00.000 + - 0.0 * - :ref:`sphx_glr_example_gallery_03-communication_compute_communication.py` (``../../Experiments/03-communication/compute_communication.py``) - 00:00.000 - 0.0 * - :ref:`sphx_glr_example_gallery_04-validation_compute_validation.py` (``../../Experiments/04-validation/compute_validation.py``) - 00:00.000 - 0.0 - * - :ref:`sphx_glr_example_gallery_05-scaling_sweep.py` (``../../Experiments/05-scaling/sweep.py``) + * - :ref:`sphx_glr_example_gallery_04-validation_plot_validation.py` (``../../Experiments/04-validation/plot_validation.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_example_gallery_05-scaling_compute_scaling.py` (``../../Experiments/05-scaling/compute_scaling.py``) - 00:00.000 - 0.0 diff --git a/main.py b/main.py index f11a4c1..cd23562 100644 --- a/main.py +++ b/main.py @@ -146,7 +146,11 @@ def run_compute_scripts(): print("✓") success_count += 1 else: - error_msg = result.stderr[:200] if result.stderr else f"exit {result.returncode}" + error_msg = ( + result.stderr[:200] + if result.stderr + else f"exit {result.returncode}" + ) print(f"✗ ({error_msg})") fail_count += 1 @@ -176,7 +180,7 @@ def copy_plots(): if dest_dir.exists(): shutil.rmtree(dest_dir) shutil.copytree(source_dir, dest_dir) - print(f" ✓ Copied figures/ to docs/reports/TexReport/figures/") + print(" ✓ Copied figures/ to docs/reports/TexReport/figures/") except Exception as e: print(f" ✗ Failed to copy: {e}") @@ -191,6 +195,10 @@ def build_docs(): print("\nBuilding Sphinx documentation...") + if not source_dir.exists(): + print(f" Error: Documentation source directory not found: {source_dir}") + return False + try: result = subprocess.run( [ @@ -229,6 +237,70 @@ def build_docs(): return False +def hpc_submit_pack(scaling_type: str, dry_run: bool): + """Generate and optionally submit an LSF job pack.""" + print(f"\nGenerating {scaling_type} scaling job pack...") + + pack_file_name = f"Experiments/05-scaling/{scaling_type}_scaling_jobs.pack" + pack_file_path = REPO_ROOT / pack_file_name + + # Generate the pack file + cmd = [ + "uv", + "run", + "python", + "-m", + "src.utils.generate_pack", + "--type", + scaling_type, + "--output", + str(pack_file_path), + "--config-dir", + str(REPO_ROOT / "Experiments" / "05-scaling"), + ] + # Use standard N values for strong scaling if not specified in generate_pack default + if scaling_type == "strong": + # Hardcoded defaults for project consistency + cmd.extend(["--N", "64", "128", "256"]) + + result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(REPO_ROOT)) + + if result.returncode != 0: + print(f" ✗ Failed to generate pack file: {result.stderr}") + return + print(f" ✓ {result.stdout.strip()}") + + if dry_run: + print(f"\n[DRY RUN] Content of {pack_file_name}:") + print("-" * 40) + print(pack_file_path.read_text()) + print("-" * 40) + print( + f" To submit manually: bsub -pack {pack_file_name}" + ) + return + + # Submit the pack file + print(f"\nSubmitting {pack_file_path} to LSF...") + + if not shutil.which("bsub"): + print(" ✗ 'bsub' command not found. Are you on the HPC login node?") + return + + submit_cmd = ["bsub", "-pack", str(pack_file_path)] + + result = subprocess.run( + submit_cmd, capture_output=True, text=True, cwd=str(REPO_ROOT) + ) + + if result.returncode != 0: + print(f" ✗ Failed to submit jobs: {result.stderr}") + else: + print(f" ✓ Jobs submitted successfully.") + if result.stdout: + print(f" {result.stdout.strip()}") + + def clean_all(): """Clean all generated files and caches.""" print("\nCleaning all generated files and caches...") @@ -248,9 +320,16 @@ def remove_item(path): # Directories to clean dirs = [ - "docs/build", "docs/source/example_gallery", "docs/source/generated", - "docs/source/gen_modules", "plots", "build", "dist", - ".pytest_cache", ".ruff_cache", ".mypy_cache", + "docs/build", + "docs/source/example_gallery", + "docs/source/generated", + "docs/source/gen_modules", + "plots", + "build", + "dist", + ".pytest_cache", + ".ruff_cache", + ".mypy_cache", ] for d in dirs: path = REPO_ROOT / d @@ -301,6 +380,43 @@ def remove_item(path): print() +def fetch_mlflow(): + """Fetch artifacts from MLflow for all converged runs.""" + print("\nFetching MLflow artifacts...") + + try: + # Import locally to avoid hard dependency if not fetching + # from utils.mlflow_io import download_artifacts_with_naming, setup_mlflow_auth + from utils.mlflow_io import setup_mlflow_auth + + setup_mlflow_auth() + + # Define download targets - modifying for LSM Project 2 context + # Assuming we might have experiments named like 'LSM-Project-2/Scaling' or similar + # For now, we'll use a placeholder or a generic search if available, + # but matching the ANA-P3 pattern: + + # output_dir = REPO_ROOT / "data" / "downloaded" + + # Example: Fetch from a "Scaling" experiment + # experiments = ["LSM-Scaling", "LSM-Kernels"] + # for exp in experiments: + # print(f"\n{exp}:") + # paths = download_artifacts_with_naming(exp, output_dir / exp) + # print(f" ✓ Downloaded {len(paths)} files to data/downloaded/{exp}/") + + print( + " (No experiments configured for auto-fetch yet. Edit main.py to specify experiments.)" + ) + print() + + except ImportError as e: + print(f" ✗ Missing dependency: {e}") + print(" Install with: uv sync") + except Exception as e: + print(f" ✗ Failed to fetch: {e}\n") + + def main(): """Main entry point.""" parser = argparse.ArgumentParser( @@ -313,15 +429,45 @@ def main(): python main.py --plot Run all plotting scripts python main.py --copy-plots Copy plots to plots/ directory python main.py --clean Clean all generated files - python main.py --compute --plot Run compute then plot scripts + python main.py --hpc strong --dry Generate strong scaling pack (dry run) """, ) - parser.add_argument("--docs", action="store_true", help="Build Sphinx HTML documentation") - parser.add_argument("--compute", action="store_true", help="Run all compute scripts (sequentially)") - parser.add_argument("--plot", action="store_true", help="Run all plotting scripts (in parallel)") - parser.add_argument("--copy-plots", action="store_true", help="Copy plots to plots/ directory") - parser.add_argument("--clean", action="store_true", help="Clean all generated files and caches") + # Action Group + actions = parser.add_argument_group("Actions") + actions.add_argument( + "--docs", action="store_true", help="Build Sphinx HTML documentation" + ) + actions.add_argument( + "--compute", action="store_true", help="Run all compute scripts (sequentially)" + ) + actions.add_argument( + "--plot", action="store_true", help="Run all plotting scripts (in parallel)" + ) + actions.add_argument( + "--copy-plots", action="store_true", help="Copy plots to plots/ directory" + ) + actions.add_argument( + "--clean", action="store_true", help="Clean all generated files and caches" + ) + actions.add_argument( + "--fetch", + action="store_true", + help="Fetch artifacts from MLflow for all converged runs", + ) + actions.add_argument( + "--hpc", + choices=["strong", "weak"], + help="Generate and submit LSF job pack for scaling", + ) + + # Options Group + options = parser.add_argument_group("Options") + options.add_argument( + "--dry", + action="store_true", + help="Print generated job pack without submitting (for --hpc)", + ) # Show help if no arguments provided if len(sys.argv) == 1: @@ -344,6 +490,15 @@ def main(): if args.copy_plots: copy_plots() + if args.fetch: + fetch_mlflow() + + # Handle HPC pack submission + if args.hpc: + hpc_submit_pack(args.hpc, args.dry) + + # Handle documentation commands + if args.docs: build_docs() diff --git a/pyproject.toml b/pyproject.toml index f390589..d09c7cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ dependencies = [ "pyvista>=0.46.4", "h5py>=3.15.1", "tables>=3.10.2", + "psutil>=7.1.3", + "pyyaml>=6.0.3", ] [tool.pytest.ini_options] @@ -55,4 +57,7 @@ package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] +[dependency-groups] +dev = [] + diff --git a/setup_mlflow.py b/setup_mlflow.py index 647ca56..89252c1 100644 --- a/setup_mlflow.py +++ b/setup_mlflow.py @@ -1,6 +1,6 @@ import mlflow -# Login to Databricks +# Login to Databricks mlflow.login(backend="databricks", interactive=True) -# https://dbc-6756e917-e5fc.cloud.databricks.com \ No newline at end of file +# https://dbc-6756e917-e5fc.cloud.databricks.com diff --git a/src/Poisson/__init__.py b/src/Poisson/__init__.py index 905ed59..144cdd0 100644 --- a/src/Poisson/__init__.py +++ b/src/Poisson/__init__.py @@ -13,11 +13,21 @@ KernelSeries, ) from .kernels import NumPyKernel, NumbaKernel -from .jacobi import JacobiPoisson -from .decomposition import DomainDecomposition, RankInfo, NoDecomposition -from .communicators import NumpyHaloExchange, CustomHaloExchange -from .problems import create_grid_3d, sinusoidal_exact_solution, sinusoidal_source_term, setup_sinusoidal_problem -from .runner import run_solver +from .solver import JacobiPoisson +from .mpi import ( + DomainDecomposition, + RankInfo, + NoDecomposition, + NumpyHaloExchange, + CustomHaloExchange, +) +from .problems import ( + create_grid_3d, + sinusoidal_exact_solution, + sinusoidal_source_term, + setup_sinusoidal_problem, +) +from .helpers import run_solver __all__ = [ # Data structures - Kernel diff --git a/src/Poisson/datastructures.py b/src/Poisson/datastructures.py index 8458977..ff4782a 100644 --- a/src/Poisson/datastructures.py +++ b/src/Poisson/datastructures.py @@ -12,9 +12,10 @@ # ============================================================================ -# Kernel +# Kernel # ============================================================================ + @dataclass class KernelParams: """Kernel configuration parameters. @@ -22,6 +23,7 @@ class KernelParams: Note: N is the LOCAL grid size (after domain decomposition + halo zones for MPI). For standalone usage, N is the full problem size. """ + N: int # Local grid size (including halo zones for MPI) omega: float tolerance: float = 1e-10 @@ -39,6 +41,7 @@ def __post_init__(self): @dataclass class KernelMetrics: """Final convergence metrics (updated during kernel execution).""" + converged: bool = False iterations: int = 0 final_residual: float | None = None @@ -52,6 +55,7 @@ class KernelSeries: The kernel automatically populates residuals and compute_times during step(). Physical errors can be optionally appended by the caller for validation. """ + residuals: list[float] = field(default_factory=list) compute_times: list[float] = field(default_factory=list) physical_errors: list[float] | None = None @@ -61,6 +65,7 @@ class KernelSeries: # Solver - Global (identical across ranks, or rank 0 only) # ============================================================================ + @dataclass class GlobalParams: """Global problem definition (all ranks have identical copy). @@ -68,6 +73,7 @@ class GlobalParams: Note: N is the GLOBAL grid size (before domain decomposition). The solver internally computes N_local for each rank after decomposition. """ + # Global problem parameters N: int = 0 # Global grid size (before decomposition) omega: float = 0.75 @@ -77,7 +83,7 @@ class GlobalParams: # MPI configuration mpi_size: int = 1 decomposition: str = "none" # "none", "sliced", "cubic" - communicator: str = "none" # "none", "numpy", "custom" + communicator: str = "none" # "none", "numpy", "custom" # Kernel backend selection use_numba: bool = False @@ -87,6 +93,7 @@ class GlobalParams: @dataclass class GlobalMetrics: """Final convergence metrics (computed/stored on rank 0 only).""" + iterations: int = 0 converged: bool = False final_error: float | None = None @@ -101,6 +108,7 @@ class GlobalMetrics: # Solver - Local (each rank has different values) # ============================================================================ + @dataclass class LocalParams: """Local rank-specific parameters (computed after decomposition). @@ -108,11 +116,12 @@ class LocalParams: Each rank has its own LocalParams with rank-specific values including the kernel configuration for that rank's local domain size. """ + N_local: int # Local grid size including halo zones # Domain coordinates in global grid local_start: tuple[int, int, int] # (i_start, j_start, k_start) - local_end: tuple[int, int, int] # (i_end, j_end, k_end) + local_end: tuple[int, int, int] # (i_end, j_end, k_end) # Kernel configuration kernel: KernelParams @@ -128,6 +137,7 @@ def __post_init__(self): @dataclass class LocalFields: """Local domain arrays with halo zones (each rank).""" + u1: np.ndarray = field(default_factory=lambda: np.zeros((0, 0, 0))) u2: np.ndarray = field(default_factory=lambda: np.zeros((0, 0, 0))) f: np.ndarray = field(default_factory=lambda: np.zeros((0, 0, 0))) @@ -140,9 +150,8 @@ class LocalSeries: Each rank accumulates its own timing data for each iteration. Rank 0 additionally stores residual history. """ + compute_times: list[float] = field(default_factory=list) mpi_comm_times: list[float] = field(default_factory=list) halo_exchange_times: list[float] = field(default_factory=list) residual_history: list[float] = field(default_factory=list) - - diff --git a/src/Poisson/helpers/__init__.py b/src/Poisson/helpers/__init__.py new file mode 100644 index 0000000..b2a1771 --- /dev/null +++ b/src/Poisson/helpers/__init__.py @@ -0,0 +1,5 @@ +"""Helper utilities for running solvers.""" + +from .runner import run_solver + +__all__ = ["run_solver"] diff --git a/src/Poisson/runner.py b/src/Poisson/helpers/runner.py similarity index 79% rename from src/Poisson/runner.py rename to src/Poisson/helpers/runner.py index d97d4fc..12caccd 100644 --- a/src/Poisson/runner.py +++ b/src/Poisson/helpers/runner.py @@ -30,12 +30,22 @@ def run_solver(N: int, n_ranks: int = 1, output: str = None, **kwargs) -> dict: # Use temp file if no output path specified use_temp = output is None if use_temp: - tmp = tempfile.NamedTemporaryFile(suffix='.h5', delete=False) + tmp = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) output = tmp.name tmp.close() config = {"N": N, "output": output, **kwargs} - cmd = ["mpiexec", "-n", str(n_ranks), "uv", "run", "python", "-m", "Poisson.runner_helper", json.dumps(config)] + cmd = [ + "mpiexec", + "-n", + str(n_ranks), + "uv", + "run", + "python", + "-m", + "Poisson.helpers.runner_helper", + json.dumps(config), + ] proc = subprocess.run(cmd, capture_output=True, text=True) @@ -46,7 +56,7 @@ def run_solver(N: int, n_ranks: int = 1, output: str = None, **kwargs) -> dict: if not Path(output).exists(): return {"error": "No output file created", "stderr": proc.stderr} - result = pd.read_hdf(output, key='results').iloc[0].to_dict() + result = pd.read_hdf(output, key="results").iloc[0].to_dict() # Clean up temp file if we created one if use_temp: diff --git a/src/Poisson/runner_helper.py b/src/Poisson/helpers/runner_helper.py similarity index 82% rename from src/Poisson/runner_helper.py rename to src/Poisson/helpers/runner_helper.py index f1c1efd..38967c7 100644 --- a/src/Poisson/runner_helper.py +++ b/src/Poisson/helpers/runner_helper.py @@ -1,9 +1,14 @@ -"""MPI worker - invoked via: mpiexec -n X uv run python -m Poisson.runner_helper '{config}'""" +"""MPI worker - invoked via: mpiexec -n X uv run python -m Poisson.helpers.runner_helper '{config}'""" import sys import json from mpi4py import MPI -from Poisson import JacobiPoisson, DomainDecomposition, NumpyHaloExchange, CustomHaloExchange +from Poisson import ( + JacobiPoisson, + DomainDecomposition, + NumpyHaloExchange, + CustomHaloExchange, +) config = json.loads(sys.argv[1]) comm = MPI.COMM_WORLD @@ -14,7 +19,11 @@ strategy=config.get("strategy", "sliced"), axis=config.get("axis", "z"), ) -halo = CustomHaloExchange() if config.get("communicator") == "custom" else NumpyHaloExchange() +halo = ( + CustomHaloExchange() + if config.get("communicator") == "custom" + else NumpyHaloExchange() +) solver = JacobiPoisson( N=config["N"], diff --git a/src/Poisson/kernels.py b/src/Poisson/kernels.py index 1bcf096..ff13027 100644 --- a/src/Poisson/kernels.py +++ b/src/Poisson/kernels.py @@ -9,7 +9,9 @@ @njit(parallel=True) -def _jacobi_step_numba(uold: np.ndarray, u: np.ndarray, f: np.ndarray, h: float, omega: float) -> float: +def _jacobi_step_numba( + uold: np.ndarray, u: np.ndarray, f: np.ndarray, h: float, omega: float +) -> float: """Numba JIT implementation of Jacobi iteration step.""" c = 1.0 / 6.0 h2 = h * h @@ -18,7 +20,9 @@ def _jacobi_step_numba(uold: np.ndarray, u: np.ndarray, f: np.ndarray, h: float, for j in range(1, u.shape[1] - 1): for k in range(1, u.shape[2] - 1): u[i, j, k] = ( - omega * c * ( + omega + * c + * ( uold[i - 1, j, k] + uold[i + 1, j, k] + uold[i, j - 1, k] @@ -74,7 +78,9 @@ def step(self, uold: np.ndarray, u: np.ndarray, f: np.ndarray) -> float: h2 = self.parameters.h * self.parameters.h u[1:-1, 1:-1, 1:-1] = ( - self.parameters.omega * c * ( + self.parameters.omega + * c + * ( uold[0:-2, 1:-1, 1:-1] + uold[2:, 1:-1, 1:-1] + uold[1:-1, 0:-2, 1:-1] @@ -89,7 +95,7 @@ def step(self, uold: np.ndarray, u: np.ndarray, f: np.ndarray) -> float: # Compute sum of squared differences over interior points only # Return unnormalized sum - normalization done globally in solver diff = u[1:-1, 1:-1, 1:-1] - uold[1:-1, 1:-1, 1:-1] - diff_sum = np.sum(diff ** 2) + diff_sum = np.sum(diff**2) self._track(diff_sum, time.perf_counter() - start) return diff_sum @@ -105,7 +111,9 @@ def __init__(self, **kwargs): def step(self, uold: np.ndarray, u: np.ndarray, f: np.ndarray) -> float: """Perform one Jacobi iteration step.""" start = time.perf_counter() - residual = _jacobi_step_numba(uold, u, f, self.parameters.h, self.parameters.omega) + residual = _jacobi_step_numba( + uold, u, f, self.parameters.h, self.parameters.omega + ) self._track(residual, time.perf_counter() - start) return residual diff --git a/src/Poisson/mpi/__init__.py b/src/Poisson/mpi/__init__.py new file mode 100644 index 0000000..599785b --- /dev/null +++ b/src/Poisson/mpi/__init__.py @@ -0,0 +1,12 @@ +"""MPI domain decomposition and communication.""" + +from .decomposition import DomainDecomposition, RankInfo, NoDecomposition +from .communicators import NumpyHaloExchange, CustomHaloExchange + +__all__ = [ + "DomainDecomposition", + "RankInfo", + "NoDecomposition", + "NumpyHaloExchange", + "CustomHaloExchange", +] diff --git a/src/Poisson/communicators.py b/src/Poisson/mpi/communicators.py similarity index 70% rename from src/Poisson/communicators.py rename to src/Poisson/mpi/communicators.py index e5279a3..b0538ea 100644 --- a/src/Poisson/communicators.py +++ b/src/Poisson/mpi/communicators.py @@ -1,9 +1,10 @@ """Halo exchange communicators for MPI domain decomposition.""" + import numpy as np from mpi4py import MPI # Axis config: (index, name) - used to build neighbor keys like 'z_lower' -_AXES = [(0, 'z'), (1, 'y'), (2, 'x')] +_AXES = [(0, "z"), (1, "y"), (2, "x")] def _face_slice(axis, idx, has_halo): @@ -20,18 +21,19 @@ class _BaseHaloExchange: def exchange_halos(self, u, decomposition, rank, comm): """Exchange halo zones with neighbors.""" neighbors = decomposition.get_neighbors(rank) - has_halo = {ax: f'{name}_lower' in neighbors for ax, name in _AXES} + has_halo = {ax: f"{name}_lower" in neighbors for ax, name in _AXES} for axis, name in _AXES: if not has_halo[axis]: continue - lo = neighbors.get(f'{name}_lower') - hi = neighbors.get(f'{name}_upper') + lo = neighbors.get(f"{name}_lower") + hi = neighbors.get(f"{name}_upper") self._exchange_axis(u, axis, lo, hi, has_halo, comm, tag=axis * 100) class NumpyHaloExchange(_BaseHaloExchange): """Halo exchange using NumPy array copies.""" + name = "NumPy" def _exchange_axis(self, u, axis, lo_rank, hi_rank, has_halo, comm, tag): @@ -42,12 +44,15 @@ def _exchange_axis(self, u, axis, lo_rank, hi_rank, has_halo, comm, tag): send = np.ascontiguousarray(u[_face_slice(axis, send_i, has_halo)]) recv = np.empty_like(send) comm.Sendrecv(send, dest, tag, recv, src, tag) - u[_face_slice(axis, recv_i, has_halo)] = recv if src != MPI.PROC_NULL else 0.0 + u[_face_slice(axis, recv_i, has_halo)] = ( + recv if src != MPI.PROC_NULL else 0.0 + ) tag += 1 class CustomHaloExchange(_BaseHaloExchange): """Halo exchange using MPI derived datatypes (zero-copy).""" + name = "MPI Datatype" def __init__(self): @@ -68,26 +73,53 @@ def _exchange_axis(self, u, axis, lo_rank, hi_rank, has_halo, comm, tag): # Compute offsets into flattened array start = [1 if has_halo[ax] else 0 for ax in range(3)] - flat = lambda z, y, x: z * ny * nx + y * nx + x + + def flat(z, y, x): + return z * ny * nx + y * nx + x if axis == 0: - send_hi, recv_lo = flat(nz-2, start[1], start[2]), flat(0, start[1], start[2]) - send_lo, recv_hi = flat(1, start[1], start[2]), flat(nz-1, start[1], start[2]) + send_hi, recv_lo = ( + flat(nz - 2, start[1], start[2]), + flat(0, start[1], start[2]), + ) + send_lo, recv_hi = ( + flat(1, start[1], start[2]), + flat(nz - 1, start[1], start[2]), + ) elif axis == 1: - send_hi, recv_lo = flat(start[0], ny-2, start[2]), flat(start[0], 0, start[2]) - send_lo, recv_hi = flat(start[0], 1, start[2]), flat(start[0], ny-1, start[2]) + send_hi, recv_lo = ( + flat(start[0], ny - 2, start[2]), + flat(start[0], 0, start[2]), + ) + send_lo, recv_hi = ( + flat(start[0], 1, start[2]), + flat(start[0], ny - 1, start[2]), + ) else: - send_hi, recv_lo = flat(start[0], start[1], nx-2), flat(start[0], start[1], 0) - send_lo, recv_hi = flat(start[0], start[1], 1), flat(start[0], start[1], nx-1) + send_hi, recv_lo = ( + flat(start[0], start[1], nx - 2), + flat(start[0], start[1], 0), + ) + send_lo, recv_hi = ( + flat(start[0], start[1], 1), + flat(start[0], start[1], nx - 1), + ) # Exchange - comm.Sendrecv([u_flat[send_hi:], 1, dtype], hi, tag, - [u_flat[recv_lo:], 1, dtype], lo, tag) + comm.Sendrecv( + [u_flat[send_hi:], 1, dtype], hi, tag, [u_flat[recv_lo:], 1, dtype], lo, tag + ) if lo_rank is None: u[_face_slice(axis, 0, has_halo)] = 0.0 - comm.Sendrecv([u_flat[send_lo:], 1, dtype], lo, tag + 1, - [u_flat[recv_hi:], 1, dtype], hi, tag + 1) + comm.Sendrecv( + [u_flat[send_lo:], 1, dtype], + lo, + tag + 1, + [u_flat[recv_hi:], 1, dtype], + hi, + tag + 1, + ) if hi_rank is None: u[_face_slice(axis, -1, has_halo)] = 0.0 diff --git a/src/Poisson/decomposition.py b/src/Poisson/mpi/decomposition.py similarity index 86% rename from src/Poisson/decomposition.py rename to src/Poisson/mpi/decomposition.py index daa89f0..6d96799 100644 --- a/src/Poisson/decomposition.py +++ b/src/Poisson/mpi/decomposition.py @@ -11,12 +11,13 @@ @dataclass class RankInfo: """Decomposition information for a single rank.""" + rank: int # Local domain (interior points only) local_shape: tuple[int, int, int] # (nx, ny, nz) interior global_start: tuple[int, int, int] # (i, j, k) in global grid - global_end: tuple[int, int, int] # (i, j, k) exclusive + global_end: tuple[int, int, int] # (i, j, k) exclusive # Halo zones halo_shape: tuple[int, int, int] # Shape including halos @@ -53,21 +54,21 @@ class DomainDecomposition: ... print(f"Rank {rank}: {info.local_shape}") """ - def __init__(self, N, size, strategy='sliced', axis='z'): + def __init__(self, N, size, strategy="sliced", axis="z"): self.N = N self.size = size self.strategy = strategy # Normalize axis to integer (0=z, 1=y, 2=x in ZYX ordering) - axis_map = {'z': 0, 'y': 1, 'x': 2, 0: 0, 1: 1, 2: 2} + axis_map = {"z": 0, "y": 1, "x": 2, 0: 0, 1: 1, 2: 2} if axis not in axis_map: raise ValueError(f"Invalid axis: {axis}. Use 'x', 'y', 'z' or 0, 1, 2") self.axis = axis_map[axis] # Decompose domain - if strategy == 'sliced': + if strategy == "sliced": self._decompose_sliced() - elif strategy == 'cubic': + elif strategy == "cubic": self._decompose_cubic() else: raise ValueError(f"Unknown strategy: {strategy}") @@ -109,7 +110,7 @@ def _decompose_sliced(self): """Decompose domain with 1D slicing along configurable axis.""" interior_N = self.N - 2 axis = self.axis # 0=z, 1=y, 2=x - axis_names = ['z', 'y', 'x'] + axis_names = ["z", "y", "x"] axis_name = axis_names[axis] self._rank_info = [] @@ -145,8 +146,8 @@ def _decompose_sliced(self): # Neighbors along decomposition axis only neighbors = {} - neighbors[f'{axis_name}_lower'] = rank - 1 if rank > 0 else None - neighbors[f'{axis_name}_upper'] = rank + 1 if rank < self.size - 1 else None + neighbors[f"{axis_name}_lower"] = rank - 1 if rank > 0 else None + neighbors[f"{axis_name}_upper"] = rank + 1 if rank < self.size - 1 else None n_neighbors = sum(1 for n in neighbors.values() if n is not None) @@ -162,7 +163,7 @@ def _decompose_sliced(self): halo_shape=halo_shape, neighbors=neighbors, n_neighbors=n_neighbors, - halo_cells_total=halo_cells_total + halo_cells_total=halo_cells_total, ) self._rank_info.append(info) @@ -193,9 +194,9 @@ def split_sizes(n, parts): # Store for later use self._split_info = { - 'nx': (nx_counts, nx_starts), - 'ny': (ny_counts, ny_starts), - 'nz': (nz_counts, nz_starts), + "nx": (nx_counts, nx_starts), + "ny": (ny_counts, ny_starts), + "nz": (nz_counts, nz_starts), } self._rank_info = [] @@ -227,22 +228,22 @@ def split_sizes(n, parts): # Neighbors neighbors = {} - neighbors['x_lower'] = self._cart_neighbor(ix-1, iy, iz, px, py, pz) - neighbors['x_upper'] = self._cart_neighbor(ix+1, iy, iz, px, py, pz) - neighbors['y_lower'] = self._cart_neighbor(ix, iy-1, iz, px, py, pz) - neighbors['y_upper'] = self._cart_neighbor(ix, iy+1, iz, px, py, pz) - neighbors['z_lower'] = self._cart_neighbor(ix, iy, iz-1, px, py, pz) - neighbors['z_upper'] = self._cart_neighbor(ix, iy, iz+1, px, py, pz) + neighbors["x_lower"] = self._cart_neighbor(ix - 1, iy, iz, px, py, pz) + neighbors["x_upper"] = self._cart_neighbor(ix + 1, iy, iz, px, py, pz) + neighbors["y_lower"] = self._cart_neighbor(ix, iy - 1, iz, px, py, pz) + neighbors["y_upper"] = self._cart_neighbor(ix, iy + 1, iz, px, py, pz) + neighbors["z_lower"] = self._cart_neighbor(ix, iy, iz - 1, px, py, pz) + neighbors["z_upper"] = self._cart_neighbor(ix, iy, iz + 1, px, py, pz) n_neighbors = sum(1 for n in neighbors.values() if n is not None) nz, ny, nx = local_shape halo_cells = 0 - if neighbors['x_lower'] is not None or neighbors['x_upper'] is not None: + if neighbors["x_lower"] is not None or neighbors["x_upper"] is not None: halo_cells += 2 * nz * ny - if neighbors['y_lower'] is not None or neighbors['y_upper'] is not None: + if neighbors["y_lower"] is not None or neighbors["y_upper"] is not None: halo_cells += 2 * nz * nx - if neighbors['z_lower'] is not None or neighbors['z_upper'] is not None: + if neighbors["z_lower"] is not None or neighbors["z_upper"] is not None: halo_cells += 2 * ny * nx info = RankInfo( @@ -253,7 +254,7 @@ def split_sizes(n, parts): halo_shape=halo_shape, neighbors=neighbors, n_neighbors=n_neighbors, - halo_cells_total=halo_cells + halo_cells_total=halo_cells, ) self._rank_info.append(info) @@ -266,19 +267,20 @@ def _cart_neighbor(self, ix, iy, iz, px, py, pz): def _factorize_3d(self, n): """Simple 3D factorization (as cubic as possible).""" candidates = np.arange(1, int(n**0.5) + 1) - divisors = np.concatenate([candidates[n % candidates == 0], - n // candidates[n % candidates == 0]]) + divisors = np.concatenate( + [candidates[n % candidates == 0], n // candidates[n % candidates == 0]] + ) divisors = np.unique(divisors) best = (n, 1, 1) - best_score = float('inf') + best_score = float("inf") for i in divisors: remaining = n // i valid_j = divisors[divisors <= remaining] for j in valid_j[remaining % valid_j == 0]: k = remaining // j - score = (i - j)**2 + (j - k)**2 + (k - i)**2 + score = (i - j) ** 2 + (j - k) ** 2 + (k - i) ** 2 if score < best_score: best = (int(i), int(j), int(k)) best_score = score @@ -316,7 +318,7 @@ def initialize_local_arrays_distributed(self, N, rank, comm): # Build local source term using physical coordinates h = 2.0 / (N - 1) - if self.strategy == 'sliced': + if self.strategy == "sliced": # Sliced: one axis decomposed, others full gs = info.global_start axis = self.axis # 0=z, 1=y, 2=x @@ -333,8 +335,10 @@ def initialize_local_arrays_distributed(self, N, rank, comm): # Full axis coords.append(np.linspace(-1, 1, N)) - Z, Y, X = np.meshgrid(coords[0], coords[1], coords[2], indexing='ij') - source = 3 * np.pi**2 * np.sin(np.pi * X) * np.sin(np.pi * Y) * np.sin(np.pi * Z) + Z, Y, X = np.meshgrid(coords[0], coords[1], coords[2], indexing="ij") + source = ( + 3 * np.pi**2 * np.sin(np.pi * X) * np.sin(np.pi * Y) * np.sin(np.pi * Z) + ) # Build interior slice (skip halo on decomposed axis only) interior = [slice(None), slice(None), slice(None)] @@ -361,13 +365,19 @@ def initialize_local_arrays_distributed(self, N, rank, comm): Yl = ys.reshape((1, ny, 1)) Xl = xs.reshape((1, 1, nx)) - f_local[1:-1, 1:-1, 1:-1] = 3 * np.pi**2 * np.sin(np.pi * Xl) * np.sin(np.pi * Yl) * np.sin(np.pi * Zl) + f_local[1:-1, 1:-1, 1:-1] = ( + 3 + * np.pi**2 + * np.sin(np.pi * Xl) + * np.sin(np.pi * Yl) + * np.sin(np.pi * Zl) + ) return u1, u2, f_local def extract_interior(self, u_local): """Extract interior points from local array (excluding halos).""" - if self.strategy == 'sliced': + if self.strategy == "sliced": interior = [slice(None), slice(None), slice(None)] interior[self.axis] = slice(1, -1) return u_local[tuple(interior)].copy() @@ -381,7 +391,7 @@ def get_interior_placement(self, rank_id, N, comm): """ info = self.get_rank_info(rank_id) - if self.strategy == 'sliced': + if self.strategy == "sliced": gs = info.global_start ge = info.global_end slices = [slice(None), slice(None), slice(None)] @@ -410,7 +420,7 @@ def apply_boundary_conditions(self, u_local, rank): rank : int MPI rank """ - if self.strategy != 'cubic': + if self.strategy != "cubic": return # Sliced doesn't need this - boundaries are handled differently info = self.get_rank_info(rank) @@ -446,7 +456,7 @@ class NoDecomposition: """Stub decomposition for single-rank (sequential) execution.""" def __init__(self): - self.strategy = 'none' + self.strategy = "none" self._N = None def get_rank_info(self, rank): @@ -454,18 +464,18 @@ def get_rank_info(self, rank): N = self._N or 1 return RankInfo( rank=0, - local_shape=(N-2, N-2, N-2), # Interior only + local_shape=(N - 2, N - 2, N - 2), # Interior only global_start=(1, 1, 1), - global_end=(N-1, N-1, N-1), + global_end=(N - 1, N - 1, N - 1), halo_shape=(N, N, N), neighbors={}, n_neighbors=0, - halo_cells_total=0 + halo_cells_total=0, ) def initialize_local_arrays_distributed(self, N, rank, comm): """Initialize arrays for single-rank execution.""" - from .problems import sinusoidal_source_term + from ..problems import sinusoidal_source_term self._N = N if rank == 0: diff --git a/src/Poisson/problems.py b/src/Poisson/problems.py index 4468f19..708a919 100644 --- a/src/Poisson/problems.py +++ b/src/Poisson/problems.py @@ -5,7 +5,9 @@ import numpy as np -def create_grid_3d(N: int, value: float = 0.0, boundary_value: float = 0.0) -> np.ndarray: +def create_grid_3d( + N: int, value: float = 0.0, boundary_value: float = 0.0 +) -> np.ndarray: """Create 3D grid with specified interior and boundary values.""" u = np.full((N, N, N), value, dtype=np.float64) u[[0, -1], :, :] = boundary_value @@ -35,7 +37,9 @@ def sinusoidal_source_term(N: int) -> np.ndarray: return 3 * np.pi**2 * np.sin(np.pi * xs) * np.sin(np.pi * ys) * np.sin(np.pi * zs) -def setup_sinusoidal_problem(N: int, initial_value: float = 0.0) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]: +def setup_sinusoidal_problem( + N: int, initial_value: float = 0.0 +) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]: """Set up sinusoidal test problem: -∇²u = 3π² sin(π x)sin(π y)sin(π z).""" h = 2.0 / (N - 1) u1 = create_grid_3d(N, value=initial_value, boundary_value=0.0) diff --git a/src/Poisson/jacobi.py b/src/Poisson/solver.py similarity index 61% rename from src/Poisson/jacobi.py rename to src/Poisson/solver.py index a7f0524..a929b69 100644 --- a/src/Poisson/jacobi.py +++ b/src/Poisson/solver.py @@ -5,6 +5,8 @@ is enabled by providing decomposition and communicator strategies. """ +import time + import numpy as np from dataclasses import asdict from mpi4py import MPI @@ -13,8 +15,8 @@ from .kernels import NumPyKernel, NumbaKernel from .datastructures import GlobalParams, GlobalMetrics, LocalSeries -from .communicators import NumpyHaloExchange -from .decomposition import NoDecomposition +from .mpi.communicators import NumpyHaloExchange +from .mpi.decomposition import NoDecomposition def _get_strategy_name(obj): @@ -22,7 +24,9 @@ def _get_strategy_name(obj): if obj is None: return "numpy" name = obj.__class__.__name__.lower() - return name.replace('decomposition', '').replace('communicator', '').replace('mpi', '') + return ( + name.replace("decomposition", "").replace("communicator", "").replace("mpi", "") + ) class JacobiPoisson: @@ -59,32 +63,39 @@ def __init__(self, decomposition=None, communicator=None, **kwargs): self.kernel = KernelClass( N=self.config.N, omega=self.config.omega, - numba_threads=self.config.numba_threads if self.config.use_numba else None + numba_threads=self.config.numba_threads if self.config.use_numba else None, ) # Strategy setup self._setup_strategies(decomposition, communicator) # Initialize arrays - self.u1_local, self.u2_local, self.f_local = \ + self.u1_local, self.u2_local, self.f_local = ( self.decomposition.initialize_local_arrays_distributed( self.config.N, self.rank, self.comm ) + ) def _setup_strategies(self, decomposition, communicator): """Configure decomposition and communicator strategies.""" if self.size == 1: # Single rank: store names but use NoDecomposition internally - self.config.decomposition = getattr(decomposition, 'strategy', 'none') if decomposition else "none" - self.config.communicator = _get_strategy_name(communicator) if communicator else "none" + self.config.decomposition = ( + getattr(decomposition, "strategy", "none") if decomposition else "none" + ) + self.config.communicator = ( + _get_strategy_name(communicator) if communicator else "none" + ) self.decomposition = NoDecomposition() self.communicator = communicator or NumpyHaloExchange() else: if decomposition is None: - raise ValueError("Decomposition strategy required for multi-rank execution") + raise ValueError( + "Decomposition strategy required for multi-rank execution" + ) self.decomposition = decomposition self.communicator = communicator or NumpyHaloExchange() - self.config.decomposition = getattr(decomposition, 'strategy', 'unknown') + self.config.decomposition = getattr(decomposition, "strategy", "unknown") self.config.communicator = _get_strategy_name(self.communicator) # ======================================================================== @@ -103,9 +114,18 @@ def _iterate(self): """Execute Jacobi iteration loop.""" uold, u = self.u1_local, self.u2_local + # MLflow timing + mlflow_time = 0.0 + for i in range(self.config.max_iter): residual = self._step(uold, u) + # Live MLflow logging every 50 iterations + if self.rank == 0 and i % 50 == 0 and mlflow.active_run(): + t_log_start = time.time() + mlflow.log_metrics({"residual": residual}, step=i) + mlflow_time += time.time() - t_log_start + if residual < self.config.tolerance: self._record_convergence(i + 1, converged=True) return u @@ -131,13 +151,15 @@ def _step(self, uold, u): # Compute residual after BCs (so boundary cells don't contribute) diff = u[1:-1, 1:-1, 1:-1] - uold[1:-1, 1:-1, 1:-1] - local_diff_sum = np.sum(diff ** 2) + local_diff_sum = np.sum(diff**2) self.timeseries.compute_times.append(MPI.Wtime() - t0) # Global residual t0 = MPI.Wtime() n_interior = (self.config.N - 2) ** 3 - global_residual = np.sqrt(self.comm.allreduce(local_diff_sum, op=MPI.SUM)) / n_interior + global_residual = ( + np.sqrt(self.comm.allreduce(local_diff_sum, op=MPI.SUM)) / n_interior + ) self.timeseries.mpi_comm_times.append(MPI.Wtime() - t0) if self.rank == 0: @@ -153,7 +175,9 @@ def _gather_solution(self, u_local): all_interiors = self.comm.gather(local_interior, root=0) self.u_global = np.zeros((self.config.N,) * 3) for rank_id, data in enumerate(all_interiors): - placement = self.decomposition.get_interior_placement(rank_id, self.config.N, self.comm) + placement = self.decomposition.get_interior_placement( + rank_id, self.config.N, self.comm + ) self.u_global[placement] = data else: self.comm.gather(local_interior, root=0) @@ -190,7 +214,10 @@ def compute_l2_error(self): gs = info.global_start local_shape = info.local_shape - if hasattr(self.decomposition, 'strategy') and self.decomposition.strategy == 'cubic': + if ( + hasattr(self.decomposition, "strategy") + and self.decomposition.strategy == "cubic" + ): # Cubic: all dims decomposed nz, ny, nx = local_shape z_idx = np.arange(gs[0], gs[0] + nz) @@ -216,7 +243,7 @@ def compute_l2_error(self): ys = np.linspace(-1, 1, N)[1:-1] # Interior only xs = np.linspace(-1, 1, N)[1:-1] - Z, Y, X = np.meshgrid(zs, ys, xs, indexing='ij') + Z, Y, X = np.meshgrid(zs, ys, xs, indexing="ij") u_exact = np.sin(np.pi * X) * np.sin(np.pi * Y) * np.sin(np.pi * Z) u_numerical = u_local[1:-1, 1:-1, 1:-1] @@ -246,39 +273,125 @@ def save_hdf5(self, path): return import pandas as pd + row = {**asdict(self.config), **asdict(self.results)} - pd.DataFrame([row]).to_hdf(path, key='results', mode='w') + pd.DataFrame([row]).to_hdf(path, key="results", mode="w") # ======================================================================== - # MLflow + # MLflow Integration # ======================================================================== - def mlflow_start(self, experiment_name): - """Start MLflow run (rank 0 only).""" + def mlflow_start( + self, experiment_name: str, run_name: str = None, parent_run_name: str = None + ): + """Start MLflow run and log parameters (rank 0 only).""" if self.rank != 0: return mlflow.login() - # Databricks requires absolute paths - experiment_name = f"/Shared/{experiment_name}" + # Databricks requires absolute paths - using a standard project prefix if not present + if not experiment_name.startswith("/"): + experiment_name = f"/Shared/LSM-Project-2/{experiment_name}" if mlflow.get_experiment_by_name(experiment_name) is None: mlflow.create_experiment(name=experiment_name) mlflow.set_experiment(experiment_name) - mlflow.start_run() + + # Handle parent run if specified + if parent_run_name: + experiment = mlflow.get_experiment_by_name(experiment_name) + client = mlflow.tracking.MlflowClient() + + # Search for existing parent run + runs = client.search_runs( + experiment_ids=[experiment.experiment_id], + filter_string=f"tags.mlflow.runName = '{parent_run_name}' AND tags.is_parent = 'true'", + max_results=1, + ) + + if runs: + parent_run_id = runs[0].info.run_id + else: + parent_run = client.create_run( + experiment_id=experiment.experiment_id, + run_name=parent_run_name, + tags={"is_parent": "true"}, + ) + parent_run_id = parent_run.info.run_id + + # Start nested child run + mlflow.start_run(run_id=parent_run_id, log_system_metrics=False) + mlflow.start_run(run_name=run_name, nested=True, log_system_metrics=True) + self._mlflow_nested = True + else: + mlflow.start_run(log_system_metrics=True, run_name=run_name) + self._mlflow_nested = False + + # Log all parameters from config mlflow.log_params(asdict(self.config)) - def mlflow_end(self): - """End MLflow run with metrics (rank 0 only).""" + def mlflow_end(self, log_time_series: bool = True): + """End MLflow run and log metrics (rank 0 only).""" if self.rank != 0: return - import mlflow - mlflow.log_metrics({ - "total_compute_time": sum(self.timeseries.compute_times), - "total_halo_time": sum(self.timeseries.halo_exchange_times), - "total_mpi_comm_time": sum(self.timeseries.mpi_comm_times), - }) + # Populate timing totals in results if timeseries exists + if self.timeseries.compute_times: + self.results.total_compute_time = sum(self.timeseries.compute_times) + if self.timeseries.halo_exchange_times: + self.results.total_halo_time = sum(self.timeseries.halo_exchange_times) + if self.timeseries.mpi_comm_times: + self.results.total_mpi_comm_time = sum(self.timeseries.mpi_comm_times) + + # Log final metrics + mlflow.log_metrics(asdict(self.results)) + + # Log time series as step-based metrics + if log_time_series: + self._mlflow_log_time_series() + + # End child run mlflow.end_run() + + # End parent run if nested + if getattr(self, "_mlflow_nested", False): + mlflow.end_run() + + def _mlflow_log_time_series(self): + """Log time series as step-based metrics using async batch logging.""" + from mlflow.entities import Metric + + if not mlflow.active_run(): + return + + run_id = mlflow.active_run().info.run_id + client = mlflow.tracking.MlflowClient() + timestamp = int(time.time() * 1000) + + # Build all metrics from timeseries + metrics = [] + ts_dict = asdict(self.timeseries) + for name, values in ts_dict.items(): + if values: # Skip empty lists + for step, value in enumerate(values): + # Ensure value is float + try: + val = float(value) + metrics.append(Metric(name, val, timestamp, step)) + except (ValueError, TypeError): + continue + + # Async batch log (non-blocking) - split into chunks of 1000 (MLflow limit) + batch_size = 1000 + for i in range(0, len(metrics), batch_size): + chunk = metrics[i : i + batch_size] + if chunk: + client.log_batch(run_id, metrics=chunk, synchronous=False) + + def mlflow_log_artifact(self, filepath: str): + """Log an artifact to MLflow (rank 0 only).""" + if self.rank != 0: + return + mlflow.log_artifact(filepath) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 4212172..1a8ea02 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,11 +1,12 @@ -"""Utility modules for data handling, plotting, and CLI. +"""Utility modules for plotting and CLI. Import conveniences: -- from utils import datatools # For data operations - from utils import plotting # For plotting operations - from utils import cli # For command-line argument parsing +- from utils import mlflow_io # For MLflow I/O operations +- from utils import hpc # For HPC job generation """ -from . import datatools, plotting, cli +from . import plotting, cli, mlflow_io, hpc -__all__ = ["datatools", "plotting", "cli"] +__all__ = ["plotting", "cli", "mlflow_io", "hpc"] diff --git a/src/utils/datatools.py b/src/utils/datatools.py deleted file mode 100644 index 280238a..0000000 --- a/src/utils/datatools.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Data utilities for creating, loading, and saving simulation data. - -This module provides utilities for: -- Path management (automatic directory structure mirroring Experiments/) -- Data I/O (loading and saving dataframes) -- Numerical computations (norms and errors) -- DataFrame utilities -""" - -from __future__ import annotations - -import inspect -from pathlib import Path -from typing import Any, Literal - -import numpy as np -import pandas as pd - - -# ============================================================================== -# Path Management -# ============================================================================== - - -def get_repo_root() -> Path: - """Get repository root directory. - - Returns the repository root by detecting the presence of pyproject.toml. - Works from any subdirectory of the repository. - - Returns - ------- - Path - Absolute path to the repository root - - """ - # Start from this file's location - current = Path(__file__).resolve().parent - - # Walk up until we find pyproject.toml (marks repo root) - for parent in [current] + list(current.parents): - if (parent / "pyproject.toml").exists(): - return parent - - # Fallback: assume two levels up from utils - return current.parent.parent - - -def get_experiment_name(caller_file: Path | str | None = None) -> str: - """Get experiment name from the calling script's location. - - Extracts the experiment name from the path relative to Experiments/. - For example: - - Experiments/sequential/compute.py → "sequential" - - Experiments/parallel/mpi/compute.py → "parallel/mpi" - - Parameters - ---------- - caller_file : Path or str, optional - Path to the calling file. If None, automatically detects the caller. - - Returns - ------- - str - Experiment name (relative path from Experiments/) - - Raises - ------ - ValueError - If the calling file is not in an Experiments/ subdirectory - - """ - if caller_file is None: - # Get the calling file from the stack - # We need to go up 2 frames: this function -> get_data_dir/get_figures_dir -> actual caller - frame = inspect.currentframe() - if frame is None or frame.f_back is None or frame.f_back.f_back is None: - raise RuntimeError("Cannot detect caller file") - caller_file = Path(frame.f_back.f_back.f_globals["__file__"]) - else: - caller_file = Path(caller_file) - - caller_file = caller_file.resolve() - - # Find Experiments directory in the path - experiments_idx = None - for i, part in enumerate(caller_file.parts): - if part == "Experiments": - experiments_idx = i - break - - if experiments_idx is None: - raise ValueError( - f"File {caller_file} is not in an Experiments/ subdirectory. " - "This utility is designed for scripts in Experiments/*/" - ) - - # Get the path components between Experiments/ and the filename - # e.g., for Experiments/sequential/compute.py → ["sequential"] - # or for Experiments/parallel/mpi/compute.py → ["parallel", "mpi"] - experiment_parts = caller_file.parts[experiments_idx + 1 : -1] - - if not experiment_parts: - raise ValueError( - f"File {caller_file} is directly in Experiments/. " - "Scripts should be in a subdirectory (e.g., Experiments/sequential/)" - ) - - return "/".join(experiment_parts) - - -def get_data_dir(caller_file: Path | str | None = None, create: bool = True) -> Path: - """Get data directory for the calling experiment. - - Automatically determines the correct data directory based on the - calling script's location in Experiments/, mirroring the structure. - - Parameters - ---------- - caller_file : Path or str, optional - Path to the calling file. If None, automatically detects the caller. - create : bool, default True - Whether to create the directory if it doesn't exist - - Returns - ------- - Path - Data directory path (e.g., repo_root/data/sequential/) - - Examples - -------- - From Experiments/sequential/compute.py: - >>> data_dir = get_data_dir() # Returns repo_root/data/sequential/ - - From Experiments/parallel/mpi/compute.py: - >>> data_dir = get_data_dir() # Returns repo_root/data/parallel/mpi/ - - """ - experiment_name = get_experiment_name(caller_file) - repo_root = get_repo_root() - data_dir = repo_root / "data" / experiment_name - - if create: - data_dir.mkdir(parents=True, exist_ok=True) - - return data_dir - - -def get_figures_dir(caller_file: Path | str | None = None, create: bool = True) -> Path: - """Get figures directory for the calling experiment. - - Automatically determines the correct figures directory based on the - calling script's location in Experiments/, mirroring the structure. - - Parameters - ---------- - caller_file : Path or str, optional - Path to the calling file. If None, automatically detects the caller. - create : bool, default True - Whether to create the directory if it doesn't exist - - Returns - ------- - Path - Figures directory path (e.g., repo_root/figures/sequential/) - - Examples - -------- - From Experiments/sequential/plot.py: - >>> figures_dir = get_figures_dir() # Returns repo_root/figures/sequential/ - - From Experiments/parallel/mpi/plot.py: - >>> figures_dir = get_figures_dir() # Returns repo_root/figures/parallel/mpi/ - - """ - experiment_name = get_experiment_name(caller_file) - repo_root = get_repo_root() - figures_dir = repo_root / "figures" / experiment_name - - if create: - figures_dir.mkdir(parents=True, exist_ok=True) - - return figures_dir - - -def ensure_output_dir(path: Path | str) -> Path: - """Ensure output directory exists, creating it if necessary. - - Parameters - ---------- - path : Path or str - Directory path to create - - Returns - ------- - Path - The created/existing directory path - - """ - path = Path(path) - path.mkdir(parents=True, exist_ok=True) - return path - - diff --git a/src/utils/generate_pack.py b/src/utils/generate_pack.py new file mode 100644 index 0000000..abb235a --- /dev/null +++ b/src/utils/generate_pack.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +Generate LSF job pack for scaling experiments. + +Usage: + python generate_pack.py --type strong --output jobs.pack + python generate_pack.py --type weak +""" + +import argparse +from pathlib import Path +import sys +# Use relative import since this is now in utils +from .hpc import load_config, generate_pack_lines, write_pack_file + +def main(): + parser = argparse.ArgumentParser(description="Generate LSF job pack") + parser.add_argument("--type", choices=["strong", "weak"], required=True, help="Scaling type") + parser.add_argument("--output", type=str, default="jobs.pack", help="Output pack file") + parser.add_argument("--config-dir", type=str, required=True, help="Directory containing config files") + # Optional override for strong scaling grid sizes + parser.add_argument("--N", type=int, nargs="+", help="Grid sizes for strong scaling") + + args = parser.parse_args() + + # Load config based on type from the specified directory + config_path = Path(args.config_dir) / f"{args.type}_scaling.yaml" + if not config_path.exists(): + # Fallback to template for strong scaling if specific file doesn't exist + if args.type == "strong": + config_path = Path(args.config_dir) / "template_config.yaml" + + if not config_path.exists(): + print(f"Error: Config file {config_path} not found") + sys.exit(1) + + config = load_config(config_path) + + # Override N if provided + if args.N and args.type == "strong": + if "groups" in config: + for group in config["groups"]: + if "sweep" in group: + group["sweep"]["N"] = args.N + elif "sweep" in config: + config["sweep"]["N"] = args.N + + # Generate content + job_name_base = f"{args.type}_scaling" + lines = generate_pack_lines(config, job_name_base) + + # Write file + write_pack_file(args.output, lines) + + print(f"Generated {len(lines)} jobs in {args.output}") + +if __name__ == "__main__": + main() diff --git a/src/utils/hpc.py b/src/utils/hpc.py new file mode 100644 index 0000000..ded264c --- /dev/null +++ b/src/utils/hpc.py @@ -0,0 +1,168 @@ +"""Utilities for LSF job pack generation.""" + +import yaml +import itertools +from pathlib import Path +from typing import List, Dict, Any + + +def load_config(config_path: Path) -> Dict[str, Any]: + """Load YAML configuration file.""" + with open(config_path, "r") as f: + return yaml.safe_load(f) + + +def generate_pack_lines(config: Dict[str, Any], job_name_base: str) -> List[str]: + """Generate list of job pack lines (options + command).""" + script = config.get("script", "compute_scaling.py") + base_cmd = f"mpiexec -n {{ranks}} uv run python {script}" + + # Global defaults + global_params = config.get("parameters", {}) + global_lsf = config.get("lsf", {}) + global_sweep = config.get("sweep", {}) + global_mpi = config.get("mpi_options", "") + + # Normalize to a list of groups + if "groups" in config: + groups = config["groups"] + else: + # Legacy/Simple mode: Treat top-level as a single group + groups = [{ + "name": "default", + "lsf": {}, + "parameters": {}, + "sweep": global_sweep, + "mpi_options": "" + }] + + lines = [] + global_job_counter = 1 + + for group in groups: + # Merge configurations (Group overrides Global) + group_lsf = {**global_lsf, **group.get("lsf", {})} + group_params = {**global_params, **group.get("parameters", {})} + group_sweep = group.get("sweep", {}) + group_mpi = group.get("mpi_options", global_mpi) + + # If no sweep in group, use global sweep (if it exists and wasn't just the legacy fallback) + if not group_sweep and "groups" in config: + group_sweep = global_sweep + + # Prepare sweep combinations + keys = list(group_sweep.keys()) + values = list(group_sweep.values()) + combinations = list(itertools.product(*values)) + + group_name = group.get("name", "job") + + for combo in combinations: + current_params = group_params.copy() + current_sweep = dict(zip(keys, combo)) + + # Handle scaling logic + scaling_type = config.get("type", "strong") + if scaling_type == "weak": + # For weak scaling, N depends on ranks + base_N = current_params.get("base_N", 32) + ranks = current_sweep.get("ranks", 1) + N = int(round(base_N * (ranks**(1/3)))) + current_params["N"] = N + + # Merge sweep parameters + current_params.update(current_sweep) + + # Extract ranks for mpiexec and bsub -n + ranks = current_params.pop("ranks") + + # Calculate job name early to pass to script + current_job_name = f"{job_name_base}_{group_name}_{global_job_counter}" + + # Build command arguments + args = [] + for k, v in current_params.items(): + if isinstance(v, bool): + if v: + args.append(f"--{k}") + else: + args.append(f"--{k} {v}") + + # Add logging info for MLflow artifact upload + args.append(f"--job-name {current_job_name}") + args.append("--log-dir logs") + args.append(f"--experiment-name {group_name}") + + # Construct the actual command to run + # base_cmd pattern: mpiexec -n {ranks} {mpi_options} uv run python {script} {args} + cmd_parts = [f"mpiexec -n {ranks}"] + if group_mpi: + cmd_parts.append(group_mpi) + cmd_parts.append(f"uv run python {script}") + cmd_parts.append(" ".join(args)) + + main_cmd = " ".join(cmd_parts) + + # Chain the log uploader command + # We use (cmd; uploader) to ensure uploader runs regardless of cmd exit status + uploader_cmd = f"uv run python src/utils/upload_logs.py --job-name {current_job_name} --log-dir logs --experiment-name {group_name}" + cmd = f'({main_cmd}; {uploader_cmd})' + + # Build LSF options for this specific job + # current_job_name is already defined above + + # Map config keys to LSF flags + lsf_opts = [] + lsf_opts.append(f"-J {current_job_name}") + + # Cores: driven by ranks (if in sweep/params) or explicit 'cores' config + cores = ranks if ranks is not None else group_lsf.get('cores', 1) + lsf_opts.append(f"-n {cores}") + + # Value mappings + val_map = { + 'queue': '-q', + 'walltime': '-W', + 'memory': '-M', + 'email': '-u', + 'project': '-P', + } + for key, flag in val_map.items(): + if key in group_lsf: + lsf_opts.append(f"{flag} {group_lsf[key]}") + + # Boolean mappings + bool_map = { + 'exclusive': '-x', + 'notify_start': '-B', + 'notify_end': '-N', + } + for key, flag in bool_map.items(): + if group_lsf.get(key, False): + lsf_opts.append(flag) + + # Resource requirements (-R) + resources = group_lsf.get('resources', []) + if isinstance(resources, str): + resources = [resources] + for r in resources: + lsf_opts.append(f'-R "{r}"') + + # Logs + lsf_opts.append(f"-o logs/{current_job_name}.out") + lsf_opts.append(f"-e logs/{current_job_name}.err") + + # Combine options and command + line = " ".join(lsf_opts) + " " + cmd + lines.append(line) + global_job_counter += 1 + + return lines + + +def write_pack_file(output_path: Path, lines: List[str]): + """Write LSF pack file.""" + with open(output_path, "w") as f: + f.write("# LSF Job Pack generated by generate_pack.py\n") + for line in lines: + f.write(line + "\n") \ No newline at end of file diff --git a/src/utils/mlflow_io.py b/src/utils/mlflow_io.py new file mode 100644 index 0000000..c01721f --- /dev/null +++ b/src/utils/mlflow_io.py @@ -0,0 +1,207 @@ +"""MLflow I/O utilities for fetching runs and artifacts. + +This module provides helpers for retrieving experiment data from +MLflow/Databricks and downloading artifacts to the local data directory. +""" + +import os +from pathlib import Path +from typing import List, Optional +import mlflow +import pandas as pd + + +def setup_mlflow_auth(): + """Configure MLflow authentication. + + Uses DATABRICKS_TOKEN environment variable if available (for CI), + otherwise falls back to interactive login. + """ + token = os.environ.get("DATABRICKS_TOKEN") + if token: + # CI environment - set both host and token for Databricks auth + host = "https://dbc-6756e917-e5fc.cloud.databricks.com" + os.environ["DATABRICKS_HOST"] = host + mlflow.set_tracking_uri("databricks") + else: + # Local environment - interactive login + mlflow.login() + + +def load_runs( + experiment: str, + converged_only: bool = True, + exclude_parent_runs: bool = True, +) -> pd.DataFrame: + """Load runs from an MLflow experiment. + + Parameters + ---------- + experiment : str + Experiment name (e.g., "HPC-FV-Solver" or full path "/Shared/ANA-P3/HPC-FV-Solver"). + converged_only : bool, default True + Only return runs where metrics.converged = 1. + exclude_parent_runs : bool, default True + Exclude parent runs (nested run containers). + + Returns + ------- + pd.DataFrame + DataFrame with run info, parameters (params.*), and metrics (metrics.*). + + Examples + -------- + >>> df = load_runs("HPC-FV-Solver") + >>> df[["run_id", "params.nx", "metrics.wall_time_seconds"]] + """ + # Normalize experiment name + if not experiment.startswith("/"): + # Note: Adapted for LSM Project 2, assuming same shared folder structure or user should adjust + experiment = f"/Shared/LSM-Project-2/{experiment}" + + # Build filter string + filters = [] + if converged_only: + filters.append("metrics.converged = 1") + + filter_string = " and ".join(filters) if filters else "" + + # Fetch runs + df = mlflow.search_runs( + experiment_names=[experiment], + filter_string=filter_string, + order_by=["start_time DESC"], + ) + + # Filter out parent runs in pandas (MLflow filter doesn't handle None well) + if exclude_parent_runs and "tags.is_parent" in df.columns: + df = df[df["tags.is_parent"] != "true"] + + return df + + +def download_artifacts( + experiment: str, + output_dir: Path, + converged_only: bool = True, + artifact_filter: Optional[List[str]] = None, +) -> List[Path]: + """Download artifacts from MLflow runs to local directory. + + Parameters + ---------- + experiment : str + Experiment name (e.g., "HPC-FV-Solver"). + output_dir : Path + Directory to save artifacts. Files are named based on run parameters. + converged_only : bool, default True + Only download from converged runs. + artifact_filter : list of str, optional + Only download artifacts matching these patterns (e.g., ["*.h5", "*.png"]). + If None, downloads all artifacts. + + Returns + ------- + list of Path + Paths to downloaded files. + + Examples + -------- + >>> paths = download_artifacts("HPC-FV-Solver", Path("data/FV-Solver")) + >>> print(paths) + [Path('data/FV-Solver/LDC_N32_Re100.h5'), ...] + """ + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Get runs + df = load_runs(experiment, converged_only=converged_only) + if df.empty: + print(f"No runs found for {experiment}") + return [] + + client = mlflow.tracking.MlflowClient() + downloaded = [] + + for _, row in df.iterrows(): + run_id = row["run_id"] + + # List artifacts + artifacts = client.list_artifacts(run_id) + + for artifact in artifacts: + # Apply filter if specified + if artifact_filter: + if not any(artifact.path.endswith(f) for f in artifact_filter): + continue + + # Download to output directory + local_path = client.download_artifacts(run_id, artifact.path, output_dir) + downloaded.append(Path(local_path)) + print(f" Downloaded: {artifact.path}") + + return downloaded + + +def download_artifacts_with_naming( + experiment: str, + output_dir: Path, + converged_only: bool = True, +) -> List[Path]: + """Download HDF5 artifacts with standardized naming. + + Names files as: POISSON_N{n}_Iter{iter}.h5 (Adapted for LSM) + + Parameters + ---------- + experiment : str + Experiment name. + output_dir : Path + Directory to save artifacts. + converged_only : bool, default True + Only download from converged runs. + + Returns + ------- + list of Path + Paths to downloaded files. + """ + import tempfile + import shutil + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + df = load_runs(experiment, converged_only=converged_only) + if df.empty: + print(f"No runs found for {experiment}") + return [] + + client = mlflow.tracking.MlflowClient() + downloaded = [] + + for _, row in df.iterrows(): + run_id = row["run_id"] + + # Extract parameters for naming - Adapting to typical Poisson params + # Assuming 'n' is grid size, 'max_iter' or 'iterations' might be useful + n = row.get("params.n", row.get("params.N", "unknown")) + + # List artifacts and find HDF5 files + artifacts = client.list_artifacts(run_id) + + for artifact in artifacts: + if artifact.path.endswith(".h5"): + # Download to temp location first + with tempfile.TemporaryDirectory() as tmpdir: + tmp_path = client.download_artifacts(run_id, artifact.path, tmpdir) + + # Rename with standardized naming + new_name = f"Poisson_N{n}_{artifact.path.split('/')[-1]}" + final_path = output_dir / new_name + + shutil.copy(tmp_path, final_path) + downloaded.append(final_path) + print(f" {artifact.path} -> {new_name}") + + return downloaded diff --git a/src/utils/upload_logs.py b/src/utils/upload_logs.py new file mode 100644 index 0000000..d8b9300 --- /dev/null +++ b/src/utils/upload_logs.py @@ -0,0 +1,80 @@ +"""Helper script to upload LSF logs to MLflow after a job finishes. + +This script is designed to run after the main computation, regardless of success or failure. +It relies on a .runid file created by the main script to know which MLflow run to attach logs to. +""" + +import argparse +import os +import time +from pathlib import Path +import mlflow + +def main(): + parser = argparse.ArgumentParser(description="Upload LSF logs to MLflow") + parser.add_argument("--job-name", type=str, required=True, help="LSF Job Name") + parser.add_argument("--log-dir", type=str, default="logs", help="Directory containing logs") + parser.add_argument("--experiment-name", type=str, default="HPC-Poisson-Scaling", help="MLflow experiment name") + args = parser.parse_args() + + log_dir = Path(args.log_dir) + job_name = args.job_name + run_id_file = log_dir / f"{job_name}.runid" + out_log = log_dir / f"{job_name}.out" + err_log = log_dir / f"{job_name}.err" + + # Give the filesystem a moment to sync logs if the job just crashed + time.sleep(2) + + run_id = None + if run_id_file.exists(): + try: + with open(run_id_file, "r") as f: + run_id = f.read().strip() + print(f"Found Run ID: {run_id}") + except Exception as e: + print(f"Error reading run ID file: {e}") + + # Logic to handle upload + try: + active_run = None + + if run_id: + # Resume existing run + active_run = mlflow.start_run(run_id=run_id, log_system_metrics=False) + else: + # Create new run for startup failure + print("Run ID file not found. Creating new run for startup failure.") + experiment_name = args.experiment_name + + # Ensure experiment exists + if mlflow.get_experiment_by_name(experiment_name) is None: + try: + mlflow.create_experiment(name=experiment_name) + except: + pass # concurrent creation might fail, ignore + + mlflow.set_experiment(experiment_name) + active_run = mlflow.start_run(run_name=f"{job_name} (Startup Failure)") + mlflow.set_tag("status", "startup_failure") + + with active_run: + if out_log.exists(): + print(f"Uploading stdout: {out_log}") + mlflow.log_artifact(str(out_log), artifact_path="logs") + else: + print(f"Warning: stdout log not found at {out_log}") + + if err_log.exists(): + print(f"Uploading stderr: {err_log}") + mlflow.log_artifact(str(err_log), artifact_path="logs") + else: + print(f"Warning: stderr log not found at {err_log}") + + print("Log upload complete.") + + except Exception as e: + print(f"Failed to upload logs to MLflow: {e}") + +if __name__ == "__main__": + main() diff --git a/tests/test_communicators.py b/tests/test_communicators.py deleted file mode 100644 index 7f6c6c2..0000000 --- a/tests/test_communicators.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Tests for halo exchange communicators.""" - -import numpy as np -import pytest -from Poisson import NumpyHaloExchange, CustomHaloExchange - - -class MockDecomposition: - """Mock decomposition for testing communicators.""" - - def __init__(self, neighbors): - self._neighbors = neighbors - - def get_neighbors(self, rank): - return self._neighbors - - -class MockComm: - """Mock MPI communicator for single-rank testing.""" - - PROC_NULL = -1 - - def Sendrecv(self, sendbuf, dest, sendtag, recvbuf, source, recvtag): - """No-op for single rank - just zero the receive buffer.""" - if hasattr(recvbuf, '__getitem__') and hasattr(recvbuf[0], 'fill'): - recvbuf[0].fill(0.0) - - -class TestNumpyHaloExchange: - """Tests for NumPy-based halo exchange.""" - - def test_sliced_z_axis_boundaries_zeroed(self): - """Boundary halos should be zero (Dirichlet BC).""" - comm = NumpyHaloExchange() - u = np.ones((10, 8, 8), dtype=np.float64) - - # No neighbors = boundary rank - decomp = MockDecomposition({'z_lower': None, 'z_upper': None}) - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - - # Halos at boundaries should remain unchanged (no neighbor to receive from) - # Interior should be unchanged - assert np.all(u[1:-1, :, :] == 1.0) - - def test_sliced_y_axis_detection(self): - """Should detect y-axis decomposition from neighbor keys.""" - comm = NumpyHaloExchange() - u = np.ones((8, 10, 8), dtype=np.float64) - - decomp = MockDecomposition({'y_lower': None, 'y_upper': None}) - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - - # Should not crash - verifies axis detection works - assert u.shape == (8, 10, 8) - - def test_sliced_interior_unchanged(self): - """Sliced halo exchange should not modify interior values.""" - comm = NumpyHaloExchange() - u = np.ones((10, 8, 8), dtype=np.float64) - interior_before = u[1:-1, :, :].copy() - - decomp = MockDecomposition({'z_lower': None, 'z_upper': None}) - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - - # Interior should be unchanged - assert np.allclose(u[1:-1, :, :], interior_before) - - def test_cubic_all_boundaries(self): - """Cubic decomposition with all boundaries should zero halos.""" - comm = NumpyHaloExchange() - u = np.ones((10, 10, 10), dtype=np.float64) - - # Corner rank - no neighbors - decomp = MockDecomposition({ - 'x_lower': None, 'x_upper': None, - 'y_lower': None, 'y_upper': None, - 'z_lower': None, 'z_upper': None, - }) - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - - # All boundary halos should be zeroed - assert np.all(u[1:-1, 1:-1, 0] == 0.0) # x_lower - assert np.all(u[1:-1, 1:-1, -1] == 0.0) # x_upper - assert np.all(u[1:-1, 0, 1:-1] == 0.0) # y_lower - assert np.all(u[1:-1, -1, 1:-1] == 0.0) # y_upper - assert np.all(u[0, 1:-1, 1:-1] == 0.0) # z_lower - assert np.all(u[-1, 1:-1, 1:-1] == 0.0) # z_upper - - # Interior unchanged - assert np.all(u[1:-1, 1:-1, 1:-1] == 1.0) - - -class TestCustomHaloExchange: - """Tests for MPI datatype-based halo exchange.""" - - def test_datatype_caching(self): - """Should cache datatypes after first exchange.""" - comm = CustomHaloExchange() - u = np.ones((10, 10, 10), dtype=np.float64) - - decomp = MockDecomposition({'z_lower': None, 'z_upper': None}) - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - - # Datatypes should be cached - assert len(comm._cache) == 1 - - def test_datatype_reuse(self): - """Should reuse datatypes for same shape/decomposition.""" - comm = CustomHaloExchange() - u = np.ones((10, 10, 10), dtype=np.float64) - decomp = MockDecomposition({'z_lower': None, 'z_upper': None}) - - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - cache_size_after_first = len(comm._cache) - - comm.exchange_halos(u, decomp, rank=0, comm=MockComm()) - assert len(comm._cache) == cache_size_after_first # No new entries - - def test_datatype_recreation_on_shape_change(self): - """Should create new datatypes when array shape changes.""" - comm = CustomHaloExchange() - decomp = MockDecomposition({'z_lower': None, 'z_upper': None}) - - u1 = np.ones((10, 10, 10), dtype=np.float64) - comm.exchange_halos(u1, decomp, rank=0, comm=MockComm()) - assert len(comm._cache) == 1 - - u2 = np.ones((12, 12, 12), dtype=np.float64) - comm.exchange_halos(u2, decomp, rank=0, comm=MockComm()) - assert len(comm._cache) == 2 # New entry for different shape diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index b13820b..e0de391 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -11,30 +11,30 @@ class TestSlicedDecomposition: @pytest.mark.parametrize("N,size", [(50, 4), (64, 8), (100, 7)]) def test_full_coverage_no_overlaps(self, N, size): """Each interior point owned by exactly one rank.""" - decomp = DomainDecomposition(N=N, size=size, strategy='sliced') + decomp = DomainDecomposition(N=N, size=size, strategy="sliced") total_nz = sum(decomp.get_rank_info(r).local_shape[0] for r in range(size)) assert total_nz == N - 2 # interior size def test_neighbors_correct(self): """Interior ranks have 2 neighbors, boundary ranks have 1.""" - decomp = DomainDecomposition(N=50, size=4, strategy='sliced') + decomp = DomainDecomposition(N=50, size=4, strategy="sliced") # First rank: no lower neighbor - assert decomp.get_rank_info(0).neighbors['z_lower'] is None - assert decomp.get_rank_info(0).neighbors['z_upper'] == 1 + assert decomp.get_rank_info(0).neighbors["z_lower"] is None + assert decomp.get_rank_info(0).neighbors["z_upper"] == 1 # Interior rank: both neighbors - assert decomp.get_rank_info(1).neighbors['z_lower'] == 0 - assert decomp.get_rank_info(1).neighbors['z_upper'] == 2 + assert decomp.get_rank_info(1).neighbors["z_lower"] == 0 + assert decomp.get_rank_info(1).neighbors["z_upper"] == 2 # Last rank: no upper neighbor - assert decomp.get_rank_info(3).neighbors['z_lower'] == 2 - assert decomp.get_rank_info(3).neighbors['z_upper'] is None + assert decomp.get_rank_info(3).neighbors["z_lower"] == 2 + assert decomp.get_rank_info(3).neighbors["z_upper"] is None def test_halo_shape(self): """Halo shape adds 2 in z-direction only.""" - decomp = DomainDecomposition(N=50, size=4, strategy='sliced') + decomp = DomainDecomposition(N=50, size=4, strategy="sliced") info = decomp.get_rank_info(1) nz, ny, nx = info.local_shape @@ -51,7 +51,7 @@ class TestCubicDecomposition: def test_full_coverage_no_overlaps(self): """Each point owned by exactly one rank.""" N, size = 64, 8 - decomp = DomainDecomposition(N=N, size=size, strategy='cubic') + decomp = DomainDecomposition(N=N, size=size, strategy="cubic") covered = np.zeros((N, N, N), dtype=bool) for rank in range(size): @@ -65,11 +65,16 @@ def test_full_coverage_no_overlaps(self): def test_neighbor_reciprocity(self): """If A neighbors B, then B neighbors A.""" - decomp = DomainDecomposition(N=64, size=8, strategy='cubic') + decomp = DomainDecomposition(N=64, size=8, strategy="cubic") - opposites = {'x_lower': 'x_upper', 'x_upper': 'x_lower', - 'y_lower': 'y_upper', 'y_upper': 'y_lower', - 'z_lower': 'z_upper', 'z_upper': 'z_lower'} + opposites = { + "x_lower": "x_upper", + "x_upper": "x_lower", + "y_lower": "y_upper", + "y_upper": "y_lower", + "z_lower": "z_upper", + "z_upper": "z_lower", + } for rank in range(8): info = decomp.get_rank_info(rank) @@ -80,7 +85,7 @@ def test_neighbor_reciprocity(self): def test_corner_has_3_neighbors(self): """Corner rank (rank 0) touches 3 boundaries, has 3 neighbors.""" - decomp = DomainDecomposition(N=64, size=8, strategy='cubic') + decomp = DomainDecomposition(N=64, size=8, strategy="cubic") info = decomp.get_rank_info(0) assert info.global_start == (0, 0, 0) @@ -88,7 +93,7 @@ def test_corner_has_3_neighbors(self): def test_interior_has_6_neighbors(self): """Interior rank has all 6 neighbors.""" - decomp = DomainDecomposition(N=81, size=27, strategy='cubic') # 3x3x3 + decomp = DomainDecomposition(N=81, size=27, strategy="cubic") # 3x3x3 # Center rank at (1,1,1) in processor grid py, pz = decomp.dims[1], decomp.dims[2] @@ -104,30 +109,30 @@ class TestConfigurableAxis: def test_z_axis_works(self): """Default z-axis decomposition.""" - decomp = DomainDecomposition(N=50, size=4, strategy='sliced') + decomp = DomainDecomposition(N=50, size=4, strategy="sliced") info = decomp.get_rank_info(0) assert info.local_shape[1] == 50 # full y assert info.local_shape[2] == 50 # full x - assert 'z_lower' in info.neighbors + assert "z_lower" in info.neighbors def test_y_axis_decomposition(self): """Decompose along y-axis.""" - decomp = DomainDecomposition(N=50, size=4, strategy='sliced', axis='y') + decomp = DomainDecomposition(N=50, size=4, strategy="sliced", axis="y") info = decomp.get_rank_info(0) assert info.local_shape[0] == 50 # full z assert info.local_shape[2] == 50 # full x - assert 'y_lower' in info.neighbors + assert "y_lower" in info.neighbors def test_x_axis_decomposition(self): """Decompose along x-axis.""" - decomp = DomainDecomposition(N=50, size=4, strategy='sliced', axis='x') + decomp = DomainDecomposition(N=50, size=4, strategy="sliced", axis="x") info = decomp.get_rank_info(0) assert info.local_shape[0] == 50 # full z assert info.local_shape[1] == 50 # full y - assert 'x_lower' in info.neighbors + assert "x_lower" in info.neighbors class TestEdgeCases: @@ -135,7 +140,7 @@ class TestEdgeCases: def test_single_rank(self): """Single rank gets entire interior.""" - decomp = DomainDecomposition(N=50, size=1, strategy='sliced') + decomp = DomainDecomposition(N=50, size=1, strategy="sliced") info = decomp.get_rank_info(0) assert info.local_shape[0] == 48 @@ -144,4 +149,4 @@ def test_single_rank(self): def test_invalid_strategy(self): """Unknown strategy raises ValueError.""" with pytest.raises(ValueError): - DomainDecomposition(N=50, size=4, strategy='invalid') + DomainDecomposition(N=50, size=4, strategy="invalid") diff --git a/tests/test_kernels.py b/tests/test_kernels.py index 6b348ba..38391b3 100644 --- a/tests/test_kernels.py +++ b/tests/test_kernels.py @@ -1,8 +1,7 @@ """Tests for Jacobi iteration kernels.""" import numpy as np -import pytest -from Poisson import NumPyKernel, NumbaKernel, setup_sinusoidal_problem, sinusoidal_exact_solution +from Poisson import NumPyKernel, NumbaKernel, setup_sinusoidal_problem def run_iterations(kernel, u1, u2, f, n_iter): @@ -21,62 +20,12 @@ def test_kernels_produce_identical_results(): u1, u2, f, _ = setup_sinusoidal_problem(N) numpy_kernel = NumPyKernel(N=N, omega=1.0, tolerance=1e-10, max_iter=1000) - numba_kernel = NumbaKernel(N=N, omega=1.0, tolerance=1e-10, max_iter=1000, numba_threads=1) + numba_kernel = NumbaKernel( + N=N, omega=1.0, tolerance=1e-10, max_iter=1000, numba_threads=1 + ) numba_kernel.warmup() u_numpy = run_iterations(numpy_kernel, u1.copy(), u2.copy(), f.copy(), 10) u_numba = run_iterations(numba_kernel, u1.copy(), u2.copy(), f.copy(), 10) assert np.allclose(u_numpy, u_numba, atol=1e-10) - - -def test_convergence_order(): - """Jacobi kernel should achieve O(h²) convergence.""" - problem_sizes = [15, 25, 50] - errors, h_values = [], [] - - for N in problem_sizes: - u1, u2, f, h = setup_sinusoidal_problem(N) - exact = sinusoidal_exact_solution(N) - kernel = NumPyKernel(N=N, omega=1.0, tolerance=1e-12, max_iter=50000) - - # Solve until converged - for i in range(50000): - if i % 2 == 0: - res = kernel.step(u1, u2, f) - u = u2 - else: - res = kernel.step(u2, u1, f) - u = u1 - if np.sqrt(res / (N-2)**3) < 1e-12: - break - - l2_error = np.sqrt(np.mean((u[1:-1,1:-1,1:-1] - exact[1:-1,1:-1,1:-1])**2)) - errors.append(l2_error) - h_values.append(h) - - # Calculate convergence rate - rates = [np.log(errors[i]/errors[i+1]) / np.log(h_values[i]/h_values[i+1]) - for i in range(len(errors)-1)] - avg_rate = np.mean(rates) - - assert abs(avg_rate - 2.0) < 0.2, f"Rate {avg_rate:.2f} not close to 2.0" - - -def test_boundary_preservation(): - """Kernels should not modify boundary conditions.""" - N = 16 - u1, u2, f, _ = setup_sinusoidal_problem(N) - boundaries = [u1[0,:,:].copy(), u1[-1,:,:].copy(), - u1[:,0,:].copy(), u1[:,-1,:].copy(), - u1[:,:,0].copy(), u1[:,:,-1].copy()] - - kernel = NumPyKernel(N=N, omega=1.0, tolerance=1e-10, max_iter=100) - u = run_iterations(kernel, u1, u2, f, 10) - - assert np.allclose(u[0,:,:], boundaries[0]) - assert np.allclose(u[-1,:,:], boundaries[1]) - assert np.allclose(u[:,0,:], boundaries[2]) - assert np.allclose(u[:,-1,:], boundaries[3]) - assert np.allclose(u[:,:,0], boundaries[4]) - assert np.allclose(u[:,:,-1], boundaries[5]) diff --git a/tests/test_mpi_integration.py b/tests/test_mpi_integration.py index 44be69e..198ed07 100644 --- a/tests/test_mpi_integration.py +++ b/tests/test_mpi_integration.py @@ -1,156 +1,67 @@ -"""MPI integration tests using the subprocess runner. - -These tests spawn actual MPI processes to verify end-to-end distributed execution. -Each configuration is run once and results are reused across assertions. -""" +"""MPI integration tests - spawn actual MPI processes via run_solver.""" import numpy as np import pytest from Poisson import run_solver -# ============================================================================= -# Test Configuration -# ============================================================================= - -RANK_CONFIGS = [ - (1, "sliced"), - (2, "sliced"), - (4, "sliced"), - (8, "cubic"), -] - -CONVERGENCE_GRID_SIZES = [15, 25, 45] - -COMMUNICATORS = ["numpy", "custom"] - -DECOMPOSITIONS = ["sliced", "cubic"] - - -# ============================================================================= -# Fixtures - run each config once, reuse results -# ============================================================================= - -@pytest.fixture(scope="module") -def rank_results(): - """Run solver for different rank/strategy configs.""" - results = {} - for n_ranks, strategy in RANK_CONFIGS: - result = run_solver(N=25, n_ranks=n_ranks, strategy=strategy, max_iter=10000, tol=1e-8, validate=True) - results[(n_ranks, strategy)] = result - return results - - -@pytest.fixture(scope="module") -def convergence_results(): - """Run solver at multiple grid sizes to test O(h²) convergence.""" - results = {} - for N in CONVERGENCE_GRID_SIZES: - result = run_solver(N=N, n_ranks=2, strategy="sliced", max_iter=50000, tol=1e-10, validate=True) - results[N] = result - return results - - +# Run solver once per config, reuse results @pytest.fixture(scope="module") -def communicator_results(): - """Run solver with different communicators.""" - results = {} - for comm_type in COMMUNICATORS: - result = run_solver(N=20, n_ranks=2, strategy="sliced", communicator=comm_type, max_iter=500, tol=0.0, validate=True) - results[comm_type] = result - return results - - -@pytest.fixture(scope="module") -def decomposition_results(): - """Run solver with sliced vs cubic decomposition.""" +def mpi_results(): + """Run all MPI configurations once.""" return { - strategy: run_solver(N=24, n_ranks=8, strategy=strategy, max_iter=5000, tol=1e-8, validate=True) - for strategy in DECOMPOSITIONS + (2, "sliced"): run_solver( + N=25, n_ranks=2, strategy="sliced", max_iter=10000, tol=1e-8, validate=True + ), + (4, "sliced"): run_solver( + N=25, n_ranks=4, strategy="sliced", max_iter=10000, tol=1e-8, validate=True + ), + (8, "cubic"): run_solver( + N=25, n_ranks=8, strategy="cubic", max_iter=10000, tol=1e-8, validate=True + ), + "numpy": run_solver( + N=20, n_ranks=2, communicator="numpy", max_iter=500, tol=0.0 + ), + "custom": run_solver( + N=20, n_ranks=2, communicator="custom", max_iter=500, tol=0.0 + ), + "convergence": { + N: run_solver(N=N, n_ranks=2, max_iter=20000, tol=1e-8, validate=True) + for N in [15, 25] + }, } -# ============================================================================= -# Tests - assertions on cached results -# ============================================================================= - -class TestMPIExecution: - """Tests for multi-rank MPI execution.""" - - @pytest.mark.parametrize("n_ranks,strategy", RANK_CONFIGS) - def test_no_errors(self, rank_results, n_ranks, strategy): - """Solver should complete without errors.""" - result = rank_results[(n_ranks, strategy)] - assert "error" not in result, f"Solver failed: {result.get('error')}" - - @pytest.mark.parametrize("n_ranks,strategy", RANK_CONFIGS) - def test_converged(self, rank_results, n_ranks, strategy): - """Solver should converge.""" - result = rank_results[(n_ranks, strategy)] - assert result["converged"], f"Did not converge in {result['iterations']} iterations" - - -class TestMPIAccuracy: - """Tests for solution accuracy across ranks.""" - - @pytest.mark.parametrize("n_ranks,strategy", RANK_CONFIGS) - def test_accuracy(self, rank_results, n_ranks, strategy): - """Solution should match analytical solution.""" - result = rank_results[(n_ranks, strategy)] - assert result["final_error"] < 0.1, f"L2 error {result['final_error']} too large" - - def test_ranks_produce_consistent_error(self, rank_results): - """Different rank counts should produce similar errors.""" - sliced_configs = [(n, s) for n, s in RANK_CONFIGS if s == "sliced"] - errors = [rank_results[cfg]["final_error"] for cfg in sliced_configs] - assert max(errors) / min(errors) < 1.1, f"Errors diverge: {errors}" - - def test_convergence_order(self, convergence_results): - """MPI solver should exhibit O(h²) convergence.""" - # Check all runs succeeded - for N in CONVERGENCE_GRID_SIZES: - assert "error" not in convergence_results[N], f"N={N} failed: {convergence_results[N].get('error')}" - - errors = [convergence_results[N]["final_error"] for N in CONVERGENCE_GRID_SIZES] - h = [2.0 / (N - 1) for N in CONVERGENCE_GRID_SIZES] - - # Compute convergence order: error ~ h^p => log(e1/e2) / log(h1/h2) = p - orders = [] - for i in range(len(errors) - 1): - order = np.log(errors[i] / errors[i + 1]) / np.log(h[i] / h[i + 1]) - orders.append(order) - - avg_order = np.mean(orders) - assert 1.8 < avg_order < 2.5, f"Expected O(h²), got order {avg_order:.2f} (orders: {orders})" +@pytest.mark.parametrize("config", [(2, "sliced"), (4, "sliced"), (8, "cubic")]) +def test_mpi_runs_and_converges(mpi_results, config): + """MPI solver should run without errors and converge.""" + r = mpi_results[config] + assert "error" not in r, f"Failed: {r.get('error')}" + assert r["converged"] + assert r["final_error"] < 0.1 -class TestCommunicators: - """Tests for different halo exchange implementations.""" +def test_ranks_produce_consistent_error(mpi_results): + """Different rank counts should produce similar errors.""" + errors = [mpi_results[(n, "sliced")]["final_error"] for n in [2, 4]] + assert max(errors) / min(errors) < 1.1 - @pytest.mark.parametrize("communicator", COMMUNICATORS) - def test_no_errors(self, communicator_results, communicator): - """Communicator should work without errors.""" - result = communicator_results[communicator] - assert "error" not in result, f"Solver failed: {result.get('error')}" - def test_produce_same_iterations(self, communicator_results): - """Both communicators should produce identical iteration counts.""" - iters = [communicator_results[c]["iterations"] for c in COMMUNICATORS] - assert len(set(iters)) == 1, f"Iteration counts differ: {dict(zip(COMMUNICATORS, iters))}" +def test_convergence_order(mpi_results): + """Should exhibit O(h²) convergence.""" + conv = mpi_results["convergence"] + errors = [conv[N]["final_error"] for N in [15, 25]] + h = [2.0 / (N - 1) for N in [15, 25]] + order = np.log(errors[0] / errors[1]) / np.log(h[0] / h[1]) + assert 1.8 < order < 2.5, f"Expected ~2, got {order:.2f}" -class TestDecompositionStrategies: - """Tests for different decomposition strategies.""" +@pytest.mark.parametrize("comm", ["numpy", "custom"]) +def test_communicators_work(mpi_results, comm): + """Both communicators should work.""" + assert "error" not in mpi_results[comm] - @pytest.mark.parametrize("strategy", DECOMPOSITIONS) - def test_no_errors(self, decomposition_results, strategy): - """Decomposition should work without errors.""" - result = decomposition_results[strategy] - assert "error" not in result, f"Solver failed: {result.get('error')}" - assert result["decomposition"] == strategy - def test_same_accuracy(self, decomposition_results): - """Decomposition strategies should produce similar accuracy.""" - errors = [decomposition_results[s]["final_error"] for s in DECOMPOSITIONS] - ratio = max(errors) / min(errors) - assert ratio < 1.2, f"Errors differ: {dict(zip(DECOMPOSITIONS, errors))}" +def test_communicators_same_iterations(mpi_results): + """Communicators should produce identical iteration counts.""" + assert mpi_results["numpy"]["iterations"] == mpi_results["custom"]["iterations"] diff --git a/tests/test_problems.py b/tests/test_problems.py deleted file mode 100644 index 4a881f2..0000000 --- a/tests/test_problems.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Tests for problem setup utilities.""" - -import numpy as np -import pytest -from Poisson import ( - create_grid_3d, - sinusoidal_exact_solution, - sinusoidal_source_term, - setup_sinusoidal_problem, -) - - -class TestGridCreation: - """Tests for grid creation utilities.""" - - def test_grid_shape(self): - """Grid should have correct shape.""" - N = 10 - u = create_grid_3d(N) - - assert u.shape == (N, N, N) - - def test_grid_default_values(self): - """Grid should have specified interior value and zero boundaries.""" - N = 10 - u = create_grid_3d(N, value=1.0, boundary_value=0.0) - - # Interior should be 1.0 - assert np.all(u[1:-1, 1:-1, 1:-1] == 1.0) - # Boundaries should be 0.0 - assert np.all(u[0, :, :] == 0.0) - assert np.all(u[-1, :, :] == 0.0) - assert np.all(u[:, 0, :] == 0.0) - assert np.all(u[:, -1, :] == 0.0) - assert np.all(u[:, :, 0] == 0.0) - assert np.all(u[:, :, -1] == 0.0) - - def test_grid_dtype(self): - """Grid should be float64.""" - N = 10 - u = create_grid_3d(N) - assert u.dtype == np.float64 - - -class TestSinusoidalSolution: - """Tests for sinusoidal test problem.""" - - def test_exact_solution_shape(self): - """Exact solution should have correct shape.""" - N = 15 - u_exact = sinusoidal_exact_solution(N) - - assert u_exact.shape == (N, N, N) - - def test_exact_solution_boundaries_zero(self): - """Exact solution should be zero on boundaries.""" - N = 15 - u = sinusoidal_exact_solution(N) - - # sin(pi * x) = 0 at x = -1 and x = 1 - assert np.allclose(u[0, :, :], 0.0, atol=1e-10) - assert np.allclose(u[-1, :, :], 0.0, atol=1e-10) - assert np.allclose(u[:, 0, :], 0.0, atol=1e-10) - assert np.allclose(u[:, -1, :], 0.0, atol=1e-10) - assert np.allclose(u[:, :, 0], 0.0, atol=1e-10) - assert np.allclose(u[:, :, -1], 0.0, atol=1e-10) - - def test_exact_solution_symmetry(self): - """Solution should be symmetric about origin.""" - N = 21 # Odd for center point - u = sinusoidal_exact_solution(N) - mid = N // 2 - - # Check point symmetry - assert np.isclose(u[mid, mid, mid], u[mid, mid, mid]) # Center - assert np.isclose(u[mid-1, mid, mid], u[mid+1, mid, mid], rtol=1e-10) - - def test_source_term_shape(self): - """Source term should have correct shape.""" - N = 15 - f = sinusoidal_source_term(N) - - assert f.shape == (N, N, N) - - def test_source_term_consistent(self): - """Source term should be -3*pi^2 * u_exact for Poisson equation.""" - N = 15 - u_exact = sinusoidal_exact_solution(N) - f = sinusoidal_source_term(N) - - # f = 3*pi^2 * sin(pi*x)*sin(pi*y)*sin(pi*z) = 3*pi^2 * u_exact - expected = 3 * np.pi**2 * u_exact - assert np.allclose(f, expected, rtol=1e-10) - - -class TestProblemSetup: - """Tests for complete problem setup.""" - - def test_setup_returns_four_values(self): - """setup_sinusoidal_problem should return (u1, u2, f, h).""" - result = setup_sinusoidal_problem(N=15) - - assert len(result) == 4 - - def test_setup_array_shapes(self): - """Setup arrays should have correct shapes.""" - N = 15 - u1, u2, f, h = setup_sinusoidal_problem(N) - - assert u1.shape == (N, N, N) - assert u2.shape == (N, N, N) - assert f.shape == (N, N, N) - - def test_setup_grid_spacing(self): - """Grid spacing h should be 2/(N-1).""" - N = 11 - _, _, _, h = setup_sinusoidal_problem(N) - - expected_h = 2.0 / (N - 1) - assert np.isclose(h, expected_h) - - def test_setup_initial_zero(self): - """Initial guess should be zero.""" - N = 15 - u1, u2, _, _ = setup_sinusoidal_problem(N) - - assert np.all(u1 == 0.0) - assert np.all(u2 == 0.0) - - def test_setup_boundary_conditions(self): - """Initial arrays should have zero boundaries (Dirichlet BC).""" - N = 15 - u1, u2, f, _ = setup_sinusoidal_problem(N) - - # Check u1 boundaries - assert np.all(u1[0, :, :] == 0.0) - assert np.all(u1[-1, :, :] == 0.0) - assert np.all(u1[:, 0, :] == 0.0) - assert np.all(u1[:, -1, :] == 0.0) - assert np.all(u1[:, :, 0] == 0.0) - assert np.all(u1[:, :, -1] == 0.0) diff --git a/tests/test_solver.py b/tests/test_solver.py index 003aa61..b286d8e 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -2,121 +2,52 @@ import numpy as np import pytest -from Poisson import JacobiPoisson, sinusoidal_exact_solution +from Poisson import JacobiPoisson -class TestSequentialSolver: - """Tests for single-rank (sequential) solver execution.""" +@pytest.fixture +def solver(): + """Basic solver for quick tests.""" + s = JacobiPoisson(N=15, omega=1.0, max_iter=100, tolerance=0.0) + s.solve() + return s - def test_solver_converges(self): - """Solver should converge within max iterations.""" - solver = JacobiPoisson(N=15, omega=1.0, max_iter=5000, tolerance=1e-6) - solver.solve() - assert solver.results.converged or solver.results.iterations < 5000 +@pytest.fixture +def converged_solver(): + """Solver run to convergence.""" + s = JacobiPoisson(N=20, omega=1.0, max_iter=10000, tolerance=1e-8) + s.solve() + return s - def test_solver_reduces_residual(self): - """Residual should decrease over iterations.""" - solver = JacobiPoisson(N=15, omega=1.0, max_iter=100, tolerance=0.0) - solver.solve() - - residuals = solver.timeseries.residual_history - assert len(residuals) == 100 - assert residuals[-1] < residuals[0] # Residual decreased - def test_solver_with_numba(self): - """Solver should work with Numba kernel.""" - solver = JacobiPoisson(N=15, omega=1.0, max_iter=100, tolerance=0.0, use_numba=True) - solver.warmup() - solver.solve() +class TestSolverBasics: + """Core solver functionality.""" - assert solver.results.iterations == 100 + def test_converges(self, converged_solver): + """Solver should converge and have low error.""" + assert converged_solver.results.converged + assert converged_solver.compute_l2_error() < 0.1 - def test_solver_timing_recorded(self): - """Solver should record timing data.""" - solver = JacobiPoisson(N=15, omega=1.0, max_iter=50, tolerance=0.0) - solver.solve() + def test_reduces_residual(self, solver): + """Residual should decrease over iterations.""" + r = solver.timeseries.residual_history + assert r[-1] < r[0] - assert len(solver.timeseries.compute_times) == 50 - assert len(solver.timeseries.halo_exchange_times) == 50 + def test_timing_recorded(self, solver): + """Should record timing data.""" + assert len(solver.timeseries.compute_times) == 100 assert all(t >= 0 for t in solver.timeseries.compute_times) - def test_solver_solution_accuracy(self): - """Converged solution should match analytical solution.""" - N = 20 - solver = JacobiPoisson(N=N, omega=1.0, max_iter=10000, tolerance=1e-8) - solver.solve() - - # Compute L2 error - error = solver.compute_l2_error() - - # Error should be reasonable for this grid size - assert error < 0.1, f"L2 error {error} too large" - - -class TestSolverConfiguration: - """Tests for solver configuration options.""" - - def test_omega_affects_convergence(self): - """Different omega values should affect convergence rate.""" - results = {} - for omega in [0.5, 1.0]: - solver = JacobiPoisson(N=15, omega=omega, max_iter=200, tolerance=1e-6) - solver.solve() - results[omega] = solver.timeseries.residual_history[-1] - - # Both should reduce residual (exact comparison depends on problem) - assert all(r < 1.0 for r in results.values()) - - def test_larger_grid_more_iterations(self): - """Larger grids generally need more iterations for same tolerance.""" - iters = {} - for N in [10, 20]: - solver = JacobiPoisson(N=N, omega=1.0, max_iter=50000, tolerance=1e-4) - solver.solve() - iters[N] = solver.results.iterations - - # Larger grid should need more iterations (or hit max) - assert iters[20] >= iters[10] - - def test_invalid_N_raises(self): - """Very small N should still work (edge case).""" - solver = JacobiPoisson(N=5, omega=1.0, max_iter=10, tolerance=0.0) - solver.solve() # Should not crash - assert solver.results.iterations == 10 - - -class TestSolverDataStructures: - """Tests for solver data structure handling.""" - - def test_u_global_shape(self): - """Global solution should have correct shape.""" - N = 15 - solver = JacobiPoisson(N=N, omega=1.0, max_iter=10, tolerance=0.0) - solver.solve() - - assert hasattr(solver, 'u_global') - assert solver.u_global.shape == (N, N, N) - - def test_boundary_conditions_preserved(self): - """Dirichlet BCs (zero) should be preserved on boundaries.""" - N = 15 - solver = JacobiPoisson(N=N, omega=1.0, max_iter=100, tolerance=0.0) - solver.solve() - + def test_boundary_conditions(self, solver): + """Dirichlet BCs (zero) should be preserved.""" u = solver.u_global - # All boundaries should be zero (Dirichlet BC) - assert np.allclose(u[0, :, :], 0.0) - assert np.allclose(u[-1, :, :], 0.0) - assert np.allclose(u[:, 0, :], 0.0) - assert np.allclose(u[:, -1, :], 0.0) - assert np.allclose(u[:, :, 0], 0.0) - assert np.allclose(u[:, :, -1], 0.0) - - def test_results_populated(self): - """Results dataclass should be populated after solve.""" - solver = JacobiPoisson(N=10, omega=1.0, max_iter=50, tolerance=0.0) - solver.solve() - - assert solver.results.iterations == 50 - assert isinstance(solver.results.converged, bool) + for face in [u[0], u[-1], u[:, 0], u[:, -1], u[:, :, 0], u[:, :, -1]]: + assert np.allclose(face, 0.0) + + def test_numba_kernel(self): + """Numba kernel should work.""" + s = JacobiPoisson(N=15, omega=1.0, max_iter=50, tolerance=0.0, use_numba=True) + s.warmup() + s.solve() + assert s.results.iterations == 50 From fee2c23f99a240ee62898f1cf786cd56ab463e61 Mon Sep 17 00:00:00 2001 From: Philip Korsager Nickel Date: Fri, 28 Nov 2025 16:38:02 +0100 Subject: [PATCH 2/2] readme link update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2ab08e..86b6ab9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A modular framework for studying parallel performance of 3D Poisson equation sol ## Documentation -📖 **[View Full Documentation](https://lsm-p2.readthedocs.io/)** +📖 **[View Full Documentation](https://pn-coursework.github.io/LSM-P2/)** For local documentation, see [Building Documentation](#building-documentation) below. ## Quick Start