Coverage for klayout_pex/magic/magic_runner.py: 43%
72 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 15:48 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 15:48 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 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#
24from enum import StrEnum
25import time
26from typing import *
28import os
29import subprocess
31from ..log import (
32 info,
33 # warning,
34 rule,
35 subproc,
36)
37from ..version import __version__
40class MagicPEXMode(StrEnum):
41 CC = "CC"
42 RC = "RC"
43 R = "R"
44 DEFAULT = CC
47class MagicShortMode(StrEnum):
48 NONE = "none"
49 RESISTOR = "resistor"
50 VOLTAGE = "voltage"
51 DEFAULT = NONE
54class MagicMergeMode(StrEnum):
55 NONE = "none" # don't merge parallel devices
56 CONSERVATIVE = "conservative" # merge devices with same L, W
57 AGGRESSIVE = "aggressive" # merge devices with same L
58 DEFAULT = NONE
60class MagicUniqueMode(StrEnum):
61 IMPLICIT = "implicit" # use Magic's default value
62 OFF = "off" # Force off
63 ON = "on" # Force on all nets
64 NO_TOP_PORTS = "no_top_ports" # Force all nets, except top-level ports
65 DEFAULT = IMPLICIT
67 def to_cmd(self) -> str:
68 """Translates enum to Magic command"""
69 match self:
70 case MagicUniqueMode.OFF:
71 return "extract no unique"
72 case MagicUniqueMode.ON:
73 return "extract do unique"
74 case MagicUniqueMode.NO_TOP_PORTS:
75 return "extract do unique notopports"
76 case MagicUniqueMode.IMPLICIT | _:
77 return ""
79def prepare_magic_script(gds_path: str,
80 cell_name: str,
81 run_dir_path: str,
82 script_path: str,
83 output_netlist_path: str,
84 pex_mode: MagicPEXMode,
85 c_threshold: float,
86 r_threshold: float,
87 threshold: float,
88 min_res: float,
89 min_delay: float,
90 halo: Optional[float],
91 short_mode: MagicShortMode,
92 merge_mode: MagicMergeMode,
93 unique_mode: MagicUniqueMode):
94 gds_path = os.path.abspath(gds_path)
95 run_dir_path = os.path.abspath(run_dir_path)
96 output_netlist_path = os.path.abspath(output_netlist_path)
98 halo_scale = 200.0
100 # NOTE: do not that those are doing nothing useful:
101 # extract do resistance
102 # ext2spice rthresh {r_threshold}
103 #
104 # see https://github.com/martinjankoehler/magic/issues/4#issuecomment-3381935719
106 has_res = pex_mode in (MagicPEXMode.RC, MagicPEXMode.R)
107 has_cap = pex_mode in (MagicPEXMode.RC, MagicPEXMode.CC)
108 # Per corner extraction modes
109 ext_coupling: str = "do" if has_cap else "no"
110 ext_cap: str = "do" if has_cap else "no"
111 ext_res: str = "do" if has_res else "no"
113 script_lines: list[str|None] = [
114 f"# Generated by kpex {__version__}",
115 "crashbackups stop",
116 "drc off",
117 f"gds read {gds_path}",
118 f"load {cell_name}",
119 "select top cell",
120 f"flatten {cell_name}_flat",
121 f"load {cell_name}_flat",
122 f"cellname delete {cell_name} -noprompt",
123 f"cellname rename {cell_name}_flat {cell_name}",
124 "select top cell",
125 f"extresist threshold {threshold}" if threshold is not None else None,
126 f"extresist minres {min_res}" if min_res is not None else None,
127 f"extresist mindelay {min_delay}" if min_delay is not None else None,
128 f"extract path {run_dir_path}",
129 f"extract halo {round(halo * halo_scale)}" if halo is not None else None,
130 f"extract {ext_cap} capacitance",
131 f"extract {ext_coupling} coupling",
132 f"extract {ext_res} resistance",
133 f"{unique_mode.to_cmd()}",
134 "extract all",
135 f"ext2spice short {short_mode}",
136 f"ext2spice merge {merge_mode}",
137 f"ext2spice cthresh {c_threshold}" if has_cap else None,
138 "ext2spice extresist on" if has_res else None,
139 "ext2spice subcircuits top on",
140 "ext2spice format ngspice",
141 f"ext2spice -p {run_dir_path} -o {output_netlist_path}",
142 "quit -noprompt"
143 ]
145 # filter out all empty strings and write out script
146 script: str = "\n".join(filter(None, script_lines)) + "\n"
148 with open(script_path, 'w', encoding='utf-8') as f:
149 f.write(script)
151def run_magic(exe_path: str,
152 magicrc_path: str,
153 script_path: str,
154 log_path: str):
155 args = [
156 exe_path,
157 '-dnull', #
158 '-noconsole', #
159 '-rcfile', #
160 magicrc_path, #
161 script_path, # TCL script
162 ]
164 info('Calling MAGIC')
165 subproc(f"{' '.join(args)}, output file: {log_path}")
167 rule('MAGIC Output')
169 start = time.time()
171 proc = subprocess.Popen(args,
172 stdin=subprocess.DEVNULL,
173 stdout=subprocess.PIPE,
174 stderr=subprocess.STDOUT,
175 universal_newlines=True,
176 text=True)
177 with open(log_path, 'w', encoding='utf-8') as f:
178 while True:
179 line = proc.stdout.readline()
180 if not line:
181 break
182 subproc(line[:-1]) # remove newline
183 f.writelines([line])
184 proc.wait()
186 duration = time.time() - start
188 rule()
190 if proc.returncode == 0:
191 info(f"MAGIC succeeded after {'%.4g' % duration}s")
192 else:
193 raise Exception(f"MAGIC failed with status code {proc.returncode} after {'%.4g' % duration}s, "
194 f"see log file: {log_path}")