feat (FXC-5013): Lumped element support for arbitrary RLC circuit#3336
feat (FXC-5013): Lumped element support for arbitrary RLC circuit#3336George-Guryev-flxcmp wants to merge 1 commit intodevelopfrom
Conversation
cb3df02 to
5a81249
Compare
Diff CoverageDiff: origin/develop...HEAD, staged and unstaged changes
Summary
tidy3d/components/lumped_element.pyLines 82-90 82
83 def lookup_index(self, name: str) -> int:
84 """Return index for a node that must already be in the mapper. Use for port nodes."""
85 if name not in self._name_to_idx:
! 86 raise ValueError(
87 f"Node {name!r} is not in the circuit (not an endpoint of any R, L, or C component)."
88 )
89 return self._name_to_idx[name]Lines 110-118 110 _node_minus_idx: int = PrivateAttr(default=-1)
111
112 def model_post_init(self, __context: Any) -> None:
113 if not self.name:
! 114 object.__setattr__(self, "name", f"{self.element_type}{id(self)}")
115
116
117 class LumpedElement(MicrowaveBaseModel, ABC):
118 """Base class describing the interface all lumped elements obey."""Lines 949-957 949 @model_validator(mode="after")
950 def _warn_future_rename(self) -> Self:
951 """Warn about upcoming rename to AdmittanceModel."""
952 if type(self).__name__ == "CircuitImpedanceModel":
! 953 return self
954 warnings.warn(
955 "AdmittanceNetwork may be renamed to AdmittanceModel in a future release. "
956 "For building from SPICE or component lists, use CircuitImpedanceModel.",
957 category=FutureWarning,Lines 1093-1101 1093
1094 for line in lines:
1095 toks = line.split()
1096 if not toks:
! 1097 continue
1098 kind = toks[0][0].upper()
1099 comp_name = toks[0]
1100
1101 if kind == "V":Lines 1103-1111 1103 raise ValueError(
1104 "SPICE file must contain at most one voltage source for port detection."
1105 )
1106 if len(toks) < 3:
! 1107 raise ValueError(f"Voltage source line needs at least two nodes: {line!r}")
1108 port_plus_node = toks[1]
1109 port_minus_node = toks[2]
1110 continueLines 1163-1171 1163 branch_admittance_list.append(1.0 / (1j * omega * comp.value))
1164 elif comp.element_type == "C":
1165 branch_admittance_list.append(1j * omega * comp.value)
1166 else:
! 1167 raise ValueError(f"Unknown component type: {comp.element_type}")
1168 return np.diag(branch_admittance_list)
1169
1170 @staticmethod
1171 def _build_incidence_matrix_and_branch_admittance_factory(Lines 1254-1266 1254 idx_minus = node_mapper.lookup_index(port_minus_node)
1255 n_nodes = node_mapper.total_nodes()
1256
1257 if idx_minus != 0:
! 1258 raise ValueError(
1259 "effective_admittance_one_port requires port_minus_node to be ground ('0' or 'GND')."
1260 )
1261 if idx_plus == 0:
! 1262 raise ValueError(
1263 "effective_admittance_one_port requires port_plus_node to be a non-ground node; "
1264 "port_plus_node and port_minus_node cannot both be ground."
1265 )
1266 # Reduced matrix has rows/cols for nodes 1, 2, ... (full indices 1, 2, ...).Lines 1332-1345 1332 -------
1333 np.ndarray
1334 Complex permittivity array (same length as *frequencies*).
1335 """
! 1336 from tidy3d.constants import EPSILON_0
1337
! 1338 frequencies = np.asarray(frequencies, dtype=float)
! 1339 Y_complex = np.asarray(Y_complex, dtype=complex)
! 1340 omega = 2 * np.pi * frequencies
! 1341 return 1.0 + 1j * admittance_scale * np.conj(Y_complex) / (omega * EPSILON_0)
1342
1343 @staticmethod
1344 def _fit_admittance_to_pole_residue(
1345 frequencies: np.ndarray,Lines 1394-1422 1394 ------
1395 ValueError
1396 If ``frequencies`` and ``Y_complex`` lengths differ or any frequency is non-positive.
1397 """
! 1398 from tidy3d.components.dispersion_fitter import AdvancedFastFitterParam, fit
! 1399 from tidy3d.components.medium import PoleResidue
1400
! 1401 frequencies = np.asarray(frequencies, dtype=float)
! 1402 Y_complex = np.asarray(Y_complex, dtype=complex)
! 1403 if frequencies.size != Y_complex.size:
! 1404 raise ValueError("frequencies and Y_complex must have the same length.")
! 1405 if np.any(frequencies <= 0):
! 1406 raise ValueError("All frequencies must be positive.")
1407
! 1408 omega = 2 * np.pi * frequencies
! 1409 eps_data = CircuitImpedanceModel._admittance_to_eps_data(
1410 frequencies, Y_complex, admittance_scale
1411 )
1412
1413 # Scale factor for numerical conditioning: normalize max(omega) to ~1
! 1414 scale_factor = 1.0 / (np.max(omega) + 1e-30)
1415
! 1416 advanced_param = AdvancedFastFitterParam(show_progress=show_progress)
1417
! 1418 (eps_inf, poles, residues), rms = fit(
1419 omega_data=omega,
1420 resp_data=eps_data,
1421 min_num_poles=min_num_poles,
1422 max_num_poles=max_num_poles,Lines 1426-1437 1426 advanced_param=advanced_param,
1427 )
1428
1429 # Build PoleResidue from fitter output
! 1430 pole_pairs = tuple((complex(a), complex(c)) for a, c in zip(poles, residues))
! 1431 medium = PoleResidue(eps_inf=float(eps_inf), poles=pole_pairs)
1432
! 1433 return medium, float(rms)
1434
1435 @staticmethod
1436 def _eps_medium_to_admittance_ab(
1437 medium: PoleResidue,Lines 1456-1494 1456 ------
1457 ImportError
1458 If the microwave plugin is not available (required for pole-residue to polynomial conversion).
1459 """
! 1460 from tidy3d.constants import EPSILON_0
1461
! 1462 try:
! 1463 from tidy3d.plugins.microwave.generalized_RLC_network import _pole_residue_to_ab
! 1464 except ImportError:
! 1465 raise ImportError(
1466 "CircuitImpedanceModel._eps_medium_to_admittance_ab requires the microwave plugin."
1467 ) from None
1468
! 1469 poles_arr = np.array([complex(p) for p, _ in medium.poles])
! 1470 resid_arr = np.array([complex(c) for _, c in medium.poles])
! 1471 P, Q = _pole_residue_to_ab(float(medium.eps_inf) - 1.0, poles_arr, resid_arr)
! 1472 P = np.asarray(P)
! 1473 Q = np.asarray(Q)
! 1474 b = np.array([((-1) ** k) * Q[k] for k in range(len(Q))], dtype=float)
! 1475 a = np.zeros(len(P) + 1, dtype=float)
! 1476 for k in range(1, len(a)):
! 1477 a[k] = ((-1) ** (k + 1)) * EPSILON_0 * P[k - 1]
! 1478 b0 = b[0] if abs(b[0]) > 1e-30 else 1.0
! 1479 a = a / b0
! 1480 b = b / b0
1481 # Clip to non-negative for CircuitImpedanceModel
! 1482 a = np.maximum(np.real(a), 0.0)
! 1483 b = np.maximum(np.real(b), 0.0)
! 1484 a = np.trim_zeros(a, "b")
! 1485 b = np.trim_zeros(b, "b")
! 1486 if len(a) == 0:
! 1487 a = np.array([0.0])
! 1488 if len(b) == 0:
! 1489 b = np.array([1.0])
! 1490 return (tuple(float(x) for x in a), tuple(float(x) for x in b))
1491
1492 @classmethod
1493 def from_spice_file(
1494 cls,Lines 1534-1547 1534 -------
1535 CircuitImpedanceModel
1536 Model with (a, b) coefficients for use in :class:`LinearLumpedElement`.
1537 """
! 1538 component_list, port_p, port_m = cls._parse_spice_file(spice_file)
! 1539 if port_plus_node is not None:
! 1540 port_p = port_plus_node
! 1541 if port_minus_node is not None:
! 1542 port_m = port_minus_node
! 1543 return cls.from_component_list(
1544 component_list,
1545 frequencies,
1546 port_plus_node=port_p,
1547 port_minus_node=port_m,Lines 1596-1605 1596 -------
1597 CircuitImpedanceModel
1598 Model with (a, b) coefficients for use in :class:`LinearLumpedElement`.
1599 """
! 1600 frequencies = np.asarray(frequencies, dtype=float)
! 1601 Y_complex = cls._get_effective_admittance(
1602 component_list,
1603 frequencies,
1604 port_plus_node=port_plus_node,
1605 port_minus_node=port_minus_node,Lines 1603-1611 1603 frequencies,
1604 port_plus_node=port_plus_node,
1605 port_minus_node=port_minus_node,
1606 )
! 1607 medium, _ = cls._fit_admittance_to_pole_residue(
1608 frequencies=frequencies,
1609 Y_complex=Y_complex,
1610 min_num_poles=min_num_poles,
1611 max_num_poles=max_num_poles,Lines 1612-1621 1612 tolerance_rms=tolerance_rms,
1613 admittance_scale=admittance_scale,
1614 show_progress=show_progress,
1615 )
! 1616 a, b = cls._eps_medium_to_admittance_ab(medium)
! 1617 return cls(a=a, b=b)
1618
1619 @classmethod
1620 def from_touchstone_file(
1621 cls,Lines 1645-1653 1645 ------
1646 NotImplementedError
1647 Touchstone file support is not yet implemented.
1648 """
! 1649 raise NotImplementedError(
1650 "CircuitImpedanceModel.from_touchstone_file is not yet implemented. "
1651 "Use CircuitImpedanceModel.from_spice_file or from_component_list."
1652 ) |
5a81249 to
55bab10
Compare
a38aa6d to
b15736e
Compare
b15736e to
415e2f8
Compare
055ce6c to
859f10d
Compare
859f10d to
32108a7
Compare
32108a7 to
0650096
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable autofix in the Cursor dashboard.
| # lumped elements | ||
| from .components.lumped_element import ( | ||
| AdmittanceNetwork, | ||
| CircuitImpedanceModel, |
There was a problem hiding this comment.
Component class not exported from public namespace
Medium Severity
CircuitImpedanceModel.from_component_list() is a public factory method that takes list[Component] as its primary input, but Component is not exported from tidy3d or tidy3d.rf. Users must discover and use the internal import path from tidy3d.components.lumped_element import Component to construct the required input. Since CircuitImpedanceModel is exported and from_component_list is prominently documented, Component is effectively part of the public API and needs to be exported alongside CircuitImpedanceModel.


Summary
Introduces
CircuitImpedanceModelas the main way to describe general RLC circuits forLinearLumpedElement. It subclassesAdmittanceNetwork(same(a, b)admittance rational) and adds factory methods to build from SPICE netlists or from explicit R/L/C component lists.RLCNetworkis deprecated in favor of these factories;AdmittanceNetworkgets a future-rename notice (toAdmittanceModel). Supporting typesComponentandNodeMapperare added for parsed netlists and node indexing.Added
AdmittanceNetworkwith the same(a, b)storage and medium conversion; overrides the base rename validator so no warning is emitted for this class.from_spice_file()Builds a model from a SPICE netlist: parses R/C/L lines and optional single voltage source for port; returns a
CircuitImpedanceModelvia the Y→ε→pole-residue fit path.from_component_list()Builds a model from a list of Component instances; computes driving-point admittance at frequencies, fits pole-residue ε, then extracts
(a, b).from_touchstone_file(...)Stub only; raises
NotImplementedErrorwith a message to usefrom_spice_file()orfrom_component_list()._parse_spice_value,_parse_spice_file,_build_incidence_matrix_and_branch_admittance_factory,_create_branch_admittance_matrix,_get_effective_admittance,_admittance_to_eps_data,_fit_admittance_to_pole_residue,_eps_medium_to_admittance_ab(Y→ε→fit→(a,b)).Single R, L, or C branch:
element_type('R'/'L'/'C'), node_plus, node_minus, value (Ohms/Henrys/Farads), optional name. Used by SPICE parsing and by from_component_list.NodeMapperMaps node names to indices (ground always index 0; supports "0" and "GND"). Used when building incidence matrices and branch admittance from a component list.
LinearLumpedElementnetwork now accepts
CircuitImpedanceModelin addition toRLCNetworkandAdmittanceNetworkviaNetworkType = RLCNetwork | AdmittanceNetwork | CircuitImpedanceModel.CircuitImpedanceModelis exported fromtidy3dandtidy3d.rf.Deprecations / warnings
RLCNetwork: ADeprecationWarningis emitted in a model validator, directing users toCircuitImpedanceModel.from_component_listorCircuitImpedanceModel.from_spice_filefor general RLC circuits.AdmittanceNetwork: AFutureWarningis emitted (rename toAdmittanceModel);CircuitImpedanceModeloverrides the validator so it does not trigger for that subclass.Technical notes
Y→ε→pole-residue path: For both SPICE and component-list inputs, the effective one-port admittance Y(ω) is computed, converted to equivalent permittivity data, fitted with the dispersion fitter to a
PoleResidueε model, then converted back to admittance(a, b)coefficients (with non-negative clipping forCircuitImpedanceModel). This yields stable, causal models forLinearLumpedElement.SPICE parsing: Supports standard scale suffixes (K, M/MEG, m, u, n, p, f), comment lines (*, $), and continuation lines (+). Port is taken from the single voltage source (V) if present, otherwise from the first R/C/L element’s two nodes.
Testing
tests/test_components/test_lumped_element.py: All tests updated to useCircuitImpedanceModelfor moved helpers (e.g._parse_spice_file,_get_effective_admittance). New/updated tests:test_effective_admittance_parallel_rc: Component list, SPICE (with and without voltage source), and analytical Y = 1/R + jωC agree.test_effective_admittance_series_rc: Component list, SPICE, and analytical series RC admittance agree.test_effective_admittance_series_rcl: Component list, SPICE, and analytical L ‖ (R–C) admittance agree.Note
Medium Risk
Introduces a new circuit parsing + numerical fitting path (including linear algebra and SciPy
invres) and changes warning behavior, which could impact stability/serialization for lumped-element users despite being largely additive.Overview
Adds
CircuitImpedanceModelas a new network type forLinearLumpedElement, allowing arbitrary one-port R/L/C circuits to be defined from either a SPICE netlist (from_spice_file) or an explicit component list (from_component_list) and then fit into the existing(a, b)rational form.Deprecates
RLCNetworkby emitting aDeprecationWarningon use, and adds aFutureWarningonAdmittanceNetworkabout a potential rename; exports the new model viatidy3d.__init__,tidy3d.rf, updates JSON schemas to acceptCircuitImpedanceModel, and adds extensive unit tests covering SPICE parsing, node mapping, effective-admittance computation, and the fit path.Written by Cursor Bugbot for commit 0650096. This will update automatically on new commits. Configure here.