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 into
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.
Additional Locations (1)
|
superseded by 7ee76dd |


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.