Coverage for plugins/klayout-pex-plugin-palace/src/klayout_pex_plugins/palace/exporter.py: 100%
42 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2026 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of klayout-pex-plugin-palace
7# (see https://github.com/iic-jku/klayout-pex-plugin-palace).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25"""
26The exporter ``palace``: an Electrostatic problem whose result is the Maxwell
27capacitance matrix of every net, floating conductor and the ground plane.
29Palace writes it to ``postpro/terminal-C.csv``; terminal *i* there is the group
30with tag *i* in ``mesh.json``. Paths in the config are relative, so Palace runs
31in the output directory: ``cd DIR && palace config.json``.
32"""
34from __future__ import annotations
36from dataclasses import dataclass
37import json
38import os
39from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, Optional
41from klayout_pex.log import info
42from klayout_pex.plugin_api.v1 import PEX25DSceneExporter
43from klayout_pex_plugins.gmsh import (LENGTH_UNIT_M, GmshMeshOptions, MeshResult,
44 parse_options, write_mesh)
46if TYPE_CHECKING:
47 from klayout_pex_protobuf.kpex.pex25d.pex25d_scene_pb2 import PEX25DScene
49OUTER_BOUNDARY_CONDITIONS = {'zero_charge': 'ZeroCharge', 'ground': 'Ground'}
52@dataclass
53class PalaceExporterOptions(GmshMeshOptions):
54 """The mesh's settings, and Palace's."""
56 order: int = 1
57 """
58 Polynomial order of the finite elements. 2 is about 1% more accurate with the
59 default mesh, at about 9 times the run time.
60 """
62 outer_boundary: str = 'zero_charge'
63 """
64 The domain box: ``zero_charge`` (no field crosses it) or ``ground`` (0 V). Each
65 biases the matrix the other way; a larger field margin shrinks both effects.
66 """
68 linear_tol: float = 1.0e-8
69 """Relative residual of the linear solver."""
71 linear_max_iterations: int = 200
73 def __post_init__(self):
74 super().__post_init__()
75 if self.order < 1:
76 raise ValueError("'order' must be at least 1")
77 if self.outer_boundary not in OUTER_BOUNDARY_CONDITIONS:
78 raise ValueError(f"'outer_boundary' must be one of "
79 f"{', '.join(OUTER_BOUNDARY_CONDITIONS)}")
82class PalaceSceneExporter(PEX25DSceneExporter):
83 """Write a Palace electrostatic deck from a resolved PEX25D scene."""
85 name: ClassVar[str] = 'palace'
86 default_prefix: ClassVar[str] = ''
88 def export(self,
89 scene: PEX25DScene,
90 *,
91 output_dir_path: str,
92 prefix: str = '',
93 options: Optional[Mapping[str, Any]] = None) -> List[str]:
94 """Return the config first, then the mesh and the JSON naming its groups."""
95 settings = parse_options(PalaceExporterOptions, options, self.name)
96 prefix = prefix or self.default_prefix
97 mesh = write_mesh(scene, settings, output_dir_path, prefix)
99 config_path = os.path.join(output_dir_path, f"{prefix}config.json")
100 with open(config_path, 'w', encoding='utf-8') as file:
101 json.dump(palace_config(mesh, settings), file, indent=2)
102 file.write('\n')
103 info(f"Run Palace in {output_dir_path}: palace {os.path.basename(config_path)}")
104 return [config_path, *mesh.paths]
107def palace_config(mesh: MeshResult, settings: PalaceExporterOptions) -> Dict[str, Any]:
108 boundaries: Dict[str, Any] = {
109 'Terminal': [{'Index': group.tag, 'Attributes': [group.tag]}
110 for group in mesh.terminals],
111 }
112 if mesh.outer_boundary is not None:
113 boundaries[OUTER_BOUNDARY_CONDITIONS[settings.outer_boundary]] = {
114 'Attributes': [mesh.outer_boundary.tag]}
115 return {
116 'Problem': {
117 'Type': 'Electrostatic',
118 'Verbose': 2,
119 'Output': 'postpro',
120 },
121 'Model': {
122 'Mesh': os.path.basename(mesh.msh_path),
123 'L0': LENGTH_UNIT_M,
124 },
125 'Domains': {
126 'Materials': [{'Attributes': [group.tag], 'Permittivity': group.permittivity}
127 for group in mesh.dielectrics],
128 },
129 'Boundaries': boundaries,
130 'Solver': {
131 'Order': settings.order,
132 'Linear': {
133 'Type': 'BoomerAMG',
134 'KSPType': 'CG',
135 'Tol': settings.linear_tol,
136 'MaxIts': settings.linear_max_iterations,
137 },
138 },
139 }