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

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# 

24 

25""" 

26The exporter ``palace``: an Electrostatic problem whose result is the Maxwell 

27capacitance matrix of every net, floating conductor and the ground plane. 

28 

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

33 

34from __future__ import annotations 

35 

36from dataclasses import dataclass 

37import json 

38import os 

39from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, Optional 

40 

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) 

45 

46if TYPE_CHECKING: 

47 from klayout_pex_protobuf.kpex.pex25d.pex25d_scene_pb2 import PEX25DScene 

48 

49OUTER_BOUNDARY_CONDITIONS = {'zero_charge': 'ZeroCharge', 'ground': 'Ground'} 

50 

51 

52@dataclass 

53class PalaceExporterOptions(GmshMeshOptions): 

54 """The mesh's settings, and Palace's.""" 

55 

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

61 

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

67 

68 linear_tol: float = 1.0e-8 

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

70 

71 linear_max_iterations: int = 200 

72 

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

80 

81 

82class PalaceSceneExporter(PEX25DSceneExporter): 

83 """Write a Palace electrostatic deck from a resolved PEX25D scene.""" 

84 

85 name: ClassVar[str] = 'palace' 

86 default_prefix: ClassVar[str] = '' 

87 

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) 

98 

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] 

105 

106 

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 }