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

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# 

24 

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. 

29 

30Elmer reads its own mesh format, so the gmsh mesh is converted first; both steps 

31run in the output directory:: 

32 

33 ElmerGrid 14 2 mesh.msh -autoclean 

34 ElmerSolver case.sif 

35 

36ElmerSolver writes the matrix to ``cmatrix.dat``; capacitance body *i* there is 

37the group with tag *i* in ``mesh.json``. 

38""" 

39 

40from __future__ import annotations 

41 

42from dataclasses import dataclass 

43import os 

44from typing import TYPE_CHECKING, Any, ClassVar, List, Mapping, Optional 

45 

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) 

50 

51if TYPE_CHECKING: 

52 from klayout_pex_protobuf.kpex.pex25d.pex25d_scene_pb2 import PEX25DScene 

53 

54OUTER_BOUNDARY_CONDITIONS = ('zero_charge', 'ground') 

55 

56VACUUM_PERMITTIVITY = 8.8541878128e-12 

57"""F/m, CODATA 2018.""" 

58 

59 

60@dataclass 

61class ElmerExporterOptions(GmshMeshOptions): 

62 """The mesh's settings, and Elmer's.""" 

63 

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 """ 

70 

71 linear_tol: float = 1.0e-10 

72 """Relative residual of the linear solver.""" 

73 

74 linear_max_iterations: int = 1000 

75 

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)}") 

81 

82 

83class ElmerSceneExporter(PEX25DSceneExporter): 

84 """Write an Elmer FEM electrostatics case from a resolved PEX25D scene.""" 

85 

86 name: ClassVar[str] = 'elmer' 

87 default_prefix: ClassVar[str] = '' 

88 

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) 

99 

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] 

107 

108 

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'