Skip to main content
Glama
benjhardy

palace-tools

by benjhardy
README.md
# palace-agent-tools

[![tests](https://github.com/benjhardy/palace-agent-tools/actions/workflows/tests.yml/badge.svg)](https://github.com/benjhardy/palace-agent-tools/actions/workflows/tests.yml)

Part of agent.rf — the RF lab: https://benjhardy.github.io/agent-rf/

Tools that let AI coding agents (Claude Code, Codex, Cursor, or a plain script) set up, lint, run, check and
post-process [AWS Palace](https://github.com/awslabs/palace) finite-element electromagnetic simulations
reliably.

Palace is a capable open-source FEM solver, but most mistakes in a Palace model produce a plausible answer and
no error: a mesh read in micrometres, a boundary tag that Palace silently ignores, absorbing walls that fake a
loss, a solver choice that gives wrong points on some rank counts, S-parameters that are never written. This
package turns the lessons from a series of Palace projects (MRI birdcage coil, Wilkinson divider, lumped-filter
port tuning, an microstrip AMR study, a GDS-to-Palace benchmark, a mmWave PA) into tools an agent calls
with one JSON object and gets one JSON object back:

| step | tool | what it does |
|---|---|---|
| geometry, mesh | `mesh-template`, `palace_tools.mesh.RFModel` | Gmsh OCC parts (boxes, substrates, traces, vias, port rectangles, air box padded in wavelengths), physical-group conventions, wavelength and feature size fields, MSH 2.2 + a `*.tags.json` with group tags, geometry and a mesh report (quality, exact Nedelec unknowns per order) |
| config | `build-config`, `spec` | Palace JSON from a compact spec with names instead of tag numbers and safe defaults |
| lint | `lint-config`, `lint-rules`, `gotchas` | Palace 0.16 schema validation plus 44 rules from real failures, each with the lesson and its source |
| cost | `estimate` | peak RAM and wall time (quiet / typical / busy) calibrated on 190 recorded Palace 0.16 runs |
| run | `status`, `run`, `doctor` | WSL + Apptainer, Apptainer, Docker or native; refuses to oversubscribe a busy machine or run out of RAM; logs wall time and memory |
| results | `results` | port-S / eig / EPR / Q / energy / AMR / field summaries, Touchstone with the right reference impedances |
| check | `check` | passivity, reciprocity, energy balance, port symmetry, expected eigenmode, log errors, spurious wave-port modes, convergence |

It ships as a Python package with a CLI, an MCP server (stdio) and a Claude Code skill.

**Who it is for:** RF/microwave and EM engineers who use Palace with an AI agent, and agent builders who want
an EM solver step their agent cannot quietly get wrong.

**Status:** targets Palace 0.16 (the version the rules and cost data come from), with version switches for the
behaviour that changed in 0.17 and 0.18. Simulation-only examples; nothing here was measured.

## Install

```
python -m venv .venv
. .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install -e ".[all]"          # numpy + gmsh + scikit-rf + jsonschema;  ".[dev]" adds pytest
```

The core (config, lint, estimate, results, checks) needs only numpy. `gmsh` is needed for meshing, `jsonschema`
for schema validation, `scikit-rf` for Touchstone files. Palace itself is not installed by this package: use
the official container or a native build (see [Palace install](https://awslabs.github.io/palace/v0.16.0/install/)),
then tell the runner how to call it (`PALACE_TOOLS_BACKEND`, `PALACE_TOOLS_IMAGE`, `PALACE_TOOLS_CMD`,
`PALACE_TOOLS_DISTRO`; `palace-tools doctor` shows what it found).

## Quick start (human)

```
python examples/cavity_te101/run_example.py --ranks 2       # 10 s: eigenmodes vs analytic TE101 ...
python examples/lumped_port_line/run_example.py --ranks 2   # 1.5 min: lumped ports on an exact TEM line
python examples/microstrip_thru/run_example.py --ranks 2    # 2.5 min: microstrip Z0, eeff vs Hammerstad-Jensen
```

Or step by step with the CLI:

```
palace-tools mesh-template '{"template": "microstrip_line", "out": "case/line", "w": 1.137, "h": 0.508,
    "length": 20, "eps_r": 3.55, "side": 4.5, "top": 4.5, "h_mesh": 1.6, "h_edge": 0.12, "ports": "wave", "f_ghz": 10}'
palace-tools build-config @spec.json          # {"spec": {...}, "out": "case/config.json"}
palace-tools lint-config '{"config": "case/config.json", "geometry": "case/line.tags.json"}'
palace-tools estimate '{"config": "case/config.json", "tags": "case/line.tags.json", "ranks": 4}'
palace-tools status '{"ranks": 4}'
palace-tools run '{"case_dir": "case", "ranks": 4}'
palace-tools check '{"case_dir": "case", "symmetric_pairs": [[1, 2]]}'
palace-tools results '{"case_dir": "case", "touchstone": "case/line"}'
```

`palace-tools list` prints every tool. Arguments can come inline, from `@file.json`, or from stdin (`-`). Exit
code 1 means the tool reported a problem (`ok: false` or `overall: fail`), 2 means bad input.

## Quick start (agents)

**MCP (any MCP client).** The server is dependency-free JSON-RPC over stdio:

```
claude mcp add palace-tools -- python -m palace_tools.mcp_server
```

or the repository's `.mcp.json`. Tools: `palace_spec`, `palace_build_config`, `palace_lint_config`,
`palace_lint_rules`, `palace_gotchas`, `palace_mesh_template`, `palace_mesh_report`, `palace_estimate_cost`,
`palace_status`, `palace_run`, `palace_results`, `palace_check`, `palace_doctor`. Each has a JSON input schema;
results come back as JSON text and `structuredContent`.

**Claude Code skill.** Copy `skills/palace-simulation/` into `~/.claude/skills/` (or a project's
`.claude/skills/`). It teaches the workflow spec -> geometry -> mesh -> config -> lint -> cost estimate -> run
-> check -> report, with the gotchas, recipes, run rules, check meanings and version notes in `references/`.

**Other agents.** [AGENTS.md](AGENTS.md) has the same workflow and rules in plain form.

## The config spec

```json
{"problem": "driven", "mesh": "line.msh", "units": "mm", "tags": "line.tags.json", "order": 2,
 "materials": [{"tags": ["substrate"], "eps_r": 3.55, "tan_d": 0.0027}, {"tags": ["air"]}],
 "pec": ["ground", "strip"], "pmc": ["ends"],
 "lumped_ports": [{"index": 1, "tags": ["port1"], "direction": "+Z", "R": 50},
                  {"index": 2, "tags": ["port2"], "direction": "+Z", "R": 50}],
 "frequency": {"min_ghz": 0.5, "max_ghz": 10, "step_ghz": 0.25, "adaptive_tol": 1e-3}}
```

Also: `absorbing`, `conductivity`, `impedance` (give `R`, `length`, `width` and it sets Rs = R w / l),
`lumped_elements` (with `cut: half-width | half-length` for elements split by a symmetry plane), `wave_ports`,
`wave_port_pec`, `symmetry`, `eigen`, `amr`, `solver`, `probes`, `energy_domains`, `save_fields`, and `extra`
(raw Palace keys merged last). Defaults: MUMPS; a direct factorisation (`MGMaxLevels: 1`) for eigenmode and
for driven problems up to 600k unknowns; `ComplexCoarseSolve`; `EstimatorMG` + `EstimatorTol 1e-4` when AMR is
off; `Excitation` = port index; no ParaView output unless asked. `palace-tools spec` prints every key.

## Examples (Palace 0.16, 8-thread desktop, 2 MPI ranks unless noted)

| example | reference | result | size / cost |
|---|---|---|---|
| [`cavity_te101`](examples/cavity_te101): 22.86 x 10.16 x 30 mm PEC cavity, tan d = 1e-3 | analytic TE/TM resonances; exact dielectric Q | TE101 8.243934 vs 8.243877 GHz (+0.0007 %); first 5 modes within 0.02 %; Q = 1000.00 vs 1000.00 | 17.8k unknowns (order 2), 0.24 GB, 9 s |
| [`lumped_port_line`](examples/lumped_port_line) a: 75-ohm parallel-plate line (PMC sides), 50-ohm lumped ports | closed-form line S-matrix | max \|S - S_ref\| = 8.7e-7 over 0.5-10 GHz | 34.5k unknowns, 0.65 GB, 55 s |
| `lumped_port_line` b: same line terminated in a 75-ohm lumped element | \|S11\| = 0.2 (-13.979 dB), flat | -13.97940 to -13.97942 dB; also exposes the 0.16 unequal-R bug: \|S21\|^2 = 2/3 of the delivered power (= R1/R2), flagged by `check` | 23 s |
| [`microstrip_thru`](examples/microstrip_thru) (wave ports): 50-ohm line, eps_r 3.55, h 0.508 mm, 20 mm, PEC shield at 9h | Kirschning-Jansen eeff(f) | eeff within 0.4 % over 1-10 GHz; \|S11\| < -40 dB | 105k unknowns, 2.5 GB, 78 s on 4 ranks (104 s on 2) |
| `microstrip_thru` (edge lumped ports, 10 + 20 mm lines, two-line de-embedding) | Hammerstad-Jensen Z0 = 50.0 ohm; Kirschning-Jansen eeff | Z0 49.5 ohm median (48.6-51.7) over 0.5-10 GHz; eeff within 0.25 %; fitted port inductance 143 pH. Single-line extraction without de-embedding: eeff +5.6 %, Z0 40-60 ohm above 5 GHz | 70k + 109k unknowns, 1.5 + 2.4 GB, 45 + 84 s |

Every run passed the passivity, reciprocity, energy-balance, port-symmetry and log checks, except case b,
where the energy check flags the 0.16 bug, as it should. The examples write their summaries to
`results*.json` next to each script.

## Gotchas (what the lint rules encode)

All 44 rules with their full lessons: [skills/palace-simulation/references/gotchas.md](skills/palace-simulation/references/gotchas.md)
(generated from the code). The ones that cost the most time:

| gotcha | what happened | where it was learned | rule |
|---|---|---|---|
| `Linear.Type: "Default"` is SuperLU in the 0.16 container | partition-dependent wrong points (S21 ~ -45 dB at 2 of 21 frequencies on 6 ranks) and "singular matrix" aborts; MUMPS identical on any rank count | GDS-to-Palace benchmark | SOL001 |
| p-multigrid on an eigenproblem with lumped C | ~1 min per shift-and-invert solve; direct MUMPS (`MGMaxLevels: 1`) did the whole 484k run in 6.6 min | birdcage | SOL002 |
| default error estimator when AMR is off | half of the run time; `EstimatorMG` + `EstimatorTol 1e-4` gave bit-identical S 5.8x faster | GDS-to-Palace benchmark | SOL003 |
| mixed wave and lumped ports | Palace 0.16 writes no `port-S.csv`; resistor as an Impedance sheet, or all ports lumped | Wilkinson, port tuning | PRT001 |
| lumped ports with different R | 0.16 reports \|S21\|^2 low by R1/R2 (fixed in 0.17: drop the correction after upgrading) | `lumped_port_line` example, benchmark combiner | PRT011 |
| edge lumped port beside an absorbing wall | ~20 % of the port power lost into the wall; PMC end walls are lossless | port tuning | PRT012 |
| curved or tilted lumped patch | Palace aborts (projected-length / 1-degree axis check); make patches flat, Direction along an edge | birdcage | PRT009 |
| element cut by a symmetry plane | needs C/2 along its width (full C: -7.6 % frequency error), 2C across its length | birdcage | BND004 |
| absorbing walls in the near field | 0.03 dB fake loss at 0.2 lambda (Wilkinson); 5 % flat loss and S11 != S22 at 1 mm (benchmark) | Wilkinson, benchmark | BND001 |
| wave port smaller than the mode | 3-6 % loss falling with frequency on a bare line; use the whole end face | benchmark | PRT013 |
| adaptive sweep in a closed box | aborted after the first sample ("singular matrix", SuperLU); direct points worked | benchmark | BND002 |
| adaptive sweep online phase | 2.6 s per frequency per excitation at 258k unknowns: 97 of 128 min | port tuning | DRV003 |
| nonconformal AMR with wave ports | the port mode jumped to a spurious mode (n_eff = sqrt(4.4)); conformal refinement fixed it | microstrip-amr | PRT014 |
| wave-port de-embedding with lossy conductors | the de-embedded length carries no conductor loss in 0.16 (-0.001 vs -0.30 dB per inch) | microstrip-amr | PRT015 |
| AMR size cap | checked after refining: a 450k cap ended at 623k unknowns, 14 GB | microstrip-amr | AMR001 |
| several excitations with field output | only the last excitation's ParaView fields are kept (0.16) | Wilkinson | PRT016 |
| `Model.L0` missing | the mesh is read in micrometres | Palace docs | MOD001 |
| tag typo | Palace prints "Unknown ... boundary attributes! Solver will just ignore them!" and runs on | Palace source | BND005 |
| WSL and `nohup` | WSL kills detached jobs when the last `wsl.exe` exits; keep the run in the foreground of a background job | benchmark, PA | (runner) |
| floating port / unresolved via | \|S21\| ~ -50 dB with no error; S22 = +0.8 dB from one tiny via under a pad | PA, benchmark | (`check`) |

Version-dependent behaviour (0.16 vs 0.17/0.18) is listed in
[references/version-notes.md](skills/palace-simulation/references/version-notes.md).

## Cost estimate

`palace-tools estimate` fits power laws in the number of unknowns to 190 recorded Palace 0.16 runs (eigenmode
and driven, direct and p-multigrid, orders 1-3, 1k to 1.6M unknowns) from the projects above. Anchors: a 1.12 M
unknown order-2 eigenmode solve with MUMPS on 4 ranks used 16.4 GB and 11-19 min (estimate 15.5 GB, 12 min);
a 1.32 M unknown driven sweep with p-multigrid used 10 GB (estimate 10.7 GB). On the recorded runs the median
memory error is 4 % (eigenmode, direct), 10 % (driven, p-multigrid) and 14 % (driven, direct; 80th percentile
41 %). Wall time is only good to 2-3x, because many recorded runs shared the CPU with another job. The estimate is a
go/no-go aid for "ask the user first", not a schedule.

## Limits

- Rules and cost data come from Palace 0.16 on one 8-thread, 32 GB desktop (WSL2 with 25 GB). Times do not
  scale with rank count in the model; clusters will differ.
- The lint rules catch known failure patterns; they do not prove a model right. Always compare with a reference
  and refine the mesh once on the figure of merit.
- Geometry-aware rules need the `*.tags.json` from `palace_tools.mesh`; meshes from other tools get the
  config-only rules.
- The mesh helpers cover axis-aligned planar RF structures (boxes, traces, vias, rectangular ports) and three
  templates; complex 3-D CAD should be built with Gmsh directly, using the same group conventions.
- The MCP server runs tools synchronously; `palace_run` blocks until Palace exits.
- Not affiliated with AWS or the Palace developers.

## Development

```
pip install -e ".[dev]"
pytest                      # 67 tests, ~3 s, no Palace needed
python tools/gen_gotchas.py # regenerate the gotchas reference after editing rules
python tools/harvest_costs.py label=<runs folder> ...   # add your own finished runs to the cost model
```

## Licence

Apache License 2.0, the same licence as Palace. The vendored Palace config schema
(`palace_tools/data/schema_0.16/`) is Copyright Amazon.com, Inc. or its affiliates, Apache-2.0; see
[NOTICE](NOTICE).

TDQS

B3.4/5.0

Scored across 13 tools

Disambiguation4/5

Tools mostly target distinct actions (build, lint, mesh, run, results), so selection is usually clear. However, palace_lint_rules and palace_gotchas overlap in content because gotchas includes lint rules plus workflow lessons, which could cause confusion about which to call.

Naming Consistency5/5

All tools use consistent snake_case with a palace_ prefix and a predictable verb_noun pattern (e.g., palace_build_config, palace_lint_config, palace_mesh_report). Naming is highly coherent and easy to infer.

Tool Count5/5

The 13 tools map cleanly onto the multi-step Palace workflow—config creation, linting, meshing, running, result summary, cost estimation, status, and sanity checks. Each tool appears to earn its place without obvious redundancy.

Completeness4/5

Core lifecycle coverage from config building through meshing, running, and result summarization is strong, with diagnostic and check tools rounding out the set. Minor gaps exist in case/folder management (e.g., listing, deleting, or comparing runs), but these are workaround-able.

Maintenance

ActivityMaintained
ResponsivenessNo issues