Coverage for plugins/klayout-pex-plugin-elmer/src/klayout_pex_plugins/elmer/exporter.py: 100%
47 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-elmer
7# (see https://github.com/iic-jku/klayout-pex-plugin-elmer).
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 ``elmer``: an Elmer FEM electrostatics case (StatElecSolver) whose
27result is the Maxwell capacitance matrix of every net, floating conductor and the
28ground plane.
30Elmer reads its own mesh format, so the gmsh mesh is converted first; both steps
31run in the output directory::
33 ElmerGrid 14 2 mesh.msh -autoclean
34 ElmerSolver case.sif
36ElmerSolver writes the matrix to ``cmatrix.dat``; capacitance body *i* there is
37the group with tag *i* in ``mesh.json``.
38"""
40from __future__ import annotations
42from dataclasses import dataclass
43import os
44from typing import TYPE_CHECKING, Any, ClassVar, List, Mapping, Optional
46from klayout_pex.log import info
47from klayout_pex.plugin_api.v1 import PEX25DSceneExporter
48from klayout_pex_plugins.gmsh import (LENGTH_UNIT_M, GmshMeshOptions, MeshResult,
49 parse_options, write_mesh)
51if TYPE_CHECKING:
52 from klayout_pex_protobuf.kpex.pex25d.pex25d_scene_pb2 import PEX25DScene
54OUTER_BOUNDARY_CONDITIONS = ('zero_charge', 'ground')
56VACUUM_PERMITTIVITY = 8.8541878128e-12
57"""F/m, CODATA 2018."""
60@dataclass
61class ElmerExporterOptions(GmshMeshOptions):
62 """The mesh's settings, and Elmer's."""
64 outer_boundary: str = 'zero_charge'
65 """
66 The domain box: ``zero_charge`` (no field crosses it, Elmer's natural boundary
67 condition) or ``ground`` (0 V). Each biases the matrix the other way; a larger
68 field margin shrinks both effects.
69 """
71 linear_tol: float = 1.0e-10
72 """Relative residual of the linear solver."""
74 linear_max_iterations: int = 1000
76 def __post_init__(self):
77 super().__post_init__()
78 if self.outer_boundary not in OUTER_BOUNDARY_CONDITIONS:
79 raise ValueError(f"'outer_boundary' must be one of "
80 f"{', '.join(OUTER_BOUNDARY_CONDITIONS)}")
83class ElmerSceneExporter(PEX25DSceneExporter):
84 """Write an Elmer FEM electrostatics case from a resolved PEX25D scene."""
86 name: ClassVar[str] = 'elmer'
87 default_prefix: ClassVar[str] = ''
89 def export(self,
90 scene: PEX25DScene,
91 *,
92 output_dir_path: str,
93 prefix: str = '',
94 options: Optional[Mapping[str, Any]] = None) -> List[str]:
95 """Return the case first, then the mesh and the JSON naming its groups."""
96 settings = parse_options(ElmerExporterOptions, options, self.name)
97 prefix = prefix or self.default_prefix
98 mesh = write_mesh(scene, settings, output_dir_path, prefix)
100 sif_path = os.path.join(output_dir_path, f"{prefix}case.sif")
101 with open(sif_path, 'w', encoding='utf-8') as file:
102 file.write(elmer_sif(mesh, settings))
103 info(f"Run Elmer in {output_dir_path}: "
104 f"ElmerGrid 14 2 {os.path.basename(mesh.msh_path)} -autoclean && "
105 f"ElmerSolver {os.path.basename(sif_path)}")
106 return [sif_path, *mesh.paths]
109def elmer_sif(mesh: MeshResult, settings: ElmerExporterOptions) -> str:
110 """The solver input file; ElmerGrid names the mesh directory after the .msh file."""
111 mesh_dir = os.path.splitext(os.path.basename(mesh.msh_path))[0]
112 lines = [
113 '! Electrostatics of a PEX25D scene, written by klayout-pex-plugin-elmer.',
114 f'! Capacitance body i is the terminal with tag i in {mesh_dir}.json.',
115 '',
116 'Header',
117 f' Mesh DB "." "{mesh_dir}"',
118 'End',
119 '',
120 'Simulation',
121 ' Max Output Level = 5',
122 ' Coordinate System = Cartesian 3D',
123 f' Coordinate Scaling = Real {LENGTH_UNIT_M}',
124 ' Simulation Type = Steady State',
125 ' Steady State Max Iterations = 1',
126 'End',
127 '',
128 'Constants',
129 f' Permittivity Of Vacuum = Real {VACUUM_PERMITTIVITY}',
130 'End',
131 '',
132 'Equation 1',
133 ' Active Solvers(1) = 1',
134 'End',
135 '',
136 'Solver 1',
137 ' Equation = Stat Elec Solver',
138 ' Procedure = "StatElecSolve" "StatElecSolver"',
139 ' Variable = Potential',
140 ' Variable DOFs = 1',
141 ' Calculate Electric Field = False',
142 ' Calculate Electric Flux = False',
143 ' Calculate Capacitance Matrix = True',
144 ' Capacitance Matrix Filename = File "cmatrix.dat"',
145 ' Linear System Solver = Iterative',
146 ' Linear System Iterative Method = BiCGStabl',
147 ' Linear System Preconditioning = ILU1',
148 f' Linear System Max Iterations = {settings.linear_max_iterations}',
149 f' Linear System Convergence Tolerance = {settings.linear_tol}',
150 ' Linear System Abort Not Converged = True',
151 ' Steady State Convergence Tolerance = 1.0e-5',
152 'End',
153 ]
154 for group in mesh.dielectrics:
155 lines += [
156 '',
157 f'Body {group.tag}',
158 f' Name = "{group.name}"',
159 f' Target Bodies(1) = {group.tag}',
160 ' Equation = 1',
161 f' Material = {group.tag}',
162 'End',
163 '',
164 f'Material {group.tag}',
165 f' Name = "{group.name}"',
166 f' Relative Permittivity = Real {group.permittivity}',
167 'End',
168 ]
169 for group in mesh.terminals:
170 lines += [
171 '',
172 f'Boundary Condition {group.tag}',
173 f' Name = "{group.name}"',
174 f' Target Boundaries(1) = {group.tag}',
175 f' Capacitance Body = {group.tag}',
176 'End',
177 ]
178 if mesh.outer_boundary is not None:
179 group = mesh.outer_boundary
180 lines += [
181 '',
182 f'Boundary Condition {group.tag}',
183 f' Name = "{group.name}"',
184 f' Target Boundaries(1) = {group.tag}',
185 ]
186 if settings.outer_boundary == 'ground':
187 lines.append(' Capacitance Body = 0')
188 lines.append('End')
189 return '\n'.join(lines) + '\n'