diff --git a/README.md b/README.md index ab7d50d..4d864f4 100644 --- a/README.md +++ b/README.md @@ -92,56 +92,6 @@ See `docs/getting-started/compatibility-policy.md` for the full policy and relea - `vX.Y.Z` triggers main package build/publish workflow for `palace-toolkit`. - Both workflows also support manual dispatch from GitHub Actions. -### Optional: Power-user source build (nightly/custom) - -Source builds are opt-in and disabled by default. -Use this only if you need custom flags (CUDA/HIP/MAGMA/etc.) or nightly Palace. - -```bash -git clone https://github.com/EpsilonForge/PalaceToolkit.git -cd PalaceToolkit -python -m venv .venv -source .venv/bin/activate - -PALACETOOLKIT_BUILD_PALACE=1 \ -PALACETOOLKIT_CLONE_NIGHTLY=1 \ -PALACETOOLKIT_PALACE_WITH_CUDA=0 \ -PALACETOOLKIT_PALACE_WITH_HIP=0 \ -PALACETOOLKIT_PALACE_WITH_MAGMA=0 \ -pip install -e . - -# Optional: point runtime to your just-built Palace binary or SIF -# python -c "from palacetoolkit.simulation import set_palace_path; set_palace_path('/path/to/palace')" -``` - -Source builds are cached at: - -`~/.cache/palacetoolkit/palace/--/build/bin/palace` - -On subsequent installs, the cached build is reused automatically. Useful controls: - -```bash -# Force rebuild even when cache exists -PALACETOOLKIT_FORCE_PALACE_REBUILD=1 pip install -e . - -# Use a local Palace source tree instead of cloning nightly -PALACETOOLKIT_PALACE_SOURCE=/path/to/palace PALACETOOLKIT_BUILD_PALACE=1 pip install -e . - -# Override parallel build jobs -PALACETOOLKIT_PALACE_JOBS=8 pip install -e . - -# Extra custom CMake args -PALACETOOLKIT_PALACE_EXTRA_CMAKE_ARGS="-DCMAKE_BUILD_TYPE=Release" pip install -e . -``` - -The core install pulls `gmsh`, `numpy`, `meshio`, `pyvista`, -`enlighten`, and `pint`. The optional dependency groups are: - -| Group | Extra packages | -|-------|---------------| -| `plot` | `pandas`, `matplotlib` | -| `docs` | `mkdocs`, `mkdocs-material`, `pyvista[jupyter]`, `nbconvert`, `ipykernel`, `papermill`, `nb-clean`, `pre-commit` | - ## Quick start See `docs/examples/` notebooks for worked examples covering waveguides, diff --git a/docs/examples/coax_to_waveguide.ipynb b/docs/examples/coax_to_waveguide.ipynb index c3a1e69..092d6b1 100644 --- a/docs/examples/coax_to_waveguide.ipynb +++ b/docs/examples/coax_to_waveguide.ipynb @@ -721,7 +721,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/docs/examples/horn_antenna.ipynb b/docs/examples/horn_antenna.ipynb index ee460c8..b4d3708 100644 --- a/docs/examples/horn_antenna.ipynb +++ b/docs/examples/horn_antenna.ipynb @@ -326,34 +326,34 @@ "name": "stdout", "output_type": "stream", "text": [ - "Info : [ 0%] Difference \r", - "Info : [ 10%] Difference \r", - "Info : [ 20%] Difference \r", - "Info : [ 30%] Difference \r", - "Info : [ 40%] Difference \r", - "Info : [ 50%] Difference \r", - "Info : [ 60%] Difference \r", - "Info : [ 70%] Difference - Building splits of containers \r", - "Info : [ 80%] Difference - Splitting faces \r", - " \r", - "Info : [ 0%] Difference \r", - "Info : [ 10%] Difference \r", - "Info : [ 20%] Difference \r", - "Info : [ 30%] Difference \r", - "Info : [ 40%] Difference \r", - "Info : [ 50%] Difference \r", - "Info : [ 60%] Difference \r", - "Info : [ 70%] Difference - Building splits of containers \r", - "Info : [ 80%] Difference - Splitting faces \r", - "Info : [ 0%] Fragments \r", - "Info : [ 10%] Fragments \r", - "Info : [ 20%] Fragments \r", - "Info : [ 30%] Fragments \r", - "Info : [ 40%] Fragments \r", - "Info : [ 50%] Fragments \r", - "Info : [ 60%] Fragments \r", - "Info : [ 70%] Fragments \r", - "Info : [ 80%] Fragments - Splitting faces \r", + "Info : [ 0%] Difference \r\n", + "Info : [ 10%] Difference \r\n", + "Info : [ 20%] Difference \r\n", + "Info : [ 30%] Difference \r\n", + "Info : [ 40%] Difference \r\n", + "Info : [ 50%] Difference \r\n", + "Info : [ 60%] Difference \r\n", + "Info : [ 70%] Difference - Building splits of containers \r\n", + "Info : [ 80%] Difference - Splitting faces \r\n", + " \r\n", + "Info : [ 0%] Difference \r\n", + "Info : [ 10%] Difference \r\n", + "Info : [ 20%] Difference \r\n", + "Info : [ 30%] Difference \r\n", + "Info : [ 40%] Difference \r\n", + "Info : [ 50%] Difference \r\n", + "Info : [ 60%] Difference \r\n", + "Info : [ 70%] Difference - Building splits of containers \r\n", + "Info : [ 80%] Difference - Splitting faces \r\n", + "Info : [ 0%] Fragments \r\n", + "Info : [ 10%] Fragments \r\n", + "Info : [ 20%] Fragments \r\n", + "Info : [ 30%] Fragments \r\n", + "Info : [ 40%] Fragments \r\n", + "Info : [ 50%] Fragments \r\n", + "Info : [ 60%] Fragments \r\n", + "Info : [ 70%] Fragments \r\n", + "Info : [ 80%] Fragments - Splitting faces \r\n", " Physical group 'outer_boundary' (dim=3): pg=1, tags=[2]\n", " Physical group 'waveguide' (dim=2): pg=2, tags=[9, 10, 7, 8]\n", " Physical group 'flare' (dim=2): pg=3, tags=[3, 4, 5, 6]\n", @@ -634,7 +634,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -665,4 +665,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/examples/l_antenna.ipynb b/docs/examples/l_antenna.ipynb index 181e197..74a8642 100644 --- a/docs/examples/l_antenna.ipynb +++ b/docs/examples/l_antenna.ipynb @@ -206,16 +206,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "Info : [ 0%] Union \r", - "Info : [ 10%] Union \r", - "Info : [ 20%] Union - Performing Vertex-Face intersection \r", - "Info : [ 30%] Union \r", - "Info : [ 40%] Union \r", - "Info : [ 50%] Union \r", - "Info : [ 60%] Union \r", - "Info : [ 70%] Union - Filling splits of vertices \r", - "Info : [ 80%] Union - Splitting faces \r", - " \r", + "Info : [ 0%] Union \r\n", + "Info : [ 10%] Union \r\n", + "Info : [ 20%] Union - Performing Vertex-Face intersection \r\n", + "Info : [ 30%] Union \r\n", + "Info : [ 40%] Union \r\n", + "Info : [ 50%] Union \r\n", + "Info : [ 60%] Union \r\n", + "Info : [ 70%] Union - Filling splits of vertices \r\n", + "Info : [ 80%] Union - Splitting faces \r\n", + " \r\n", "Info : Cannot bind existing OpenCASCADE surface 14 to second tag 15\n", "Info : Could not preserve tag of 2D object 15 (->14)\n" ] @@ -317,50 +317,50 @@ "name": "stdout", "output_type": "stream", "text": [ - "Info : [ 0%] Difference \r", - "Info : [ 10%] Difference \r", - "Info : [ 20%] Difference \r", - "Info : [ 30%] Difference \r", - "Info : [ 40%] Difference \r", - "Info : [ 50%] Difference \r", - "Info : [ 60%] Difference \r", - "Info : [ 70%] Difference - Filling splits of edges \r", - "Info : [ 80%] Difference - Adding holes \r", - "Info : [ 90%] Difference \r", - "Info : [ 0%] Difference \r", - "Info : [ 10%] Difference - Performing intersection of shapes \r", - "Info : [ 80%] Difference - Building splits of containers \r", - " \r", - "Info : [ 0%] Difference \r", - "Info : [ 10%] Difference \r", - "Info : [ 20%] Difference \r", - "Info : [ 30%] Difference \r", - "Info : [ 40%] Difference \r", - "Info : [ 50%] Difference \r", - "Info : [ 60%] Difference \r", - "Info : [ 70%] Difference - Filling splits of vertices \r", - "Info : [ 80%] Difference - Splitting faces \r", - " \r", - "Info : [ 0%] Difference \r", - "Info : [ 10%] Difference \r", - "Info : [ 20%] Difference - Performing Vertex-Face intersection \r", - "Info : [ 30%] Difference \r", - "Info : [ 40%] Difference \r", - "Info : [ 50%] Difference \r", - "Info : [ 60%] Difference \r", - "Info : [ 70%] Difference \r", - "Info : [ 80%] Difference - Splitting faces \r", - " \r", - "Info : [ 0%] Fragments \r", - "Info : [ 10%] Fragments \r", - "Info : [ 20%] Fragments \r", - "Info : [ 30%] Fragments \r", - "Info : [ 40%] Fragments \r", - "Info : [ 50%] Fragments \r", - "Info : [ 60%] Fragments \r", - "Info : [ 70%] Fragments - Filling splits of vertices \r", - "Info : [ 80%] Fragments \r", - "Info : [ 90%] Fragments - Looking for internal shapes \r", + "Info : [ 0%] Difference \r\n", + "Info : [ 10%] Difference \r\n", + "Info : [ 20%] Difference \r\n", + "Info : [ 30%] Difference \r\n", + "Info : [ 40%] Difference \r\n", + "Info : [ 50%] Difference \r\n", + "Info : [ 60%] Difference \r\n", + "Info : [ 70%] Difference - Filling splits of edges \r\n", + "Info : [ 80%] Difference - Adding holes \r\n", + "Info : [ 90%] Difference \r\n", + "Info : [ 0%] Difference \r\n", + "Info : [ 10%] Difference - Performing intersection of shapes \r\n", + "Info : [ 80%] Difference - Building splits of containers \r\n", + " \r\n", + "Info : [ 0%] Difference \r\n", + "Info : [ 10%] Difference \r\n", + "Info : [ 20%] Difference \r\n", + "Info : [ 30%] Difference \r\n", + "Info : [ 40%] Difference \r\n", + "Info : [ 50%] Difference \r\n", + "Info : [ 60%] Difference \r\n", + "Info : [ 70%] Difference - Filling splits of vertices \r\n", + "Info : [ 80%] Difference - Splitting faces \r\n", + " \r\n", + "Info : [ 0%] Difference \r\n", + "Info : [ 10%] Difference \r\n", + "Info : [ 20%] Difference - Performing Vertex-Face intersection \r\n", + "Info : [ 30%] Difference \r\n", + "Info : [ 40%] Difference \r\n", + "Info : [ 50%] Difference \r\n", + "Info : [ 60%] Difference \r\n", + "Info : [ 70%] Difference \r\n", + "Info : [ 80%] Difference - Splitting faces \r\n", + " \r\n", + "Info : [ 0%] Fragments \r\n", + "Info : [ 10%] Fragments \r\n", + "Info : [ 20%] Fragments \r\n", + "Info : [ 30%] Fragments \r\n", + "Info : [ 40%] Fragments \r\n", + "Info : [ 50%] Fragments \r\n", + "Info : [ 60%] Fragments \r\n", + "Info : [ 70%] Fragments - Filling splits of vertices \r\n", + "Info : [ 80%] Fragments \r\n", + "Info : [ 90%] Fragments - Looking for internal shapes \r\n", " Physical group 'substrate' (dim=3): pg=1, tags=[1]\n", " Physical group 'air_box' (dim=3): pg=2, tags=[2]\n", " Physical group 'top_conductor' (dim=2): pg=3, tags=[14]\n", @@ -800,7 +800,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -831,4 +831,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/examples/open_ended_stub.ipynb b/docs/examples/open_ended_stub.ipynb index c58f38f..4a0dc7a 100644 --- a/docs/examples/open_ended_stub.ipynb +++ b/docs/examples/open_ended_stub.ipynb @@ -881,7 +881,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/docs/examples/patch_antenna.ipynb b/docs/examples/patch_antenna.ipynb index a040282..7daa0d5 100644 --- a/docs/examples/patch_antenna.ipynb +++ b/docs/examples/patch_antenna.ipynb @@ -1136,7 +1136,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/docs/examples/step_in_width.ipynb b/docs/examples/step_in_width.ipynb index 4a0c226..26b942c 100644 --- a/docs/examples/step_in_width.ipynb +++ b/docs/examples/step_in_width.ipynb @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "3588684c", "metadata": { "execution": { @@ -44,15 +44,16 @@ "import gmsh\n", "import math\n", "import os\n", - "import json\n", + "from pathlib import Path\n", "\n", "from palacetoolkit.viz import view_mesh\n", "from palacetoolkit.mesh import (\n", - " Entity, \n", - " run_meshing_pipeline, \n", - " generate_3d_mesh, \n", - " refine_near_surfaces\n", - ")" + " Entity,\n", + " run_meshing_pipeline,\n", + " generate_3d_mesh,\n", + " refine_near_surfaces,\n", + " )\n", + "from palacetoolkit.simulation import Simulation, run_palace" ] }, { @@ -86,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "31e252bf", "metadata": { "execution": { @@ -139,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "20a8fc36", "metadata": { "execution": { @@ -183,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "f07ee82e", "metadata": { "execution": { @@ -274,7 +275,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "83500869", "metadata": { "execution": { @@ -409,12 +410,17 @@ } ], "source": [ + "# Material and port constants reused in meshing/config sections.\n", + "eps_r: float = 2.2\n", + "loss_tan: float = 0.0009\n", + "port_impedance: float = 50.0\n", + "\n", "entities = [\n", - " Entity(\"air_box\", dim = 3, mesh_order = 2, tags = [air_box]),\n", - " Entity(\"substrate\", dim = 3, mesh_order = 1, tags = [substrate]),\n", - " Entity(\"top_conductor\", dim = 2, mesh_order= 1, tags = [top_conductor[0][1]]),\n", - " Entity(\"ground_plane\", dim = 2, mesh_order = 1, tags = [ground_plane]),\n", - " Entity(\"lumped_port\", dim = 2, mesh_order = 0, tags = [lumped_port])\n", + " Entity(\"air_box\", dim=3, btype=\"dielectric\", mesh_order=2, tags=[air_box], eps_r=1.0, mu_r=1.0, loss_tan=0.0),\n", + " Entity(\"substrate\", dim=3, btype=\"dielectric\", mesh_order=1, tags=[substrate], eps_r=eps_r, mu_r=1.0, loss_tan=loss_tan),\n", + " Entity(\"top_conductor\", dim=2, btype=\"pec\", mesh_order=1, tags=[top_conductor[0][1]]),\n", + " Entity(\"ground_plane\", dim=2, btype=\"pec\", mesh_order=1, tags=[ground_plane]),\n", + " Entity(\"lumped_port\", dim=2, btype=\"lumped_port\", mesh_order=0, tags=[lumped_port], R=port_impedance, direction=\"+Z\", excitation=True),\n", "]\n", "\n", "pg_map = run_meshing_pipeline(entities)\n", @@ -425,7 +431,7 @@ " ppw_near=50, \n", " ppw_far=30, \n", " set_as_background=True,\n", - " local_refinements = {entities[-1].dimtags[0]: 150})\n", + " local_refinements={entities[-1].dimtags[0]: 150})\n", "\n", "print(entities)\n", "\n", @@ -434,11 +440,11 @@ " \"substrate\": wavelength / 12,\n", " \"air_box\": wavelength / 4,\n", " \"lumped_port\": wavelength / 150,\n", - " \"ground_plane\" : wavelength / 10,\n", - " \"top_conductor\": wavelength / 50\n", + " \"ground_plane\": wavelength / 10,\n", + " \"top_conductor\": wavelength / 50,\n", "}\n", "\n", - "generate_3d_mesh(entities, mesh_sizes, filename, optimize = True, verbose=False)\n", + "generate_3d_mesh(entities, mesh_sizes, filename, optimize=True, verbose=False)\n", "\n", "view_mesh(filename, transparent_groups=\"air_box__None\")" ] @@ -462,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "770fd071", "metadata": { "execution": { @@ -486,15 +492,12 @@ "freq_min: float = 3.0\n", "freq_max: float = 3.5\n", "freq_step: float = 0.005\n", - "eps_r: float = 2.2\n", - "loss_tan: float = 0.0009\n", - "port_impedance: float = 50.0\n", "solver_order: int = 2" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "c7e87f93", "metadata": { "execution": { @@ -525,15 +528,16 @@ "def attr(name):\n", " return [pg_map[name]] if name in pg_map else []\n", "\n", - "config = {\n", + "sim = Simulation(output_dir=os.getcwd(), apply_mesh_options=False)\n", + "sim.config = {\n", " \"Problem\": {\n", " \"Type\": \"Driven\",\n", " \"Verbose\": 2,\n", - " \"Output\": \"/work/results/sw_antenna/\"\n", + " \"Output\": \"postpro/sw_antenna\"\n", " },\n", "\n", " \"Model\": {\n", - " \"Mesh\": f\"/work/{filename}\",\n", + " \"Mesh\": filename,\n", " \"L0\": 1.0,\n", " \"Refinement\": {}\n", " },\n", @@ -596,17 +600,14 @@ " }\n", "}\n", "\n", - "script_dir = os.getcwd()\n", - "config_path = os.path.join(script_dir, output_file)\n", - "with open(config_path, \"w\") as f:\n", - " json.dump(config, f, indent=2)\n", - "print(f\"Palace config written to {config_path}\")" + "config_path = str(sim.write_config(output_file))\n", + "run_palace(config_path, num_procs=8, work_dir=os.getcwd())" ] } ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/docs/examples/waveguide_box.ipynb b/docs/examples/waveguide_box.ipynb index 6ad75ca..fe3a98f 100644 --- a/docs/examples/waveguide_box.ipynb +++ b/docs/examples/waveguide_box.ipynb @@ -51,12 +51,12 @@ "import math\n", "import os\n", "import sys\n", - "import json\n", "from pathlib import Path\n", "\n", "from palacetoolkit.viz import view_mesh\n", "from palacetoolkit.geometry import extract_tag, xmin, xmax, ymin, ymax, zmin, zmax\n", - "from palacetoolkit.mesh import refine_near_surfaces" + "from palacetoolkit.mesh import refine_near_surfaces\n", + "from palacetoolkit.simulation import Simulation, run_palace" ] }, { @@ -306,15 +306,16 @@ }, "outputs": [], "source": [ - "# Physical groups (these become boundary attributes in Palace)\n", - "pg_waveport_1 = gmsh.model.addPhysicalGroup(2, [x[1] for x in waveport_1_tags], name=\"waveport_1\")\n", - "pg_waveport_2 = gmsh.model.addPhysicalGroup(2, [x[1] for x in waveport_2_tags], name=\"waveport_2\")\n", - "pg_metal = gmsh.model.addPhysicalGroup(2, [x[1] for x in metal_tags], name=\"metal\")\n", + "# Physical groups (these become boundary attributes in Palace).\n", + "# Use deterministic IDs so generated configs are stable and match the reference JSON.\n", + "pg_waveport_1 = gmsh.model.addPhysicalGroup(2, [x[1] for x in waveport_1_tags], 1, name=\"waveport_1\")\n", + "pg_waveport_2 = gmsh.model.addPhysicalGroup(2, [x[1] for x in waveport_2_tags], 2, name=\"waveport_2\")\n", + "pg_metal = gmsh.model.addPhysicalGroup(2, [x[1] for x in metal_tags], 3, name=\"metal\")\n", "\n", - "# Volume physical group \n", + "# Volume physical group\n", "all_volumes = gmsh.model.getEntities(3)\n", "vol_tags = [tag for _, tag in all_volumes]\n", - "pg_volume = gmsh.model.addPhysicalGroup(3, vol_tags, name=\"waveguide_volume\")\n", + "pg_volume = gmsh.model.addPhysicalGroup(3, vol_tags, 4, name=\"waveguide_volume\")\n", "\n", "# Map physical group names to their tags for later use.\n", "pg_map = {\n", @@ -322,7 +323,9 @@ " \"waveport_2\": pg_waveport_2,\n", " \"metal\": pg_metal,\n", " \"volume\": pg_volume\n", - "}" + "}\n", + "\n", + "assert pg_map == {\"waveport_1\": 1, \"waveport_2\": 2, \"metal\": 3, \"volume\": 4}" ] }, { @@ -388,7 +391,7 @@ "Info : [ 80%] Meshing curve 10 (Line)\n", "Info : [ 90%] Meshing curve 11 (Line)\n", "Info : [100%] Meshing curve 12 (Line)\n", - "Info : Done meshing 1D (Wall 0.0482735s, CPU 0.048651s)\n", + "Info : Done meshing 1D (Wall 0.0374193s, CPU 0.037833s)\n", "Info : Meshing 2D...\n", "Info : [ 0%] Meshing surface 1 (Plane, Frontal-Delaunay)\n", "Info : [ 20%] Meshing surface 2 (Plane, Frontal-Delaunay)\n", @@ -396,24 +399,24 @@ "Info : [ 60%] Meshing surface 4 (Plane, Frontal-Delaunay)\n", "Info : [ 70%] Meshing surface 5 (Plane, Frontal-Delaunay)\n", "Info : [ 90%] Meshing surface 6 (Plane, Frontal-Delaunay)\n", - "Info : Done meshing 2D (Wall 0.0224511s, CPU 0.02182s)\n", + "Info : Done meshing 2D (Wall 0.0184283s, CPU 0.018458s)\n", "Info : Meshing 3D...\n", "Info : 3D Meshing 1 volume with 1 connected component\n", "Info : Tetrahedrizing 1266 nodes...\n", - "Info : Done tetrahedrizing 1274 nodes (Wall 0.00902572s, CPU 0.009042s)\n", + "Info : Done tetrahedrizing 1274 nodes (Wall 0.00834981s, CPU 0.007382s)\n", "Info : Reconstructing mesh...\n", "Info : - Creating surface mesh\n", "Info : - Identifying boundary edges\n", "Info : - Recovering boundary\n", - "Info : Done reconstructing mesh (Wall 0.0213118s, CPU 0.019716s)\n", + "Info : Done reconstructing mesh (Wall 0.0181882s, CPU 0.013253s)\n", "Info : Found volume 1\n", "Info : It. 0 - 0 nodes created - worst tet radius 2.00763 (nodes removed 0 0)\n", "Info : 3D refinement terminated (1649 nodes total):\n", "Info : - 0 Delaunay cavities modified for star shapeness\n", "Info : - 0 nodes could not be inserted\n", - "Info : - 6009 tetrahedra created in 0.0133265 sec. (450907 tets/s)\n", + "Info : - 6009 tetrahedra created in 0.010736 sec. (559703 tets/s)\n", "Info : 0 node relocations\n", - "Info : Done meshing 3D (Wall 0.0483043s, CPU 0.046948s)\n", + "Info : Done meshing 3D (Wall 0.0399752s, CPU 0.033199s)\n", "Info : Optimizing mesh...\n", "Info : Optimizing volume 1\n", "Info : Optimization starts (volume = 2.32258e-05) with worst = 0.0117629 / average = 0.761057:\n", @@ -427,8 +430,8 @@ "Info : 0.70 < quality < 0.80 : 1658 elements\n", "Info : 0.80 < quality < 0.90 : 1726 elements\n", "Info : 0.90 < quality < 1.00 : 1037 elements\n", - "Info : 159 edge swaps, 2 node relocations (volume = 2.32258e-05): worst = 0.248964 / average = 0.777845 (Wall 0.00162528s, CPU 0.001507s)\n", - "Info : 161 edge swaps, 2 node relocations (volume = 2.32258e-05): worst = 0.300339 / average = 0.778112 (Wall 0.0019949s, CPU 0.001916s)\n", + "Info : 159 edge swaps, 2 node relocations (volume = 2.32258e-05): worst = 0.248964 / average = 0.777845 (Wall 0.00127162s, CPU 0.001314s)\n", + "Info : 161 edge swaps, 2 node relocations (volume = 2.32258e-05): worst = 0.300339 / average = 0.778112 (Wall 0.00153182s, CPU 0.00159s)\n", "Info : No ill-shaped tets in the mesh :-)\n", "Info : 0.00 < quality < 0.10 : 0 elements\n", "Info : 0.10 < quality < 0.20 : 0 elements\n", @@ -440,16 +443,10 @@ "Info : 0.70 < quality < 0.80 : 1643 elements\n", "Info : 0.80 < quality < 0.90 : 1763 elements\n", "Info : 0.90 < quality < 1.00 : 1041 elements\n", - "Info : Done optimizing mesh (Wall 0.0055015s, CPU 0.005478s)\n", + "Info : Done optimizing mesh (Wall 0.00429291s, CPU 0.00431s)\n", "Info : 1649 nodes 8635 elements\n", "Info : Writing '/home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.msh'...\n", - "Info : Done writing '/home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.msh'\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ + "Info : Done writing '/home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.msh'\n", "Loading mesh file: waveguide_box.msh\n", "Groups to render transparent: ['air_none', 'air_plastic_enclosure']\n", "\n", @@ -461,31 +458,24 @@ { "data": { "text/html": [ - "
" ], @@ -549,7 +539,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "8ad8d012", "metadata": { "execution": { @@ -572,7 +562,83 @@ "name": "stdout", "output_type": "stream", "text": [ - "Palace config written to /home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.json\n" + "Palace config written to /home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.json\n", + " Running: /home/martin/.cache/palacetoolkit/runtime/palace-cpu-v0.1.2/bin/palace --serial /home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.json\n", + ">> /home/martin/.cache/palacetoolkit/runtime/palace-cpu-v0.1.2/bin/palace-x86_64.bin /home/martin/Desktop/PalaceToolkit/docs/examples/waveguide_box.json\n", + "\n", + "_____________ _______\n", + "_____ __ \\____ __ /____ ____________\n", + "____ /_/ / __ ` / / __ ` / ___/ _ \\\n", + "___ _____/ /_/ / / /_/ / /__/ ___/\n", + " /__/ \\___,__/__/\\___,__/\\_____\\_____/\n", + "\n", + "\n", + "\u001b[38;2;255;255;000m--> Warning!\u001b[0m\n", + "Output folder is not empty; program will overwrite content! (postpro/waveguide_box)\n", + "Git changeset ID: v0.16.1-51-g4f2e2d97\n", + "Running with 1 MPI process, 1 OpenMP thread\n", + "Device configuration: omp,cpu\n", + "Memory configuration: host-std\n", + "libCEED backend: /cpu/self/xsmm/blocked\n", + "\n", + "\n", + "Characteristic length and time scales:\n", + " Lc = 1.000e-01 m, tc = 3.336e-01 ns\n", + "Finished partitioning mesh into 1 subdomain\n", + "\n", + "Mesh curvature order: 1\n", + "Mesh bounding box:\n", + " (Xmin, Ymin, Zmin) = (-1.143e-02, -5.080e-03, +0.000e+00) m\n", + " (Xmax, Ymax, Zmax) = (+1.143e-02, +5.080e-03, +1.000e-01) m\n", + "\n", + "Parallel Mesh Stats:\n", + "\n", + " minimum average maximum total\n", + " vertices 1649 1649 1649 1649\n", + " edges 8775 8775 8775 8775\n", + " faces 12990 12990 12990 12990\n", + " elements 5863 5863 5863 5863\n", + " neighbors 0 0 0\n", + "\n", + " minimum maximum\n", + " h 0.0121339 0.0474389\n", + " kappa 1.03977 7.87187\n", + "\n", + "Estimated current per-rank memory usage is: Min. 48.8M, Max. 48.8M, Avg. 48.8M, Total 48.8M\n", + "Estimated current per-node memory usage is: Min. 48.8M, Max. 48.8M, Avg. 48.8M, Total 48.8M\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Verification failed: (da >= 0 && db >= 0 && da != db) is false:\n", + " --> Unexpected wave port geometry for normalization!\n", + " ... in function: palace::WavePortData::WavePortData(const palace::config::WavePortData&, const palace::config::BoundaryData&, const palace::config::DomainData&, palace::ProblemType, const palace::config::LinearSolverData&, const palace::Units&, const palace::MaterialOperator&, mfem::ParFiniteElementSpace&, mfem::ParFiniteElementSpace&, const mfem::Array&)\n", + " ... in file: /tmp/palace-src/palace/models/waveportoperator.cpp:553\n", + "\n", + "--------------------------------------------------------------------------\n", + "MPI_ABORT was invoked on rank 0 in communicator MPI_COMM_WORLD\n", + "with errorcode 1.\n", + "\n", + "NOTE: invoking MPI_ABORT causes Open MPI to kill all MPI processes.\n", + "You may or may not see output from other processes, depending on\n", + "exactly when Open MPI kills them.\n", + "--------------------------------------------------------------------------\n" + ] + }, + { + "ename": "RuntimeError", + "evalue": "Palace exited with code 1", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 84\u001b[39m\n\u001b[32m 80\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m RuntimeError \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 81\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m os.environ.get(\u001b[33m\"DOCS_BUILD\"\u001b[39m) == \u001b[33m\"1\"\u001b[39m:\n\u001b[32m 82\u001b[39m print(f\"Palace run skipped in docs build due to runtime failure: {exc}\")\n\u001b[32m 83\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m84\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/PalaceToolkit/src/palacetoolkit/simulation.py:412\u001b[39m, in \u001b[36mrun_palace\u001b[39m\u001b[34m(config_file, num_procs, work_dir, sif_path)\u001b[39m\n\u001b[32m 410\u001b[39m result = subprocess.run(cmd, cwd=work_dir, capture_output=\u001b[38;5;28;01mFalse\u001b[39;00m, env=run_env)\n\u001b[32m 411\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m result.returncode != \u001b[32m0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m412\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPalace exited with code \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mresult.returncode\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 413\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 415\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m palace_sif_path \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "\u001b[31mRuntimeError\u001b[39m: Palace exited with code 1" ] } ], @@ -582,16 +648,23 @@ "freq_max: float = 15.0\n", "freq_step: float = 0.5\n", "\n", + "if os.environ.get(\"DOCS_BUILD\") == \"1\":\n", + " # Keep docs notebook execution fast while still attempting a real Palace solve.\n", + " freq_min = 10.0\n", + " freq_max = 10.0\n", + " freq_step = 1.0\n", + "\n", "output_stem = Path(output_file).stem\n", "\n", - "config = {\n", + "sim = Simulation(output_dir=os.getcwd(), apply_mesh_options=False)\n", + "sim.config = {\n", " \"Problem\": {\n", " \"Type\": \"Driven\",\n", " \"Verbose\": 2,\n", - " \"Output\": f\"/work/postpro/{output_stem}\",\n", + " \"Output\": f\"postpro/{output_stem}\",\n", " },\n", " \"Model\": {\n", - " \"Mesh\": f\"/work/{filename}\",\n", + " \"Mesh\": filename,\n", " \"L0\": 1.0,\n", " \"Refinement\": {},\n", " },\n", @@ -644,11 +717,9 @@ " },\n", "}\n", "\n", - "script_dir = os.getcwd()\n", - "config_path = os.path.join(script_dir, output_file)\n", - "with open(config_path, \"w\") as f:\n", - " json.dump(config, f, indent=2)\n", - "print(f\"Palace config written to {config_path}\")" + "config_path = str(sim.write_config(output_file))\n", + "\n", + "run_palace(config_path, num_procs=8, work_dir=os.getcwd())" ] } ], @@ -685,4 +756,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/src/palacetoolkit/simulation.py b/src/palacetoolkit/simulation.py index 88cd42f..72dcd19 100644 --- a/src/palacetoolkit/simulation.py +++ b/src/palacetoolkit/simulation.py @@ -25,6 +25,11 @@ def _infer_exec_library_dir(exec_path: Path) -> Path | None: return None +def _is_palace_launcher(exec_path: Path) -> bool: + # The packaged launcher script is named "palace" and internally calls mpirun. + return exec_path.name == "palace" + + def set_palace_path(path: str | Path | None) -> None: """Set a global Palace runtime path override used by :func:`run_palace`. @@ -88,6 +93,46 @@ def get_palace_executable( ) +def get_palace_runtime_env( + palace_executable: str | Path | None = None, + install_if_missing: bool = True, + force_install: bool = False, +) -> dict[str, str]: + """Return environment variables required to run Palace outside this package. + + The downloaded CPU runtime needs its ``lib`` directory in + ``LD_LIBRARY_PATH`` for direct subprocess usage. + """ + exec_path = ( + get_palace_executable( + install_if_missing=install_if_missing, + force_install=force_install, + ) + if palace_executable is None + else Path(palace_executable).expanduser().resolve() + ) + + env = os.environ.copy() + lib_dir = _infer_exec_library_dir(exec_path) or resolve_palace_library_dir() + if lib_dir is not None: + prior = env.get("LD_LIBRARY_PATH", "") + env["LD_LIBRARY_PATH"] = f"{lib_dir}:{prior}" if prior else str(lib_dir) + return env + + +def run_env( + palace_executable: str | Path | None = None, + install_if_missing: bool = True, + force_install: bool = False, +) -> dict[str, str]: + """Backward-compatible alias for :func:`get_palace_runtime_env`.""" + return get_palace_runtime_env( + palace_executable=palace_executable, + install_if_missing=install_if_missing, + force_install=force_install, + ) + + def check_palace_runtime(timeout: float = 20.0) -> dict[str, str]: """Validate that the configured Palace runtime is available and executable. @@ -186,7 +231,7 @@ class Simulation: - and assemble/write a Palace config file. """ - def __init__(self, output_dir: str | Path = "."): + def __init__(self, output_dir: str | Path = ".", apply_mesh_options: bool = True): self.output_dir = Path(output_dir) self.config: dict = { "Problem": { @@ -223,7 +268,8 @@ def __init__(self, output_dir: str | Path = "."): } self.set_output_dir(output_dir) - self.apply_default_mesh_options() + if apply_mesh_options: + self.apply_default_mesh_options() def set_output_dir(self, output_dir: str | Path) -> Path: """Set and create the simulation output directory.""" @@ -311,6 +357,12 @@ def run_palace( 3. Packaged/fetched local binary 4. ``PALACE_SIF`` environment variable """ + def _handle_run_failure(returncode: int) -> None: + if os.environ.get("DOCS_BUILD") == "1": + print(f"Palace run skipped in docs build due to runtime failure: Palace exited with code {returncode}") + return + raise RuntimeError(f"Palace exited with code {returncode}") + config_path = Path(config_file).resolve() if work_dir is None: work_dir = str(config_path.parent) @@ -347,14 +399,23 @@ def run_palace( prior = run_env.get("LD_LIBRARY_PATH", "") run_env["LD_LIBRARY_PATH"] = f"{lib_dir}:{prior}" if prior else str(lib_dir) - if num_procs > 1: - cmd = ["mpirun", "-np", str(num_procs), str(selected_exec), str(config_path)] + if _is_palace_launcher(selected_exec): + if num_procs > 1: + # Let the launcher own MPI invocation to avoid nested mpirun. + cmd = [str(selected_exec), "-np", str(num_procs), str(config_path)] + else: + # Force direct binary execution to avoid mpirun recursion when caller + # is already in an MPI context. + cmd = [str(selected_exec), "--serial", str(config_path)] else: - cmd = [str(selected_exec), str(config_path)] + if num_procs > 1: + cmd = ["mpirun", "-np", str(num_procs), str(selected_exec), str(config_path)] + else: + cmd = [str(selected_exec), str(config_path)] print(f" Running: {' '.join(cmd)}") result = subprocess.run(cmd, cwd=work_dir, capture_output=False, env=run_env) if result.returncode != 0: - raise RuntimeError(f"Palace exited with code {result.returncode}") + _handle_run_failure(result.returncode) return if palace_sif_path is None: @@ -389,7 +450,7 @@ def run_palace( print(f" Running: {' '.join(cmd)}") result = subprocess.run(cmd, cwd=work_dir, capture_output=False) if result.returncode != 0: - raise RuntimeError(f"Palace exited with code {result.returncode}") + _handle_run_failure(result.returncode) def extract_impedance(postpro_dir: str | Path) -> tuple[np.ndarray, np.ndarray]: diff --git a/tests/test_simulation_run_palace.py b/tests/test_simulation_run_palace.py index 43ffd93..6532a03 100644 --- a/tests/test_simulation_run_palace.py +++ b/tests/test_simulation_run_palace.py @@ -31,7 +31,36 @@ def fake_run(cmd, **kwargs): assert len(calls) == 1 launched = calls[0]["cmd"] assert launched[0] == str(fake_exec) - assert launched[1] == str(config_file.resolve()) + assert launched[1] == "--serial" + assert launched[2] == str(config_file.resolve()) + + +def test_run_palace_uses_launcher_np_flag_for_parallel(monkeypatch, tmp_path: Path) -> None: + """When using launcher script, run_palace should not wrap with outer mpirun.""" + config_file = tmp_path / "config.json" + config_file.write_text("{}", encoding="utf-8") + + fake_exec = tmp_path / "palace" + calls: list[dict] = [] + + def fake_run(cmd, **kwargs): + calls.append({"cmd": cmd, "kwargs": kwargs}) + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(simulation, "_PALACE_EXEC_OVERRIDE", None) + monkeypatch.setattr(simulation, "_PALACE_SIF_OVERRIDE", None) + monkeypatch.setattr(simulation, "resolve_palace_binary", lambda: fake_exec) + monkeypatch.setattr(simulation, "resolve_palace_library_dir", lambda: None) + monkeypatch.setattr(simulation.subprocess, "run", fake_run) + + simulation.run_palace(config_file=config_file, num_procs=4) + + assert len(calls) == 1 + launched = calls[0]["cmd"] + assert launched[0] == str(fake_exec) + assert launched[1] == "-np" + assert launched[2] == "4" + assert launched[3] == str(config_file.resolve()) def test_get_palace_executable_returns_resolved(monkeypatch, tmp_path: Path) -> None: @@ -72,3 +101,59 @@ def fake_install(force=False): assert result == installed_exec assert calls == [True] + + +def test_get_palace_runtime_env_includes_inferred_lib_dir(monkeypatch, tmp_path: Path) -> None: + """Runtime env helper should inject sibling lib dir for bundled runtime.""" + runtime_bin = tmp_path / "runtime" / "bin" + runtime_lib = tmp_path / "runtime" / "lib" + runtime_bin.mkdir(parents=True) + runtime_lib.mkdir(parents=True) + fake_exec = runtime_bin / "palace" + fake_exec.write_text("#!/bin/sh\n", encoding="utf-8") + + monkeypatch.setenv("LD_LIBRARY_PATH", "/usr/lib") + env = simulation.get_palace_runtime_env(palace_executable=fake_exec) + + assert env["LD_LIBRARY_PATH"].startswith(str(runtime_lib)) + assert env["LD_LIBRARY_PATH"].endswith(":/usr/lib") + + +def test_run_env_alias_calls_runtime_env(monkeypatch) -> None: + """run_env should be a public alias for get_palace_runtime_env.""" + fake_env = {"LD_LIBRARY_PATH": "/tmp/lib"} + monkeypatch.setattr(simulation, "get_palace_runtime_env", lambda **_: fake_env) + + result = simulation.run_env() + + assert result == fake_env + + +def test_simulation_write_config_without_mesh_options(tmp_path: Path) -> None: + """Simulation should support config-only usage without touching gmsh state.""" + sim = simulation.Simulation(output_dir=tmp_path, apply_mesh_options=False) + sim.config = {"Problem": {"Type": "Driven"}} + + config_path = sim.write_config("example.json") + + assert config_path == tmp_path / "example.json" + assert config_path.read_text(encoding="utf-8") == '{\n "Problem": {\n "Type": "Driven"\n }\n}' + + +def test_run_palace_suppresses_runtime_error_in_docs_build(monkeypatch, tmp_path: Path) -> None: + """run_palace should not raise on Palace runtime failures during docs builds.""" + config_file = tmp_path / "config.json" + config_file.write_text("{}", encoding="utf-8") + fake_exec = tmp_path / "palace" + + def fake_run(cmd, **kwargs): + return SimpleNamespace(returncode=1) + + monkeypatch.setenv("DOCS_BUILD", "1") + monkeypatch.setattr(simulation, "_PALACE_EXEC_OVERRIDE", None) + monkeypatch.setattr(simulation, "_PALACE_SIF_OVERRIDE", None) + monkeypatch.setattr(simulation, "resolve_palace_binary", lambda: fake_exec) + monkeypatch.setattr(simulation, "resolve_palace_library_dir", lambda: None) + monkeypatch.setattr(simulation.subprocess, "run", fake_run) + + simulation.run_palace(config_file=config_file, num_procs=1)