Coverage for klayout_pex/netlist/vacask_writer.py: 94%

320 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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# 

24 

25""" 

26Writes a SPICE netlist (see spice_netlist.py) as a netlist of the VACASK simulator 

27(https://codeberg.org/arpadbuermen/VACASK), to be included by a VACASK testbench. 

28 

29What differs from SPICE: 

30 - names: an identifier is [A-Za-z_$][A-Za-z0-9_$]*, other names are quoted ('A,B', 'sub!'), 

31 which VACASK takes with any character but whitespace (written as \\xHH, as KLayout does). 

32 Two names that would get the same VACASK name (e.g. 'a b' and 'a\\x20b') are an error, 

33 as VACASK would short their nets. 

34 - case: VACASK is case-sensitive, the names are written as they are, the parameter names in lowercase 

35 (VACASK lowercases the parameters of its devices, and so does its ng2vc, which converts PDK models) 

36 - numbers: plain floats (VACASK reads the SI prefix M as mega, SPICE as milli); 

37 the writer writes no parameters (.param) and expressions, though VACASK has them 

38 - R, C, L: VACASK's own resistor, capacitor and inductor devices, and its builtin sources 

39 - m: the $mfactor of a device, but a VACASK subcircuit takes m only if it declares it, 

40 a call with m = N of another subcircuit is written as N calls; 

41 an element whose model is a subcircuit that declares m (e.g. an M line of a PDK MOS) takes m too, 

42 and m = 1 of another element with a model is left out (its model may be a subcircuit) 

43 

44NOTE: imports neither KLayout nor generated protobuf code, so that format-only paths can use it 

45""" 

46 

47from __future__ import annotations 

48from dataclasses import dataclass 

49import math 

50from pathlib import Path 

51import re 

52from typing import * 

53 

54from .expression.nodes import Expression, NumberOrExpression 

55from .spice_netlist import SpiceElement, SpiceNetlist, SpiceSource, SpiceSubcircuit 

56 

57 

58class VacaskWriterError(Exception): 

59 """ 

60 The netlist has no VACASK equivalent, e.g. two of its nets would get the same VACASK name, 

61 or the writer doesn't write it, e.g. an expression 

62 """ 

63 pass 

64 

65 

66ParameterValue = Union[float, str, List[float]] 

67 

68 

69# NOTE: VACASK reads these as keywords at the start of a line (e.g. an instance named model), 

70# so they are quoted wherever they are names 

71RESERVED_WORDS = frozenset({ 

72 'include', 'section', 'endsection', 'load', 'model', 'global', 'ground', 'subckt', 'ends', 

73 'parameters', 'control', 'endc', 'analysis', 'sweep', 'embed', 'save', 

74}) 

75 

76_IDENTIFIER = re.compile(r'[A-Za-z_$][A-Za-z0-9_$]*') 

77_NODE_NUMBER = re.compile(r'0|[1-9][0-9]*') # NOTE: not 007, VACASK might read it as 7 

78 

79 

80def vacask_identifier_value(name: str) -> str: 

81 """ 

82 The name with each character VACASK names can't have (whitespace, unprintable) as \\xHH (of its UTF-8 bytes) 

83 """ 

84 return ''.join(c if c.isprintable() and not c.isspace() 

85 else ''.join(f"\\x{b:02x}" for b in c.encode('utf-8')) 

86 for c in name) 

87 

88 

89def vacask_identifier(name: str, is_node: bool = False) -> str: 

90 """ 

91 The name as VACASK identifier: itself, if it is a plain identifier (or a node number), 

92 else quoted (a quote doubled) 

93 """ 

94 value = vacask_identifier_value(name) 

95 if _IDENTIFIER.fullmatch(value) and value not in RESERVED_WORDS: 

96 return value 

97 if is_node and _NODE_NUMBER.fullmatch(value): 

98 return value 

99 return "'" + value.replace("'", "''") + "'" 

100 

101 

102def vacask_number(value: float) -> str: 

103 """ 

104 The number with 12 significant digits (as the SPICE netlist printer writes them), without SI prefix 

105 """ 

106 return f"{value:.12g}".replace('e+', 'e') 

107 

108 

109def vacask_value(value: ParameterValue) -> str: 

110 if isinstance(value, str): 

111 return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"' 

112 if isinstance(value, (list, tuple)): 

113 return '[' + ', '.join(vacask_number(v) for v in value) + ']' 

114 return vacask_number(value) 

115 

116 

117@dataclass(frozen=True, order=True) 

118class VacaskDevice: 

119 """ 

120 A device of VACASK, under the model name the netlist gives it 

121 """ 

122 model_name: str 

123 device_name: str 

124 osdi_file: Optional[str] = None # None for a builtin device 

125 

126 

127RESISTOR = VacaskDevice('resistor', 'resistor', 'resistor.osdi') 

128CAPACITOR = VacaskDevice('capacitor', 'capacitor', 'capacitor.osdi') 

129INDUCTOR = VacaskDevice('inductor', 'inductor', 'inductor.osdi') 

130VSOURCE = VacaskDevice('vsource', 'vsource') 

131ISOURCE = VacaskDevice('isource', 'isource') 

132VCVS = VacaskDevice('vcvs', 'vcvs') 

133VCCS = VacaskDevice('vccs', 'vccs') 

134CCVS = VacaskDevice('ccvs', 'ccvs') 

135CCCS = VacaskDevice('cccs', 'cccs') 

136MUTUAL = VacaskDevice('mutual', 'mutual') 

137 

138ALL_DEVICES = (RESISTOR, CAPACITOR, INDUCTOR, VSOURCE, ISOURCE, VCVS, VCCS, CCVS, CCCS, MUTUAL) 

139 

140# the device and its value parameter of an element without model 

141_PASSIVE_DEVICES: Dict[str, Tuple[VacaskDevice, str]] = { 

142 'R': (RESISTOR, 'r'), 

143 'C': (CAPACITOR, 'c'), 

144 'L': (INDUCTOR, 'l'), 

145} 

146 

147# the parameters (besides m) the devices of an element without model take, as they are named in SPICE 

148_PASSIVE_PARAMETERS: Dict[str, Set[str]] = { 

149 'R': {'noisy'}, 

150 'C': set(), 

151 'L': set(), 

152} 

153 

154_CONTROLLED_SOURCES: Dict[str, VacaskDevice] = { 

155 'E': VCVS, 

156 'G': VCCS, 

157 'H': CCVS, 

158 'F': CCCS, 

159} 

160 

161 

162class _NodeNames: 

163 """ 

164 The VACASK names of the nodes of a subcircuit (or of the top level), which differ for different nodes 

165 """ 

166 

167 def __init__(self, scope: str): 

168 self.scope = scope 

169 self.identifier_by_name: Dict[str, str] = {} 

170 self.name_by_identifier: Dict[str, str] = {} 

171 

172 def __call__(self, name: str) -> str: 

173 identifier = self.identifier_by_name.get(name) 

174 if identifier is None: 

175 identifier = vacask_identifier(name, is_node=True) 

176 other = self.name_by_identifier.setdefault(identifier, name) 

177 if other != name: 

178 raise VacaskWriterError(f"The nets {other!r} and {name!r} of {self.scope} " 

179 f"would both be the net {identifier} in VACASK, which shorts them") 

180 self.identifier_by_name[name] = identifier 

181 return identifier 

182 

183 

184class _InstanceNames: 

185 """ 

186 The VACASK names of the instances of a subcircuit (or of the top level), which must differ 

187 """ 

188 

189 def __init__(self, scope: str): 

190 self.scope = scope 

191 self.description_by_identifier: Dict[str, str] = {} 

192 

193 def declare(self, name: str, origin: Optional[str] = None) -> str: 

194 identifier = vacask_identifier(name) 

195 description = repr(name) + (f" ({origin})" if origin else '') 

196 other = self.description_by_identifier.get(identifier) 

197 if other is not None: 

198 raise VacaskWriterError(f"The instances {other} and {description} of {self.scope} " 

199 f"would both be the instance {identifier} in VACASK") 

200 self.description_by_identifier[identifier] = description 

201 return identifier 

202 

203 

204class VacaskWriter: 

205 def __init__(self, 

206 subcircuits_with_m: Collection[str] = frozenset(), 

207 header: Sequence[str] = ()): 

208 """ 

209 :param subcircuits_with_m: the subcircuits whose VACASK definition declares the multiplier m 

210 (e.g. the MOS of a PDK), which a call with m = N gets as it is. 

211 A call with m = N of another subcircuit is written as N calls, 

212 the meaning of m in SPICE (VACASK rejects parameters a subcircuit doesn't declare). 

213 :param header: the lines of the comment at the top of the netlist 

214 """ 

215 self.subcircuits_with_m = frozenset(subcircuits_with_m) 

216 self.header = list(header) 

217 

218 def write(self, 

219 netlist: SpiceNetlist, 

220 output_path: str | Path): 

221 """ 

222 :raises VacaskWriterError: if the netlist has no VACASK equivalent 

223 """ 

224 text = self.text(netlist) # NOTE: before opening the file, so that an error writes none 

225 with open(output_path, 'w', encoding='utf-8') as f: 

226 f.write(text) 

227 

228 def text(self, netlist: SpiceNetlist) -> str: 

229 """ 

230 :raises VacaskWriterError: if the netlist has no VACASK equivalent 

231 """ 

232 self._check_numbers(netlist) 

233 self._check_subcircuit_names(netlist) 

234 

235 body: List[str] = [] 

236 used_devices: Set[VacaskDevice] = set() 

237 

238 if netlist.global_nodes: 

239 body.append('global ' + ' '.join(vacask_identifier(n, is_node=True) for n in netlist.global_nodes)) 

240 body.append('') 

241 

242 for subcircuit in netlist.subcircuits: 

243 body += self._subcircuit_lines(subcircuit, used_devices) 

244 body.append('') 

245 

246 if netlist.elements: 

247 scope = 'the top level' 

248 instance_lines, devices = self._instance_lines(netlist.elements, scope, _NodeNames(scope)) 

249 used_devices |= devices 

250 body += [f"model {d.model_name} {d.device_name}" for d in sorted(devices)] 

251 body += instance_lines 

252 body.append('') 

253 

254 lines = [f"// {line}".rstrip() for line in self.header] 

255 if lines: 

256 lines.append('') 

257 osdi_files = sorted({d.osdi_file for d in used_devices if d.osdi_file}) 

258 if osdi_files: 

259 lines += [f'load "{f}"' for f in osdi_files] 

260 lines.append('') 

261 lines += body 

262 return '\n'.join(lines).rstrip('\n') + '\n' 

263 

264 # -------------------------------------------------------------------------------- 

265 

266 @staticmethod 

267 def _check_numbers(netlist: SpiceNetlist): 

268 def check(values: Mapping[str, NumberOrExpression], owner: str): 

269 for name, value in values.items(): 

270 if isinstance(value, Expression): 

271 raise VacaskWriterError(f"The {name} of {owner} is the expression {value}, " 

272 f"the VACASK writer writes numbers only") 

273 

274 def check_elements(elements: List[SpiceElement], scope: str): 

275 for element in elements: 

276 owner = f"the element {element.name!r} of {scope}" 

277 check({'value': element.value} if element.value is not None else {}, owner) 

278 check({f"parameter {n}": v for n, v in element.parameters.items()}, owner) 

279 

280 def check_no_parameters(parameters: Mapping[str, NumberOrExpression], scope: str): 

281 if parameters: 

282 raise VacaskWriterError(f"{scope[0].upper()}{scope[1:]} has .param lines ({', '.join(parameters)}), " 

283 f"the VACASK writer writes numbers only") 

284 

285 check_no_parameters(netlist.parameters, 'the top level') 

286 for subcircuit in netlist.subcircuits: 

287 scope = f"subcircuit {subcircuit.name!r}" 

288 check_no_parameters(subcircuit.local_parameters, scope) 

289 check({f"parameter {n}": v for n, v in subcircuit.parameters.items()}, scope) 

290 check_elements(subcircuit.elements, scope) 

291 check_elements(netlist.elements, 'the top level') 

292 

293 @staticmethod 

294 def _check_subcircuit_names(netlist: SpiceNetlist): 

295 # NOTE: the models of the VACASK devices are defined in each subcircuit that uses them, 

296 # where they would hide a subcircuit of the same name 

297 model_names = sorted(d.model_name for d in ALL_DEVICES) 

298 names: Dict[str, str] = {} 

299 for subcircuit in netlist.subcircuits: 

300 identifier = vacask_identifier(subcircuit.name) 

301 if identifier in names: 

302 raise VacaskWriterError(f"The subcircuits {names[identifier]!r} and {subcircuit.name!r} " 

303 f"would both be the subcircuit {identifier} in VACASK") 

304 names[identifier] = subcircuit.name 

305 if subcircuit.name in model_names: 

306 raise VacaskWriterError(f"The subcircuit {subcircuit.name!r} has the name of a model " 

307 f"the VACASK netlist defines ({', '.join(model_names)})") 

308 

309 def _subcircuit_lines(self, 

310 subcircuit: SpiceSubcircuit, 

311 used_devices: Set[VacaskDevice]) -> List[str]: 

312 scope = f"subcircuit {subcircuit.name!r}" 

313 nodes = _NodeNames(scope) 

314 ports: List[str] = [] 

315 for port in subcircuit.ports: 

316 identifier = nodes(port) 

317 if identifier in ports: 

318 raise VacaskWriterError(f"The port {port!r} of {scope} is there twice") 

319 ports.append(identifier) 

320 

321 lines = [f"subckt {vacask_identifier(subcircuit.name)} ({' '.join(ports)})"] 

322 if subcircuit.parameters: 

323 lines.append(' parameters ' + self._parameters_text(list(subcircuit.parameters.items()), scope)) 

324 

325 instance_lines, devices = self._instance_lines(subcircuit.elements, scope, nodes) 

326 used_devices |= devices 

327 lines += [f" model {d.model_name} {d.device_name}" for d in sorted(devices)] 

328 lines += [f" {line}" for line in instance_lines] 

329 lines.append('ends') 

330 return lines 

331 

332 def _instance_lines(self, 

333 elements: List[SpiceElement], 

334 scope: str, 

335 nodes: _NodeNames) -> Tuple[List[str], Set[VacaskDevice]]: 

336 instances = _InstanceNames(scope) 

337 element_kinds = {e.name: e.kind for e in elements} 

338 lines: List[str] = [] 

339 devices: Set[VacaskDevice] = set() 

340 

341 def referenced_instance(element: SpiceElement, name: str, kind: str) -> str: 

342 if element_kinds.get(name) != kind: 

343 raise VacaskWriterError(f"The element {element.name!r} of {scope} refers to {name!r}, " 

344 f"which is no element {kind} there") 

345 return vacask_identifier_value(name) 

346 

347 for element in elements: 

348 kind = element.kind 

349 master: str 

350 prefix: List[Tuple[str, ParameterValue]] = [] # e.g. the value, before the element's parameters 

351 parameters: List[Tuple[str, ParameterValue]] = [] 

352 m: Optional[float] = None 

353 m_name = '$mfactor' 

354 m_index = 0 # NOTE: m stays where it is among the parameters 

355 copies = 1 

356 element_nodes = element.nodes 

357 

358 for name, value in element.parameters.items(): 

359 if name.lower() == 'm': 

360 m, m_index = value, len(parameters) 

361 else: 

362 parameters.append((name, value)) 

363 

364 if kind in _PASSIVE_DEVICES and element.model is None: 

365 device, value_name = _PASSIVE_DEVICES[kind] 

366 devices.add(device) 

367 master = device.model_name 

368 if element.value is None: 

369 raise VacaskWriterError(f"The element {element.name!r} of {scope} has no value") 

370 unknown = [n for n, _ in parameters if n.lower() not in _PASSIVE_PARAMETERS[kind]] 

371 if unknown: 

372 raise VacaskWriterError(f"The element {element.name!r} of {scope} has parameters " 

373 f"VACASK's {device.device_name} doesn't take: {', '.join(unknown)}") 

374 prefix = [(value_name, element.value)] 

375 elif kind == 'X': 

376 if element.model is None: 

377 raise VacaskWriterError(f"The subcircuit call {element.name!r} of {scope} has no subcircuit") 

378 master = vacask_identifier(element.model) 

379 if element.model in self.subcircuits_with_m: 

380 m_name = 'm' 

381 elif m is not None: 

382 if m == 1: 

383 m = None 

384 elif float(m).is_integer() and m > 1: 

385 copies = int(m) 

386 m = None 

387 else: 

388 raise VacaskWriterError( 

389 f"The subcircuit call {element.name!r} of {scope} has m={m:g}, " 

390 f"which VACASK's {element.model} doesn't declare and which is no number of copies" 

391 ) 

392 elif kind == 'K': 

393 devices.add(MUTUAL) 

394 master = MUTUAL.model_name 

395 element_nodes = [] 

396 if len(element.references) != 2 or element.value is None: 

397 raise VacaskWriterError(f"The coupling {element.name!r} of {scope} needs two inductors " 

398 f"and a coupling factor") 

399 prefix = [('k', element.value), 

400 ('ind1', referenced_instance(element, element.references[0], 'L')), 

401 ('ind2', referenced_instance(element, element.references[1], 'L'))] 

402 elif kind in ('V', 'I'): 

403 device = VSOURCE if kind == 'V' else ISOURCE 

404 devices.add(device) 

405 master = device.model_name 

406 prefix = self._source_parameters(element, scope) 

407 elif kind in _CONTROLLED_SOURCES: 

408 device = _CONTROLLED_SOURCES[kind] 

409 devices.add(device) 

410 master = device.model_name 

411 if element.value is None: 

412 raise VacaskWriterError(f"The element {element.name!r} of {scope} has no gain") 

413 prefix = [('gain', element.value)] 

414 if kind in ('F', 'H'): 

415 if len(element.references) != 1: 

416 raise VacaskWriterError(f"The element {element.name!r} of {scope} needs " 

417 f"the voltage source of its controlling current") 

418 prefix.append(('ctlinst', referenced_instance(element, element.references[0], 'V'))) 

419 elif element.model is not None: # e.g. a diode, a MOS, a resistor of a model 

420 master = vacask_identifier(element.model) 

421 if kind in _PASSIVE_DEVICES and element.value is not None: 

422 prefix = [(_PASSIVE_DEVICES[kind][1], element.value)] 

423 if element.model in self.subcircuits_with_m: 

424 m_name = 'm' # NOTE: e.g. an M line of a PDK MOS, whose VACASK model is a subcircuit 

425 elif m == 1: 

426 m = None # NOTE: the model may be a subcircuit, on which VACASK rejects $mfactor 

427 else: 

428 raise VacaskWriterError(f"The element {element.name!r} of {scope} has no model") 

429 

430 if m is not None: 

431 parameters.insert(m_index, (m_name, m)) 

432 parameters = prefix + parameters 

433 

434 node_text = ' '.join(nodes(n) for n in element_nodes) 

435 parameter_text = self._parameters_text(parameters, f"the element {element.name!r} of {scope}") 

436 for copy in range(1, copies + 1): 

437 if copies == 1: 

438 name = instances.declare(element.name) 

439 else: 

440 name = instances.declare(f"{element.name}_{copy}", 

441 origin=f"copy {copy} of {element.name}, m={copies}") 

442 line = f"{name} ({node_text}) {master}" 

443 if parameter_text: 

444 line += ' ' + parameter_text 

445 lines.append(line) 

446 

447 return lines, devices 

448 

449 @staticmethod 

450 def _parameters_text(parameters: List[Tuple[str, ParameterValue]], 

451 owner: str) -> str: 

452 names: Set[str] = set() 

453 items: List[str] = [] 

454 for name, value in parameters: 

455 name = name.lower() 

456 if name in names: 

457 raise VacaskWriterError(f"The parameter {name} of {owner} is there twice") 

458 names.add(name) 

459 items.append(f"{vacask_identifier(name)}={vacask_value(value)}") 

460 return ' '.join(items) 

461 

462 @staticmethod 

463 def _source_parameters(element: SpiceElement, 

464 scope: str) -> List[Tuple[str, ParameterValue]]: 

465 """ 

466 The parameters of VACASK's vsource or isource for the value of a SPICE source. 

467 

468 The waveform parameters SPICE defaults to the time step or stop time of the transient analysis 

469 must be given, as are those whose default differs (e.g. a pulse without fall time doesn't fall in VACASK). 

470 """ 

471 source = element.source or SpiceSource() 

472 owner = f"the source {element.name!r} of {scope}" 

473 

474 def error(problem: str) -> VacaskWriterError: 

475 return VacaskWriterError(f"{owner[0].upper()}{owner[1:]}: {problem}") 

476 

477 parameters: List[Tuple[str, ParameterValue]] = [] 

478 function = source.function 

479 if function is None: 

480 parameters.append(('dc', source.dc if source.dc is not None else 0.0)) 

481 else: 

482 a = function.arguments 

483 match function.name: 

484 case 'pulse': 

485 if len(a) not in (6, 7): 

486 raise error("PULSE needs V1 V2 TD TR TF PW and optionally PER " 

487 "(SPICE defaults TR, TF, PW to the time step or stop time)") 

488 if a[3] <= 0 or a[4] <= 0: 

489 raise error("PULSE needs TR > 0 and TF > 0 (SPICE takes the time step for 0, " 

490 "VACASK doesn't fall for TF = 0)") 

491 parameters += [('type', 'pulse'), ('val0', a[0]), ('val1', a[1]), ('delay', a[2]), 

492 ('rise', a[3]), ('fall', a[4]), ('width', a[5])] 

493 if len(a) == 7: 

494 parameters.append(('period', a[6])) 

495 initial_value = a[0] 

496 case 'sin': 

497 if not 3 <= len(a) <= 6: 

498 raise error("SIN needs VO VA FREQ and optionally TD THETA PHASE " 

499 "(SPICE defaults FREQ to the stop time)") 

500 vo, va, freq = a[0], a[1], a[2] 

501 td, theta, phase = (a[3:] + [0.0, 0.0, 0.0])[:3] 

502 if td > 0 and phase != 0: 

503 raise error("SIN with TD > 0 and PHASE != 0 holds VO until TD in SPICE, " 

504 "but VO + VA·sin(PHASE) in VACASK") 

505 parameters += [('type', 'sine'), ('sinedc', vo), ('ampl', va), ('freq', freq)] 

506 if td: 

507 parameters.append(('delay', td)) 

508 if theta: 

509 parameters.append(('theta', theta)) 

510 if phase: 

511 parameters.append(('tdphase', phase)) 

512 initial_value = vo + va * math.sin(math.radians(phase)) 

513 case 'exp': 

514 if len(a) != 6: 

515 raise error("EXP needs V1 V2 TD1 TAU1 TD2 TAU2 " 

516 "(SPICE defaults them to the time step)") 

517 if a[4] <= a[2]: 

518 raise error("EXP needs TD2 > TD1") 

519 parameters += [('type', 'exp'), ('val0', a[0]), ('val1', a[1]), ('delay', a[2]), 

520 ('tau1', a[3]), ('td2', a[4] - a[2]), ('tau2', a[5])] 

521 initial_value = a[0] 

522 case 'pwl': 

523 if len(a) < 4 or len(a) % 2: 

524 raise error("PWL needs two or more pairs of time and value") 

525 parameters += [('type', 'pwl'), ('wave', list(a))] 

526 initial_value = a[1] 

527 case _: 

528 raise error(f"VACASK has no source function {function.name.upper()}") 

529 # NOTE: SPICE takes the DC value for the operating point, VACASK the value at time 0 

530 if source.dc is not None and source.dc != initial_value: 

531 raise error(f"its DC value {source.dc:g} differs from its value {initial_value:g} at time 0, " 

532 f"which VACASK takes for the operating point") 

533 if source.ac_magnitude is not None: 

534 parameters.append(('mag', source.ac_magnitude)) 

535 if source.ac_phase is not None: 

536 parameters.append(('phase', source.ac_phase)) 

537 return parameters