Skip to content

feat(palace): expose Palace's advanced wave-port eigensolver controls - #253

Open
Alisama20 wants to merge 2 commits into
gdsfactory:mainfrom
Alisama20:feat/wave-port-eigensolver-controls
Open

Alisama20 wants to merge 2 commits into
gdsfactory:mainfrom
Alisama20:feat/wave-port-eigensolver-controls

Conversation

@Alisama20

Copy link
Copy Markdown

Item 2 of #228: "Expose Palace advanced wave-port solver controls in gsim
and benchmark SLEPc versus ARPACK, solver tolerances, and subspace sizing."

What's missing

WavePortConfig only ever produces a bare
Index/Mode/Offset/Excitation/Attributes entry for numeric wave
ports - there is nowhere to set Palace's own per-port eigensolver controls
(SolverType, EigenTol, KSPTol, MaxSize, Verbose), all marked
x-palace-advanced in Palace's own JSON schema
(awslabs/palace, scripts/schema/config-schema.json, $defs.WavePort).

The change

Five new optional fields on WavePortConfig
(eigensolver_type/eigensolver_tol/eigensolver_ksp_tol/
eigensolver_max_size/eigensolver_verbose), threaded through
Simulation.add_wave_port() -> configure_wave_port() -> PalacePort ->
the emitted WavePort config entry in generate_palace_config. Each field
is only emitted when set, matching the existing R/L/C "only if not None"
pattern already used for lumped ports a few lines above the change in
config_generator.py.

Naming note: WavePortConfig already has a field called max_size -
a bool meaning "fill the full simulation domain" (geometry sizing).
Palace's own MaxSize (eigensolver subspace dimension) is an unrelated
int, so the new field is eigensolver_max_size to avoid the collision,
and the same eigensolver_ prefix is used on all five new fields for
consistency.

Verification

Not just unit-tested in isolation - ran the real
generate_palace_config() end to end on a two-port config with all five
fields set on one port and left at their defaults on the other:

ports = [
    PalacePort(name="o1", port_type=PortType.WAVEPORT, layer="metal",
               eigensolver_type="SLEPc", eigensolver_tol=1e-6,
               eigensolver_ksp_tol=1e-8, eigensolver_max_size=40,
               eigensolver_verbose=2),
    PalacePort(name="o2", port_type=PortType.WAVEPORT, layer="metal", excited=False),
]

emits exactly:

{"Index": 1, "Mode": 1, "Offset": 0.0, "Excitation": 1, "Attributes": [6],
 "SolverType": "SLEPc", "EigenTol": 1e-06, "KSPTol": 1e-08, "MaxSize": 40, "Verbose": 2}
{"Index": 2, "Mode": 1, "Offset": 0.0, "Excitation": false, "Attributes": [7]}
  • all five keys present with the right values on the configured port, none
    of them present on the default one.

The full existing tests/palace/ suite (314 tests, cloud-marked ones
excluded) passes unmodified against these changes. 5 new tests added
(tests/palace/test_waveport_eigensolver_controls.py) covering: model
defaults, the max_size/eigensolver_max_size field independence,
all-five-emitted, none-emitted-when-unset, and per-port independence
across two ports with different settings.

Scope

This is item 2 only - it does not itself benchmark SLEPc vs ARPACK or
change any default solver behaviour (every field defaults to None,
Palace's own default is unchanged unless a caller opts in). It also
doesn't touch item 3's WavePortPEC work (#251, merged) or attempt item 6
(port-mode interpolation/ROM) - see the comment on #228 itself for a
closed-form prototype of that direction. MaxIts (GMRES iteration cap,
also x-palace-advanced in Palace's schema, alongside the five here) was
deliberately left out - happy to add it in this PR or a follow-up,
whichever you'd prefer.

Refs #228

gsim's WavePortConfig only ever emitted a bare Index/Mode/Offset/
Excitation/Attributes entry for numeric wave ports, with nowhere to put
Palace's own per-port eigensolver controls (SolverType, EigenTol,
KSPTol, MaxSize, Verbose - all marked x-palace-advanced in Palace's own
JSON schema). This is issue gdsfactory#228's item 2 ("Expose Palace advanced
wave-port solver controls in gsim and benchmark SLEPc versus ARPACK,
solver tolerances, and subspace sizing").

Adds five optional fields (eigensolver_type/tol/ksp_tol/max_size/
verbose) threaded through WavePortConfig -> Simulation.add_wave_port()
-> configure_wave_port() -> PalacePort -> the emitted WavePort config
entry, each field only emitted when set (matching the existing R/L/C
"only if not None" pattern already used for lumped ports a few lines
above the change in config_generator.py).

Named with an eigensolver_ prefix specifically to avoid colliding with
the pre-existing WavePortConfig.max_size (a bool controlling whether
the port geometry fills the simulation domain) - Palace's own MaxSize
is an unrelated int, the eigensolver's subspace dimension.

Verified end-to-end (not just unit-tested): built a two-port config
with all five fields set on one port and defaults on the other, ran it
through the real generate_palace_config(), and confirmed the emitted
JSON carries exactly the expected keys on the configured port and none
of them on the default one. Full existing tests/palace/ suite (314
tests, cloud-marked ones excluded) still passes unmodified; 5 new tests
added for this feature.

Refs gdsfactory#228
@github-actions github-actions Bot added the enhancement New feature or request label Sep 4, 2026
@mdmaas

mdmaas commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Hi, Ali! Nice.

Maybe we could use sim.set_numerical(eigensolver_type = ...) instead of the PalacePort API, which is more focused on the physics?

@Alisama20

Copy link
Copy Markdown
Author

Hi Martin, thanks - and I think you're right about the API.

The port call is already awkward with this in it: add_wave_port(max_size=True, eigensolver_max_size=20) has two max_size that mean entirely different things, one a domain-filling flag and one an eigensolver subspace size. That on its own is a good argument for keeping numerics off the physics API.

One wrinkle worth flagging: Palace defines these per wave port, not globally. From its own schema (scripts/schema/config-schema.json, $defs/WavePort), SolverType, EigenTol, KSPTol, MaxSize, MaxIts and Verbose all live inside each Boundaries.WavePort[] entry. So a purely global setter can't express a model whose ports need different treatment - which is a real case when the ports have quite different cross-sections and the 2D eigenproblems are conditioned differently.

So what I'd propose, if it suits you: defaults on set_numerical(...) exactly as you suggest, and keep an optional per-port override for when they diverge. The port API then carries nothing numerical unless someone explicitly asks for it, and we still cover the schema.

Also: MaxIts is in the schema and I did not expose it. I can add it in the same pass.

Happy to restructure it that way - just say which you prefer.

@mdmaas

mdmaas commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Yeah, leaving the per-waveport numerics as optional settings is a good idea.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.02%. Comparing base (ead7c4f) to head (0c17654).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #253      +/-   ##
==========================================
+ Coverage   61.62%   62.02%   +0.39%     
==========================================
  Files         105      106       +1     
  Lines       15304    15427     +123     
  Branches     3029     3041      +12     
==========================================
+ Hits         9431     9568     +137     
+ Misses       4882     4871      -11     
+ Partials      991      988       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

WavePortConfig.eigensolver_type was already Literal["Default", "SLEPc",
"ARPACK"] | None, matching Palace's own schema, but the three places that
feed a value into it - Simulation.add_wave_port(), the PalacePort
dataclass, and the configure_wave_port() free function - all typed the
same field as plain str | None.

ty catches the mismatch at the one call site that goes straight from
add_wave_port() into WavePortConfig(...):

    error[invalid-argument-type]: Argument is incorrect
     --> src/gsim/palace/base.py:2492:17
    eigensolver_type=eigensolver_type,
    Expected `Literal["Default", "SLEPc", "ARPACK"] | None`, found `str | None`

The other two (PalacePort.eigensolver_type, configure_wave_port()'s
parameter) carry the same field through a port.info dict, so ty doesn't
see the mismatch there - but it's the same looseness.

Verified: ty check passes; ruff check/format clean; the 81 tests in
tests/palace/{test_waveport_eigensolver_controls,test_mesh_integration,
test_mesh_regression,test_sim_classes}.py pass unmodified (4 xpassed,
pre-existing).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants