Skip to content

Fix Palace capacitance computation for new meshwell#641

Merged
nikosavola merged 8 commits intofix_meshwellfrom
fix-palace-capacitance-for-new-meshwell
Sep 18, 2025
Merged

Fix Palace capacitance computation for new meshwell#641
nikosavola merged 8 commits intofix_meshwellfrom
fix-palace-capacitance-for-new-meshwell

Conversation

@nikosavola
Copy link
Copy Markdown
Member

@nikosavola nikosavola commented Sep 16, 2025

Summary by Sourcery

Replace GMSH-based meshing with Meshwell’s 3D pipeline, streamline boundary assignment logic, and clean up debug flow in Palace capacitance simulation

New Features:

  • Generate 3D meshes via get_meshwell_prisms, cad, and mesh instead of GMsh
  • Enhance get_component_with_net_layers to handle derived and logical layers for port-specific splitting

Enhancements:

  • Simplify JSON boundary attributes with list comprehensions and remove ad-hoc substrate/metal heuristics
  • Replace print debug statements with structured logger calls and remove Spack fallback for Palace execution

Chores:

  • Remove legacy test_components and main debugging harness

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Sep 16, 2025

Reviewer's Guide

This PR refactors the palace-based capacitance workflow to use the new meshwell meshing pipeline, removes extensive debug prints in favor of structured logging, simplifies JSON boundary assignments, and tightens layer and surface handling logic.

Sequence diagram for Palace capacitance simulation workflow (Meshwell pipeline)

sequenceDiagram
    participant User
    participant PalaceWorkflow
    participant MeshwellCAD
    participant MeshwellMesh
    participant GMSH
    participant Palace
    participant Results
    User->>PalaceWorkflow: run_capacitive_simulation_palace(component)
    PalaceWorkflow->>get_component_with_net_layers: Prepare component layers
    PalaceWorkflow->>MeshwellCAD: Generate CAD (.xao)
    MeshwellCAD->>MeshwellMesh: Generate mesh (.msh)
    MeshwellMesh->>GMSH: Read mesh file
    GMSH->>Palace: Run simulation
    Palace->>Results: Output capacitance matrix
    Results->>User: Return ElectrostaticResults
Loading

Class diagram for get_component_with_net_layers refactor

classDiagram
    class get_component_with_net_layers {
        +component
        +layer_stack
        +port_names
        +delimiter
        +new_layers_init
        +add_to_layerstack
        +returns: net_component
    }
    class LayerLevel {
        +layer
        +name
        +mesh_order
    }
    get_component_with_net_layers --> LayerLevel : creates new LayerLevels
    LayerLevel <|-- DerivedLayer
    LayerLevel <|-- LogicalLayer
Loading

Class diagram for simplified boundary assignment in Palace JSON generation

classDiagram
    class PalaceJSON {
        +Model.Mesh
        +Domains.Materials
        +Boundaries.Ground
        +Boundaries.Terminal
        +Solver.Order
        +Solver.Electrostatic.Save
    }
    PalaceJSON --> Boundaries
    class Boundaries {
        +Ground.Attributes
        +Terminal.Index
        +Terminal.Attributes
    }
Loading

File-Level Changes

Change Details Files
Integrate meshwell-based mesh generation flow into capacitance simulation
  • Add imports for meshwell prisms, CAD, mesh and net-layer utility
  • Replace get_mesh call with component duplication, net-layer splitting, prism generation, CAD export, and mesh generation
  • Adjust mesh file naming and conditional mesh_file branch to match new flow
gplugins/palace/get_capacitance.py
Remove debug print statements and replace with structured logging
  • Strip out all print-based debug blocks in _generate_json, _palace and result reading
  • Introduce logger.info and logger.debug calls for key events and directory listings
gplugins/palace/get_capacitance.py
Simplify electrostatic JSON boundary assignments
  • Replace verbose ground attribute loop with list comprehension
  • Comment out unused postprocessing and solver settings
gplugins/palace/get_capacitance.py
Eliminate Spack fallback execution path for Palace
  • Remove the entire subprocess-based Spack sourcing block
  • Now immediately raise RuntimeError if palace executable is missing
  • Streamline error propagation for missing or failed Palace runs
gplugins/palace/get_capacitance.py
Refactor layer and surface grouping logic for ground and terminals
  • Add _derived_layer_equivalent_to_port_layer helper
  • Compute ground_layers via derived-layer equivalence comprehension
  • Simplify metal_surfaces and metal_signal_surfaces_grouped without fallback substrate logic
gplugins/palace/get_capacitance.py
Enhance get_component_with_net_layers to handle derived and logical layers
  • Iterate port_names correctly and gather both derived and logical layerlevels
  • Maintain new_layerlevels list to track added layers
  • Assign mesh_order and update layer_stack entries with port-specific names
gplugins/common/utils/get_component_with_net_layers.py
Minor meshwell prism extraction simplification
  • Use each_point_hole API directly for extracting hole points
gplugins/meshwell/get_meshwell_3D.py
Set Gmsh mesh file version for MFEM/Palace compatibility
  • Configure gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) before writing mesh
gplugins/palace/get_capacitance.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@nikosavola nikosavola added dependencies Pull requests that update a dependency file python Pull requests that update Python code labels Sep 16, 2025
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `gplugins/palace/get_capacitance.py:169` </location>
<code_context>
-        print(f"   JSON config file: {json_file}")
-        print(f"   Simulation folder: {simulation_folder}")
-        print(f"   Working directory contents before Palace:")
-        for item in simulation_folder.iterdir():
-            print(f"     - {item.name}")
-
</code_context>

<issue_to_address>
**suggestion:** Consider replacing print statements with logger calls for consistency.

Directory listing output is still using print; switching these to logger calls will ensure consistent logging throughout.

Suggested implementation:

```python

import logging

from kfactory import kdb
import gdsfactory as gf
import gmsh
from gdsfactory.generic_tech import LAYER_STACK
    execute_and_stream_output,
    run_async_with_event_loop,
)
from gplugins.common.utils.get_component_with_net_layers import (
    get_component_with_net_layers,

logger = logging.getLogger(__name__)

```

```python
        logger.info(f"   JSON config file: {json_file}")
        logger.info(f"   Simulation folder: {simulation_folder}")
        logger.info(f"   Working directory contents before Palace:")
        for item in simulation_folder.iterdir():
            logger.info(f"     - {item.name}")

```
</issue_to_address>

### Comment 2
<location> `gplugins/palace/get_capacitance.py:350-351` </location>
<code_context>
+    # Group signal BCs by ports
+    # TODO we need to remove the port-boundary surfaces for palace to work, why?
+    # TODO might as well remove the vacuum boundary and have just 2D sheets
+    metal_signal_surfaces_grouped = [
+        [e for e in metal_surfaces if port.name in e and boundary_delimiter not in e] for port in component.ports
     ]
-
</code_context>

<issue_to_address>
**issue:** The logic for grouping signal surfaces by port may not handle ambiguous or overlapping surface names robustly.

Using 'port.name in surface name' may cause incorrect groupings if names overlap or are ambiguous. Recommend stricter matching or an explicit mapping to prevent misassignment.
</issue_to_address>

### Comment 3
<location> `gplugins/palace/get_capacitance.py:375` </location>
<code_context>
         for dimtag in gmsh.model.getPhysicalGroups()
     }
+    # Use msh version 2.2 for MFEM / Palace compatibility, see https://mfem.org/mesh-formats/#gmsh-mesh-formats
+    gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
+    gmsh.write(str(simulation_folder / filename))
     gmsh.finalize()
</code_context>

<issue_to_address>
**suggestion:** Setting the mesh file version to 2.2 is hardcoded; consider making this configurable.

Exposing the mesh file version as a parameter or adding documentation would improve future maintainability and adaptability.

Suggested implementation:

```python
    # Set mesh file version for MFEM / Palace compatibility, see https://mfem.org/mesh-formats/#gmsh-mesh-formats
    gmsh.option.setNumber("Mesh.MshFileVersion", mesh_file_version)
    gmsh.write(str(simulation_folder / filename))
    gmsh.finalize()

```

```python
def some_mesh_function(..., mesh_file_version: float = 2.2):
    """
    ...
    Parameters
    ----------
    ...
    mesh_file_version : float, optional
        Version of the mesh file format to use (default is 2.2 for MFEM/Palace compatibility).
    """

```

You will need to:
1. Add `mesh_file_version` as a parameter to the function that contains the mesh generation code (replace `some_mesh_function` with the actual function name).
2. Pass `mesh_file_version` from wherever this function is called, or rely on the default.
3. Update any relevant documentation or usage examples to mention the new parameter.
</issue_to_address>

### Comment 4
<location> `gplugins/common/utils/get_component_with_net_layers.py:63` </location>
<code_context>
def get_component_with_net_layers(
    component: Component,
    layer_stack: LayerStack,
    port_names: list[str],
    delimiter: str = "#",
    new_layers_init: tuple[int, int] = (10010, 0),
    add_to_layerstack: bool = True,
) -> Component:
    """Returns a component where polygons touching a port are put on new logical layers.

    Uses port's layer attribute to decide which polygons need to be renamed.
    New layers are named "layername{delimiter}portname".

    Args:
        component: to process.
        layer_stack: to process.
        port_names: list of port_names to process into new layers.
        delimiter: the new layer created is called "layername{delimiter}portname".
        new_layers_init: initial layer number for the temporary new layers.
        add_to_layerstack: True by default, but can be set to False to disable parsing of the layerstack.
    """

    # Initialize returned component
    net_component = component.copy()

    new_layerlevels = list()
    # For each port to consider, convert relevant polygons
    for i, port_name in enumerate(port_names):
        port = component.ports[port_name]
        # Get original port layer polygons, and modify a new component without that layer
        polygons = (
            net_component.extract(layers=(port.layer,))
            .get_polygons()
            .get(port.layer, [])
        )
        net_component = net_component.remove_layers(layers=(port.layer,))
        for polygon in polygons:
            # If polygon belongs to port, create a unique new layer, and add the polygon to it
            if polygon.sized(int(3 * gf.kcl.dbu)).inside(
                kdb.Point(*port.to_itype().center)
            ):
                try:
                    derived_layerlevels_touching_port = [
                        e
                        for e in layer_stack.layers.values()
                        if e.derived_layer is not None
                        and e not in new_layerlevels
                        and (
                            e.derived_layer.layer.layer,
                            e.derived_layer.layer.datatype,
                        )
                        == (
                            port.layer_info.layer,
                            port.layer_info.datatype,
                        )
                    ]
                    logical_layerlevels_touching_port = [
                        e
                        for e in layer_stack.layers.values()
                        if not isinstance(
                            e.layer, gf.technology.layer_stack.DerivedLayer
                        )
                        and e not in new_layerlevels
                        and (e.layer.layer.layer, e.layer.layer.datatype)
                        == (port.layer_info.layer, port.layer_info.datatype)
                    ]

                    layerlevels_touching_port = (
                        derived_layerlevels_touching_port
                        + logical_layerlevels_touching_port
                    )
                except KeyError as e:
                    raise KeyError(
                        "Make sure your `layer_stack` contains all layers with ports"
                    ) from e

                for j, old_layerlevel in enumerate(layerlevels_touching_port):
                    new_layer_number = (
                        new_layers_init[0] + i,
                        new_layers_init[1] + j,
                    )
                    if add_to_layerstack:
                        # new_layer = copy.deepcopy(layer_stack.layers[old_layerlevel])
                        new_layerlevel = copy.deepcopy(old_layerlevel)
                        new_layerlevel.layer = LogicalLayer(
                            layer=(
                                new_layers_init[0] + i,
                                new_layers_init[1] + j,
                            )
                        )
                        new_layerlevel.name = (
                            f"{old_layerlevel.name}{delimiter}{port_name}"
                        )
                        # Increase mesh order to ensure new layer is on top old
                        new_layerlevel.mesh_order = old_layerlevel.mesh_order - 1
                        layer_stack.layers[
                            f"{old_layerlevel.name}{delimiter}{port_name}"
                        ] = new_layerlevel
                        new_layerlevels.append(new_layerlevel)
                    net_component.add_polygon(polygon, layer=new_layer_number)
            # Otherwise put the polygon back on the same layer
            else:
                net_component.add_polygon(polygon, layer=port.layer)

    net_component.name = f"{component.name}_net_layers"
    return net_component

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Replace `list()` with `[]` ([`list-literal`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/list-literal/))
- Low code quality found in get\_component\_with\_net\_layers - 19% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

```suggestion
    new_layerlevels = []
```

<br/><details><summary>Explanation</summary>The most concise and Pythonic way to create a list is to use the `[]` notation.

This fits in with the way we create lists with elements, saving a bit of mental
energy that might be taken up with thinking about two different ways of creating
lists.

```python
x = ["first", "second"]
```

Doing things this way has the added advantage of being a nice little performance
improvement.

Here are the timings before and after the change:

```
$ python3 -m timeit "x = list()"
5000000 loops, best of 5: 63.3 nsec per loop
```

```
$ python3 -m timeit "x = []"
20000000 loops, best of 5: 15.8 nsec per loop
```

Similar reasoning and performance results hold for replacing `dict()` with `{}`.
The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

### Comment 5
<location> `gplugins/palace/get_capacitance.py:144` </location>
<code_context>
def _palace(simulation_folder: Path, name: str, n_processes: int = 1) -> None:
    """Run simulations with Palace."""
    # Try to find palace in PATH first
    palace = shutil.which("palace")

    # If not found, try to load it via Spack
    if palace is None:
        raise RuntimeError(
            "palace not found. Make sure it is available in your PATH or via Spack."
        )
    else:
        # Palace found in PATH, use the original method
        json_file = simulation_folder / f"{Path(name).stem}.json"

        logger.info(f"   Running Palace simulation...")
        logger.info(f"   Palace executable: {palace}")
        logger.info(f"   JSON config file: {json_file}")
        logger.info(f"   Simulation folder: {simulation_folder}")
        logger.info(f"   Working directory contents before Palace:")
        for item in simulation_folder.iterdir():
            print(f"     - {item.name}")

        try:
            run_async_with_event_loop(
                execute_and_stream_output(
                    [palace, json_file]
                    if n_processes == 1
                    else [palace, "-np", str(n_processes), json_file],
                    shell=False,
                    log_file_dir=simulation_folder,
                    log_file_str=json_file.stem + "_palace",
                    cwd=simulation_folder,
                )
            )
        except Exception as e:
            logger.info(f"   ❌ Palace execution failed: {e}")
            raise

    # Check results after Palace execution (regardless of method used)
    logger.info(f"   Palace execution completed!")
    logger.debug(f"   Working directory contents after Palace:")
    for item in simulation_folder.iterdir():
        logger.debug(f"     - {item.name}")

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Extract code out into function ([`extract-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-method/))
- Replace f-string with no interpolated values with string [×4] ([`remove-redundant-fstring`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-fstring/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread gplugins/palace/get_capacitance.py
Comment thread gplugins/palace/get_capacitance.py
Comment thread gplugins/palace/get_capacitance.py
Comment thread gplugins/common/utils/get_component_with_net_layers.py Outdated
Comment thread gplugins/palace/get_capacitance.py
@github-actions github-actions bot added the bug Something isn't working label Sep 16, 2025
Copy link
Copy Markdown
Contributor

@joamatab joamatab left a comment

Choose a reason for hiding this comment

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

some tests seem to fail

@nikosavola
Copy link
Copy Markdown
Member Author

some tests seem to fail

The femwell tests fail because they fail in the main meshwell PR as well. Palace is failing because this doesn't have changes from #643 yet.

@nikosavola nikosavola merged commit c4d8a5e into fix_meshwell Sep 18, 2025
10 of 15 checks passed
@nikosavola nikosavola deleted the fix-palace-capacitance-for-new-meshwell branch September 18, 2025 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants