Coverage for klayout_pex/pex25d/show.py: 89%
92 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
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"""
26Human-readable summary of a PEX25D artifact.
28Not a stable output. Anything that wants to consume the contents should use
29``pex25d convert --out_format textpb`` and read the protobuf text format, which
30is stable by definition.
31"""
33from __future__ import annotations
35from typing import *
37from ..log import info, subproc, rule
39from .artifact import ArtifactKind
42ALL_SECTIONS = ['header', 'meta', 'layers', 'dielectrics',
43 'conductors', 'terminals', 'resistance', 'domain']
46def show(message: Any,
47 kind: ArtifactKind,
48 sections: Sequence[str] = ('all',)) -> None:
49 selected = ALL_SECTIONS if 'all' in sections else [s for s in ALL_SECTIONS if s in sections]
51 is_scene = kind == ArtifactKind.SCENE
53 for section in selected:
54 renderer = _RENDERERS.get(section)
55 if renderer is None:
56 continue
57 renderer(message, is_scene)
60def _grid_str(units: Any, value: int) -> str:
61 """Render a grid-unit integer back into the declared LENGTH unit, for reading only."""
62 try:
63 return f"{value * units.grid_numerator / units.grid_denominator:g}"
64 except (AttributeError, ZeroDivisionError):
65 return f"{value} grid"
68def _show_header(message: Any, is_scene: bool) -> None:
69 rule('Header')
70 suffix = f"-{message.format_version_suffix}" if message.format_version_suffix else ''
71 info(f"PEX25D {message.format_version_major}.{message.format_version_minor}{suffix} "
72 f"({'scene' if is_scene else 'file'})")
73 if message.HasField('units'):
74 u = message.units
75 subproc(f"units: {u}".replace('\n', ' '))
78def _show_meta(message: Any, is_scene: bool) -> None:
79 metas = getattr(message, 'meta', None)
80 if not metas:
81 return
82 rule('Metadata')
83 for m in metas:
84 subproc(f"{m.key} = {m.value}")
87def _show_layers(message: Any, is_scene: bool) -> None:
88 rule('Ground plane and layers')
89 if message.HasField('ground_plane'):
90 gp = message.ground_plane
91 subproc(f"GROUND_PLANE {gp.name} z {gp.zlow} … {gp.zhigh}")
93 if is_scene:
94 for layer in message.layers:
95 extra = ''
96 if layer.connects_below or layer.connects_above:
97 extra = f" connects {layer.connects_below} → {layer.connects_above}"
98 subproc(f"{layer.name} z {layer.zlow} … {layer.zhigh}{extra}")
99 else:
100 for metal in message.metals:
101 subproc(f"METAL {metal.name} z {metal.zlow} … {metal.zhigh}")
102 for via in message.vias:
103 subproc(f"VIA {via.name} connects {via.connects_below} → {via.connects_above}")
106def _show_dielectrics(message: Any, is_scene: bool) -> None:
107 rule('Dielectrics')
108 for d in message.dielectrics:
109 depth = f" depth {d.wrap_depth}" if is_scene else ''
110 wraps = f" wraps {d.wraps}" if d.wraps else ''
111 subproc(f"{d.name} k={d.permittivity}{wraps}{depth}")
112 if message.HasField('background'):
113 b = message.background
114 subproc(f"{b.name} k={b.permittivity} (background)")
117def _show_conductors(message: Any, is_scene: bool) -> None:
118 rule('Conductors')
119 for c in message.conductors:
120 floating = ' FLOATING' if getattr(c, 'floating', False) else ''
121 subproc(f"{c.name} net {c.net}{floating}")
122 if not is_scene:
123 info(f"{len(message.shapes)} shape record(s)")
126def _show_terminals(message: Any, is_scene: bool) -> None:
127 terminals = getattr(message, 'terminals', None)
128 if not terminals:
129 return
130 rule('Terminals')
131 for t in terminals:
132 subproc(f"{t.name} layer {t.layer}")
135def _show_resistance(message: Any, is_scene: bool) -> None:
136 if not message.HasField('resistance_temperature'):
137 return
138 rule('Resistance')
139 subproc(f"temperature: {message.resistance_temperature}".replace('\n', ' '))
140 for r in getattr(message, 'metal_resistances', []):
141 subproc(f"metal {r.metal}: {r}".replace('\n', ' '))
142 for r in getattr(message, 'via_resistances', []):
143 subproc(f"via {r.via}: {r}".replace('\n', ' '))
146def _show_domain(message: Any, is_scene: bool) -> None:
147 if is_scene:
148 if not message.HasField('domain'):
149 info("No computational domain (the resolver never invents one)")
150 return
151 rule('Computational domain')
152 subproc(str(message.domain).replace('\n', ' '))
153 else:
154 which = message.WhichOneof('domain')
155 if which is None:
156 return
157 rule('Computational domain')
158 subproc(f"{which}: {getattr(message, which)}".replace('\n', ' '))
161_RENDERERS: Dict[str, Callable[[Any, bool], None]] = {
162 'header': _show_header,
163 'meta': _show_meta,
164 'layers': _show_layers,
165 'dielectrics': _show_dielectrics,
166 'conductors': _show_conductors,
167 'terminals': _show_terminals,
168 'resistance': _show_resistance,
169 'domain': _show_domain,
170}