Skip to content

Add COMSOL (MPh) simulation plugin - #786

Merged
joamatab merged 4 commits into
gdsfactory:mainfrom
jackgdsf:comsol-plugin
Oct 12, 2026
Merged

joamatab merged 4 commits into
gdsfactory:mainfrom
jackgdsf:comsol-plugin

Conversation

@jackgdsf

Copy link
Copy Markdown
Contributor

Part 1 of #785: adds gplugins.comsol, generic COMSOL Multiphysics helpers built on MPh.

  • Layout: prepare_comsol_layout(component, layer, feed_ports, crop_to_feed_ports=...) extracts merged metal polygons on any layer, with an optional crop between two feed ports. Also adds crop_planes, feed_port, validate_crop_containment. Uses begin_shapes_rec, so it works on locked/cached cells.
  • Models: sheet (thin PEC) and extruded-metal builders, the driven CPW RF study (lumped ports, S parameters), the electrostatic capacitance study, mesh helpers, and a chainable COMSOL(mph.Model) wrapper. MPh is imported lazily.
  • Materials: silicon (11.7) and air (1.0) permittivity are module constants and keyword-overridable; there are no PDK imports.
  • Packaging: gplugins[comsol] extra, CI matrix entry, ruff pre-commit scope.
  • Docs: notebooks/comsol_01_cpw.ipynb does a CPW extraction and plot. The solver cells sit behind RUN_COMSOL, and the notebook links to the Quantum RF PDK's solved notebooks.
  • Tests: all mocked (220 passed, 9 skipped without mph).

Not verified on a live COMSOL install. The solver code was ported from the PDK implementation and checked against the MPh/COMSOL API docs only. A human must review the code before merge.

Requested by @nikosavola

🤖 Generated with Claude Code

Generic COMSOL Multiphysics helpers ported from the Quantum RF PDK:
layout extraction on any metal layer with optional crop to feed
ports, sheet and extruded-metal model builders, driven CPW RF study,
electrostatic capacitance study, mesh helpers, plotting and result
utilities. Mocked tests, docs notebook, CI matrix entry, and the
gplugins[comsol] extra.

Refs gdsfactory#785

Co-authored-by: Niko Savola <niko@gdsfactory.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Sorry @jackgdsf, your pull request is larger than the review limit of 150,000 diff characters

jackgdsf and others added 2 commits October 10, 2026 09:45
Co-authored-by: Niko Savola <niko@gdsfactory.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs gdsfactory#785

Co-authored-by: Niko Savola <niko@gdsfactory.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Core geometry and solver behavior has only mocked coverage and has not been exercised against a live COMSOL installation.

2 open findings
What changed in this PR

Adds a COMSOL Multiphysics plugin using MPh for layout extraction, model construction, RF and capacitance studies, meshing, plotting, and results handling.

Changes:

  • Adds COMSOL geometry, study, mesh, model, plotting, and result APIs.
  • Adds mocked test coverage and a CPW tutorial notebook.
  • Adds packaging, CI, documentation, linting, and changelog integration.
File Description
README.md Documents COMSOL support and installation.
pyproject.toml Adds the comsol optional dependency.
notebooks/​comsol_01_cpw.ipynb Demonstrates CPW and capacitance workflows.
gplugins/​comsol/​__init__.py Defines public and lazy exports.
gplugins/​comsol/​_util.py Adds shared Java API helpers.
gplugins/​comsol/​layout.py Extracts and crops layout geometry.
gplugins/​comsol/​metal.py Builds extruded metal geometry.
gplugins/​comsol/​sheet.py Builds sheet-metal dielectric models.
gplugins/​comsol/​rf.py Adds driven CPW RF studies.
gplugins/​comsol/​capacitance.py Adds electrostatic capacitance studies.
gplugins/​comsol/​mesh.py Adds mesh refinement and sizing helpers.
gplugins/​comsol/​model.py Adds the chainable MPh model wrapper.
gplugins/​comsol/​results.py Adds exported-result utilities.
gplugins/​comsol/​plotting.py Adds layout and field plotting helpers.
gplugins/​comsol/​tests/​__init__.py Initializes the test package.
gplugins/​comsol/​tests/​test_comsol.py Tests exports and metal building.
gplugins/​comsol/​tests/​test_comsol_layout.py Tests layout extraction and cropping.
gplugins/​comsol/​tests/​test_comsol_sheet.py Tests sheet geometry and materials.
gplugins/​comsol/​tests/​test_comsol_rf.py Tests RF study construction.
gplugins/​comsol/​tests/​test_comsol_capacitance.py Tests capacitance studies.
gplugins/​comsol/​tests/​test_comsol_mesh.py Tests mesh helpers.
gplugins/​comsol/​tests/​test_comsol_model.py Tests the model wrapper.
gplugins/​comsol/​tests/​test_comsol_results.py Tests result utilities.
gplugins/​comsol/​tests/​test_comsol_plotting.py Tests plotting helpers.
docs/​zensical.yml Adds the notebook to documentation navigation.
.pre-commit-config.yaml Includes COMSOL code in Ruff hooks.
.github/​workflows/​test_code.yml Adds COMSOL to the CI matrix.
.changelog.d/​786.added Records the new plugin.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Returns:
The ``range`` expression, and the ``points``-long grid it should return.
"""
step_ghz = (high_ghz - low_ghz) / (points - 1)
Comment thread pyproject.toml
file = "LICENSE"

[project.optional-dependencies]
comsol = ["MPh>=1.4,<2", "shapely"]
@jackgdsf

Copy link
Copy Markdown
Contributor Author

Critical review (automated, Fable model)

Scope: head e3f34af. Every "done" claim was treated as a hypothesis. Layout extraction was exercised against real gdsfactory 9.51 / kfactory 3.0.4; the test suite was run (229 passed, mph 1.4.0 installed); ruff passes. No COMSOL/JVM is available, so every claim about live solver behaviour is UNVERIFIED and is marked as such.

High

H1 — notebooks/comsol_01_cpw.ipynb cell 10: second mph.start() raises NotImplementedError.
Cell 7 does client = mph.start() (and client.remove(model) at the end); cell 10 calls mph.start() again. MPh 1.4 Client.__init__ raises NotImplementedError('Only one client can be instantiated per Python session.') when a JVM is already running (checked in the installed mph/client.py). With RUN_COMSOL=True — the only path in this PR that actually solves anything — the notebook cannot run end-to-end. Cell 10 also relies on mph/COMSOL names that are imported inside cell 7's if RUN_COMSOL: branch. Fix: start one client at the top and reuse it in both cells.

Medium

M1 — gplugins/comsol/rf.py:222-268 (_add_port_faces) silently depends on lateral_margin_um=0.
Port faces are selected with a thin slab at layout.bbox x-planes, but build_comsol_sheet_model lets the user pad the box with lateral_margin_um. Any lateral_margin_um > 0 followed by add_cpw_rf_study fails with "selects 0 faces" (the feed metal ends at the bbox face but the box face is further out). Neither docstring states the coupling. The notebook's capacitance cell uses lateral_margin_um=200, so a user copying that for RF hits this immediately. Fix: derive the port plane from the sheet model's actual box extents (store them on the model) or reject lateral_margin_um != 0 in add_cpw_rf_study. UNVERIFIED live, but the selection coordinates are pure arithmetic on layout.bbox.

M2 — gplugins/comsol/mesh.py:224-373 (pin_absolute_mesh_sizes) hard-fails on extra generated sizes it never sweeps.
Lines 347-353 raise if observed != desired size features, but unlike pin_absolute_edge_mesh_sizes (which calls _drop_generated_sizes at :470) the face variant never drops the physics-controlled size* features COMSOL adds. The file's own comment acknowledges physics-controlled sequences hold more than one size. tests/test_comsol_mesh.py:625 only tests sweeping for the edge variant. Fix: apply the same sweep in both, or document why the face path is immune. UNVERIFIED live.

M3 — gplugins/comsol/results.py and plotting.draw_cut_plane_field are dead code carried over from the in-house notebook.
Nothing in the PR imports results.py; explain_missing_results (:52-59) tells the user about RESULTS_DIR/MODEL_DIR/RUN_COMSOL globals that do not exist in this notebook; requested_frequency_grid (:123) raises ZeroDivisionError for points=1; exported_frequency_ghz (:94-95) assumes a unit-less @ freq= header is already GHz (COMSOL export format UNVERIFIED). draw_cut_plane_field (plotting.py:101-120) builds np.geomspace/LogNorm from the 1st percentile, which is 0 for any export with >1% zero-field nodes and crashes. Fix: drop both from Part 1 or land them with the consumer that needs them.

M4 — Tests verify the author's own API guesses, not COMSOL.
The suite is a hand-built fake Java tree (_FakeJava, _Node.entities() returning preset IDs). Assertions such as test_comsol_rf.py:402 (numericTEM == "1"), :405 (("ivl", "IntegrationLineforVoltage") created with no dimension argument), shiftactive == "on", tool.all("wp1.polN") on ProjectToFaces, and the named("interface") target all encode string literals chosen in rf.py:348-374, rf.py:377-399, sheet.py rather than anything checked against COMSOL. A web check of COMSOL docs confirms Numeric TEM ports and the Integration Line for Voltage subfeature exist but did not confirm the property names / create signature used here. The "220 passed" claim therefore establishes nothing about the solver path; until one live run is recorded (model file or log), the whole RF/ES feature set is UNVERIFIED.

Low

L1 — gplugins/comsol/rf.py:472-479: validation after mutation. _check_cpw_cross_sections runs after metal, port and gap-edge selections have already been created on the model, so a failed check leaves a half-mutated model. Move the check before the first create.

L2 — gplugins/comsol/capacitance.py:157, 271-274: tag collisions. Conductor names are used verbatim as component selection tags, so conductors={"air": ...} or "metal0" collides with tags the sheet builder already created; mesh1/std1 are fixed, so adding a capacitance study to a model that already has the RF study (or vice versa) collides. Prefix the tags and allocate mesh/std tags from uniquetag.

L3 — Dead # ruff: ignore[...] pseudo-directives at sheet.py:404, sheet.py:475, metal.py:61, plotting.py:107. That is not ruff syntax (# noqa: <code> is); they do nothing and mislead readers into thinking a rule is suppressed.

L4 — tests/test_comsol.py::test_historical_cpw_alias_is_gone asserts the absence of a name that only existed in the in-house code; it is porting noise with no meaning in this repo.

Verified OK

  • prepare_comsol_layout on a real gdsfactory CPW: cropping, hole preservation through hierarchy, missing-port / absent-layer errors all behave as documented.
  • COMSOL.__init__ passing an mph.Model to super().__init__ is valid in MPh 1.4.
  • CI matrix uv pip install .[comsol,dev] and importorskip("mph") work without a JVM; ruff passes.

Verdict: Request changes — H1 breaks the only runnable demo, M1/M2 are latent runtime failures on documented parameters, and the solver-facing code remains unverified by any live run.

- Notebook: start one MPh client in the setup cell and reuse it in both
  solve cells; COMSOL and numpy are imported unconditionally, so no cell
  relies on names imported inside another cell's if.
- rf: port-face boxes are bounded only along the port axis; a plane with
  no face (sheet built with lateral_margin_um > 0) raises an explicit
  error, and the coupling is documented on the sheet builders.
- rf: CPW cross sections are checked and the port planes probed before
  any other selection is created.
- mesh: pin_absolute_mesh_sizes sweeps extra generated size* features,
  like the edge variant.
- results: requested_frequency_grid rejects points < 2 and degenerate
  windows; exported_frequency_ghz no longer assumes a unit-less
  "@ freq=" value is GHz (new bare_unit keyword); explain_missing_results
  no longer names notebook globals.
- plotting: draw_cut_plane_field takes log colour limits from positive
  nodes only, so exports with many zero-field nodes no longer crash.
- capacitance/rf: refuse conductor tags that are not COMSOL tags or that
  clash with existing comp1 selections, and refuse a model that already
  holds mesh1/std1.
- Drop the ruff: ignore pseudo-directives (not ruff syntax; the named
  rules are not selected) and test_historical_cpw_alias_is_gone.

Co-authored-by: Niko Savola <niko@gdsfactory.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jackgdsf

Copy link
Copy Markdown
Contributor Author

Reply to the critical review, addressed in 53e488b.

H1 Fixed. The setup cell starts one MPh client (only when RUN_COMSOL), and both solve cells reuse it. COMSOL and numpy are now imported unconditionally. mph is referenced only inside that one cell. The notebook runs end to end with RUN_COMSOL=False. The RUN_COMSOL=True path is UNVERIFIED because no licence is available.

M1 Fixed with an explicit error and docs. The port-face box is now bounded only along the port axis. A plane that selects no face raises an error naming lateral_margin_um=0. The constraint is documented on build_comsol_sheet_model, COMSOL.create_sheet and add_cpw_rf_study. Supporting a margin properly would mean extending the feed metal out to the box face, which is out of scope. The live behaviour is UNVERIFIED.

M2 Fixed. pin_absolute_mesh_sizes now sweeps extra generated size* features before the explicit FreeTet, the same way the edge variant does. Test: test_extra_generated_sizes_are_swept_for_the_face_sizes. The live behaviour is UNVERIFIED.

M3 Fixed in place rather than dropped, because a downstream wrapper re-exports results and plotting.

  • requested_frequency_grid rejects points < 2, non-integer counts and degenerate windows.
  • exported_frequency_ghz no longer assumes a unit-less @ freq= value is in GHz. It raises unless the caller passes the new bare_unit= keyword. COMSOL's unit for that form is still UNVERIFIED.
  • explain_missing_results no longer refers to notebook globals.
  • draw_cut_plane_field takes its log colour limits from positive nodes only, clamps zero-field nodes to the lowest colour, and raises if the window has no positive values.

Tests were added for each of these.

M4 Not changed. The fake-Java tests only check the call sequence. Until one live run is recorded, the RF/ES solver path is UNVERIFIED.

L1 Fixed. _check_cpw_cross_sections now runs before the first create. The test asserts that a refused layout leaves comp1 untouched.

L2 Fixed by validating rather than renaming, so the tags users pass to evaluate stay the same.

  • Conductor tags must be valid COMSOL tags and must not already be selections on comp1.
  • Both study builders refuse a model that already holds mesh1 or std1. The rule is one study per sheet model.

L3 Fixed. The # ruff: ignore[...] comments were removed. Ruff 0.11.13, the pinned version, has no such directive. The rules they named (N802, T201, S603, TRY*) are not selected in pyproject.toml, so no # noqa replacement is needed.

L4 Fixed. The test was removed.

Pre-commit is green apart from lychee. It failed on one external link in gplugins/gmeep/get_simulation_grating_farfield.py (simpetus.com, connection closed), which this PR does not touch. 240 tests passed and 9 were skipped.

@joamatab
joamatab merged commit 18ec2f7 into gdsfactory:main Oct 12, 2026
16 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants