Source code for skrf.circuit

"""
circuit (:mod:`skrf.circuit`)
========================================

The Circuit class represents a circuit of arbitrary topology,
consisting of an arbitrary number of N-ports networks.

Like in an electronic circuit simulator, the circuit must have one or more ports
connected to the circuit. The Circuit object allows one retrieving the M-ports network,
where M is the number of ports defined.

The results are returned in :class:`~skrf.circuit.Circuit` object.


Building a Circuit
------------------
.. autosummary::
   :toctree: generated/

   Circuit
   Circuit.Port
   Circuit.SeriesImpedance
   Circuit.ShuntAdmittance
   Circuit.Ground
   Circuit.Open

Representing a Circuit
----------------------
.. autosummary::
   :toctree: generated/

   Circuit.plot_graph

Network Representations
-----------------------
.. autosummary::
   :toctree: generated/

   Circuit.network
   Circuit.s
   Circuit.s_external
   Circuit.s_active
   Circuit.z_active
   Circuit.y_active
   Circuit.vswr_active
   Circuit.port_z0

Voltages and Currents
---------------------
.. autosummary::
   :toctree: generated/

   Circuit.voltages
   Circuit.voltages_external
   Circuit.currents
   Circuit.currents_external

Circuit internals
------------------
.. autosummary::
   :toctree: generated/

   Circuit.networks_dict
   Circuit.networks_list
   Circuit.connections_nb
   Circuit.connections_list
   Circuit.nodes_nb
   Circuit.dim
   Circuit.intersections_dict
   Circuit.port_indexes
   Circuit.C
   Circuit.X

Graph representation
--------------------
.. autosummary::
   :toctree: generated/

   Circuit.graph
   Circuit.G
   Circuit.edges
   Circuit.edge_labels

"""
from __future__ import annotations

from itertools import chain
from typing import TYPE_CHECKING

import numpy as np

from .constants import INF, S_DEF_DEFAULT, NumberLike
from .media import media
from .network import Network, s2s
from .util import subplots

if TYPE_CHECKING:
    from .frequency import Frequency


[docs] class Circuit: """ Creates a circuit made of a set of N-ports networks. For instructions on how to create Circuit see :func:`__init__`. A Circuit object is representation a circuit assembly of an arbitrary number of N-ports networks connected together via an arbitrary topology. The algorithm used to calculate the resultant network can be found in [#]_. References ---------- .. [#] P. Hallbjörner, Microw. Opt. Technol. Lett. 38, 99 (2003). """ @staticmethod def _get_nx(): """Returns networkx module if available. Raises: ------- ImportError: If networkx module is not installed Returns: -------- networkx module """ try: import networkx as nx return nx except ImportError as err: raise ImportError('networkx package as not been installed and is required.') from err
[docs] def __init__(self, connections: list[list[tuple]], name: str = None,) -> None: """ Circuit constructor. Creates a circuit made of a set of N-ports networks. Parameters ---------- connections : list of list of tuples Description of circuit connections. Each connection is a described by a list of tuple. Each tuple contains (network, network_port_nb). Port number indexing starts from zero. name : string, optional Name assigned to the circuit (Network). Default is None. Examples -------- Example of connections between two 1-port networks: :: connections = [ [(network1, 0), (network2, 0)], ] Example of a connection between three 1-port networks connected to a single node: :: connections = [ [(network1, 0), (network2, 0), (network3, 0)] ] Example of a connection between two 1-port networks (port1 and port2) and two 2-ports networks (ntw1 and ntw2): :: connections = [ [(port1, 0), (ntw1, 0)], [(ntw1, 1), (ntw2, 0)], [(ntw2, 1), (port2, 0)] ] Example of a connection between three 1-port networks (port1, port2 and port3) and a 3-ports network (ntw): :: connections = [ [(port1, 0), (ntw, 0)], [(port2, 0), (ntw, 1)], [(port3, 0), (ntw, 2)] ] NB1: Creating 1-port network to be used as a port should be made with :func:`Port` NB2: The external ports indexing is defined by the order of appearance of the ports in the connections list. Thus, the first network identified as a port will be the 1st port of the resulting network (index 0), the second network identified as a port will be the second port (index 1), etc. """ self.connections = connections self.name = name # check if all networks have a name for cnx in self.connections: for (ntw, _) in cnx: if not self._is_named(ntw): raise AttributeError('All Networks must have a name. Faulty network:', ntw) # list of networks for initial checks ntws = self.networks_list() # check if all networks have same frequency ref_freq = ntws[0].frequency for ntw in ntws: if ntw.frequency != ref_freq: raise AttributeError('All Networks must have same frequencies') # All frequencies are the same, Circuit frequency can be any of the ntw self.frequency = ntws[0].frequency # Check that a (ntwk, port) combination appears only once in the connexion map nodes = [(ntwk.name, port) for (con_idx, (ntwk, port)) in [con for con in self.connections_list]] if len(nodes) > len(set(nodes)): raise AttributeError('A (network, port) node appears twice in the connection description.')
def _is_named(self, ntw): """ Return True is the network has a name, False otherwise """ if not ntw.name or ntw.name == '': return False else: return True
[docs] @classmethod def Port(cls, frequency: Frequency, name: str, z0: float = 50) -> Network: """ Return a 1-port Network to be used as a Circuit port. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` Frequency common to all other networks in the circuit name : string Name of the port. z0 : real, optional Characteristic impedance of the port. Default is 50 Ohm. Returns ------- port : :class:`~skrf.network.Network` object 1-port network Examples -------- .. ipython:: @suppress In [16]: import skrf as rf In [17]: freq = rf.Frequency(start=1, stop=2, npoints=101) In [18]: port1 = rf.Circuit.Port(freq, name='Port1') """ _media = media.DefinedGammaZ0(frequency, z0=z0) port = _media.match(name=name) port._is_circuit_port = True return port
[docs] @classmethod def SeriesImpedance(cls, frequency: Frequency, Z: NumberLike, name: str, z0: float = 50) -> Network: """ Return a 2-port network of a series impedance. Passing the frequency and name is mandatory. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` Frequency common to all other networks in the circuit Z : complex array of shape n_freqs or complex Impedance name : string Name of the series impedance z0 : real, optional Characteristic impedance of the port. Default is 50 Ohm. Returns ------- serie_impedance : :class:`~skrf.network.Network` object 2-port network Examples -------- .. ipython:: @suppress In [16]: import skrf as rf In [17]: freq = rf.Frequency(start=1, stop=2, npoints=101) In [18]: open = rf.Circuit.SeriesImpedance(freq, rf.INF, name='series_impedance') """ A = np.zeros(shape=(len(frequency), 2, 2), dtype=complex) A[:, 0, 0] = 1 A[:, 0, 1] = Z A[:, 1, 0] = 0 A[:, 1, 1] = 1 ntw = Network(a=A, frequency=frequency, z0=z0, name=name) return ntw
[docs] @classmethod def ShuntAdmittance(cls, frequency: Frequency, Y: NumberLike, name: str, z0: float = 50) -> Network: """ Return a 2-port network of a shunt admittance. Passing the frequency and name is mandatory. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` Frequency common to all other networks in the circuit Y : complex array of shape n_freqs or complex Admittance name : string Name of the shunt admittance z0 : real, optional Characteristic impedance of the port. Default is 50 Ohm. Returns ------- shunt_admittance : :class:`~skrf.network.Network` object 2-port network Examples -------- .. ipython:: @suppress In [16]: import skrf as rf In [17]: freq = rf.Frequency(start=1, stop=2, npoints=101) In [18]: short = rf.Circuit.ShuntAdmittance(freq, rf.INF, name='shunt_admittance') """ A = np.zeros(shape=(len(frequency), 2, 2), dtype=complex) A[:, 0, 0] = 1 A[:, 0, 1] = 0 A[:, 1, 0] = Y A[:, 1, 1] = 1 ntw = Network(a=A, frequency=frequency, z0=z0, name=name) return ntw
[docs] @classmethod def Ground(cls, frequency: Frequency, name: str, z0: float = 50) -> Network: """ Return a 2-port network of a grounded link. Passing the frequency and a name is mandatory. The ground link is modelled as an infinite shunt admittance. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` Frequency common to all other networks in the circuit name : string Name of the ground. z0 : real, optional Characteristic impedance of the port. Default is 50 Ohm. Returns ------- ground : :class:`~skrf.network.Network` object 2-port network Examples -------- .. ipython:: @suppress In [16]: import skrf as rf In [17]: freq = rf.Frequency(start=1, stop=2, npoints=101) In [18]: ground = rf.Circuit.Ground(freq, name='GND') """ return cls.ShuntAdmittance(frequency, Y=INF, name=name)
[docs] @classmethod def Open(cls, frequency: Frequency, name: str, z0: float = 50) -> Network: """ Return a 2-port network of an open link. Passing the frequency and name is mandatory. The open link is modelled as an infinite series impedance. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` Frequency common to all other networks in the circuit name : string Name of the open. z0 : real, optional Characteristic impedance of the port. Default is 50 Ohm. Returns ------- open : :class:`~skrf.network.Network` object 2-port network Examples -------- .. ipython:: @suppress In [16]: import skrf as rf In [17]: freq = rf.Frequency(start=1, stop=2, npoints=101) In [18]: open = rf.Circuit.Open(freq, name='open') """ return cls.SeriesImpedance(frequency, Z=INF, name=name)
[docs] def networks_dict(self, connections: list = None, min_nports: int = 1) -> dict: """ Return the dictionary of Networks from the connection setup X. Parameters ---------- connections : List, optional connections list, by default None (then uses the `self.connections`) min_nports : int, optional min number of ports, by default 1 Returns ------- dict Dictionnary of Networks """ if not connections: connections = self.connections ntws = [] for cnx in connections: for (ntw, _port) in cnx: ntws.append(ntw) return {ntw.name: ntw for ntw in ntws if ntw.nports >= min_nports}
[docs] def networks_list(self, connections: list = None, min_nports: int = 1) -> list: """ Return a list of unique networks (sorted by appearing order in connections). Parameters ---------- connections : List, optional connections list, by default None (then uses the `self.connections`) min_nports : int, optional min number of ports, by default 1 Returns ------- list List of unique networks """ if not connections: connections = self.connections ntw_dict = self.networks_dict(connections) return [ntw for ntw in ntw_dict.values() if ntw.nports >= min_nports]
@property def connections_nb(self) -> int: """ Return the number of intersections in the circuit. """ return len(self.connections) @property def connections_list(self) -> list: """ Return the full list of connections, including intersections. The resulting list if of the form: :: [ [connexion_number, connexion], [connexion_number, connexion], ... ] """ return [[idx_cnx, cnx] for (idx_cnx, cnx) in enumerate(chain.from_iterable(self.connections))] @property def networks_nb(self) -> int: """ Return the number of connected networks (port excluded). """ return len(self.networks_list(self.connections)) @property def nodes_nb(self) -> int: """ Return the number of nodes in the circuit. """ return self.connections_nb + self.networks_nb @property def dim(self) -> int: """ Return the dimension of the C, X and global S matrices. It correspond to the sum of all connections. """ return np.sum([len(cnx) for cnx in self.connections]) @property def G(self): """ Generate the graph of the circuit. Convenience shortname for :func:`graph`. """ return self.graph()
[docs] def graph(self): """ Generate the graph of the circuit. Returns ------- G: :class:`networkx.Graph` graph object [#]_ . References ---------- .. [#] https://networkx.github.io/ """ nx = self._get_nx() G = nx.Graph() # Adding network nodes G.add_nodes_from([it for it in self.networks_dict(self.connections)]) # Adding edges in the graph between connections and networks for (idx, cnx) in enumerate(self.connections): cnx_name = 'X'+str(idx) # Adding connection nodes and edges G.add_node(cnx_name) for (ntw, _ntw_port) in cnx: ntw_name = ntw.name G.add_edge(cnx_name, ntw_name) return G
[docs] def is_connected(self) -> bool: """ Check if the circuit's graph is connected. Check if every pair of vertices in the graph is connected. """ nx = self._get_nx() return nx.algorithms.components.is_connected(self.G)
@property def intersections_dict(self) -> dict: """ Return a dictionary of all intersections with associated ports and z0: :: { k: [(ntw1_name, ntw1_port), (ntw1_z0, ntw2_name, ntw2_port), ntw2_z0], ... } """ inter_dict = {} # for k in range(self.connections_nb): # # get all edges connected to intersection Xk # inter_dict[k] = list(nx.algorithms.boundary.edge_boundary(self.G, ('X'+str(k),) )) for (k, cnx) in enumerate(self.connections): inter_dict[k] = [(ntw, ntw_port, ntw.z0[0, ntw_port]) \ for (ntw, ntw_port) in cnx] return inter_dict @property def edges(self) -> list: """ Return the list of all circuit connections """ return list(self.G.edges) @property def edge_labels(self) -> dict: """ Return a dictionary describing the port and z0 of all graph edges. Dictionary is in the form: :: {('ntw1_name', 'X0'): '3 (50+0j)', ('ntw2_name', 'X0'): '0 (50+0j)', ('ntw2_name', 'X1'): '2 (50+0j)', ... } which can be used in `networkx.draw_networkx_edge_labels` """ # for all intersections, # get the N interconnected networks and associated ports and z0 # and forge the edge label dictionary containing labels between # two nodes edge_labels = {} for it in self.intersections_dict.items(): k, cnx = it for idx in range(len(cnx)): ntw, ntw_port, ntw_z0 = cnx[idx] #ntw_z0 = ntw.z0[0,ntw_port] edge_labels[(ntw.name, 'X'+str(k))] = str(ntw_port)+'\n'+\ str(np.round(ntw_z0, decimals=1)) return edge_labels def _Xk(self, cnx_k: list[tuple]) -> np.ndarray: """ Return the scattering matrices [X]_k of the individual intersections k. The results in [#]_ do not agree due to an error in the formula (3) for mismatched intersections. Parameters ---------- cnx_k : list of tuples each tuple contains (network, port) Returns ------- Xs : :class:`numpy.ndarray` shape `f x n x n` References ---------- .. [#] P. Hallbjörner, Microw. Opt. Technol. Lett. 38, 99 (2003). """ y0s = np.array([1/ntw.z0[:,ntw_port] for (ntw, ntw_port) in cnx_k]).T y_k = y0s.sum(axis=1) Xs = np.zeros((len(self.frequency), len(cnx_k), len(cnx_k)), dtype='complex') Xs = 2 *np.sqrt(np.einsum('ij,ik->ijk', y0s, y0s)) / y_k[:, None, None] np.einsum('kii->ki', Xs)[:] -= 1 # Sii return Xs @property def X(self) -> np.ndarray: """ Return the concatenated intersection matrix [X] of the circuit. It is composed of the individual intersection matrices [X]_k assembled by block diagonal. Returns ------- X : :class:`numpy.ndarray` Note ---- There is a numerical bottleneck in this function, when creating the block diagonal matrice [X] from the [X]_k matrices. """ Xks = [self._Xk(cnx) for cnx in self.connections] Xf = np.zeros((len(self.frequency), self.dim, self.dim), dtype='complex') off = np.array([0, 0]) for Xk in Xks: Xf[:, off[0]:off[0] + Xk.shape[1], off[1]:off[1]+Xk.shape[2]] = Xk off += Xk.shape[1:] return Xf @property def C(self) -> np.ndarray: """ Return the global scattering matrix of the networks. Returns ------- S : :class:`numpy.ndarray` Global scattering matrix of the networks. Shape `f x (nb_inter*nb_n) x (nb_inter*nb_n)` """ # list all networks which are not considered as "ports", ntws = {k:v for k,v in self.networks_dict().items() if not getattr(v, '_is_circuit_port', False)} # generate the port reordering indexes from each connections ntws_ports_reordering = {ntw:[] for ntw in ntws} for (idx_cnx, cnx) in self.connections_list: ntw, ntw_port = cnx if ntw.name in ntws.keys(): ntws_ports_reordering[ntw.name].append([ntw_port, idx_cnx]) # re-ordering scattering parameters S = np.zeros((len(self.frequency), self.dim, self.dim), dtype='complex' ) for (ntw_name, ntw_ports) in ntws_ports_reordering.items(): # get the port re-ordering indexes (from -> to) ntw_ports = np.array(ntw_ports) # port permutations from_port = ntw_ports[:,0] to_port = ntw_ports[:,1] for (_from, _to) in zip(from_port, to_port): S[:, _to, to_port] = ntws[ntw_name].s_traveling[:, _from, from_port] return S # shape (nb_frequency, nb_inter*nb_n, nb_inter*nb_n) @property def s(self) -> np.ndarray: """ Return the global scattering parameters of the circuit. Return the scattering parameters of both "inner" and "outer" ports. Returns ------- S : :class:`numpy.ndarray` global scattering parameters of the circuit. """ return self.X @ np.linalg.inv(np.identity(self.dim) - self.C @ self.X) @property def port_indexes(self) -> list: """ Return the indexes of the "external" ports. Returns ------- port_indexes : list """ port_indexes = [] for (idx_cnx, cnx) in enumerate(chain.from_iterable(self.connections)): ntw, ntw_port = cnx if getattr(ntw, '_is_circuit_port', False): port_indexes.append(idx_cnx) return port_indexes def _cnx_z0(self, cnx_k: list[tuple]) -> np.ndarray: """ Return the characteristic impedances of a specific connections. Parameters ---------- cnx_k : list of tuples each tuple contains (network, port) Returns ------- z0s : :class:`numpy.ndarray` shape `f x nb_ports_at_cnx` """ z0s = [] for (ntw, ntw_port) in cnx_k: z0s.append(ntw.z0[:,ntw_port]) return np.array(z0s).T # shape (nb_freq, nb_ports_at_cnx) @property def port_z0(self) -> np.ndarray: """ Return the external port impedances. Returns ------- z0s : :class:`numpy.ndarray` shape `f x nb_ports` """ z0s = [] for cnx in self.connections: for (ntw, ntw_port) in cnx: z0s.append(ntw.z0[:,ntw_port]) return np.array(z0s)[self.port_indexes, :].T # shape (nb_freq, nb_ports) @property def s_external(self) -> np.ndarray: """ Return the scattering parameters for the external ports. Returns ------- S : :class:`numpy.ndarray` Scattering parameters of the circuit for the external ports. Shape `f x nb_ports x nb_ports` """ port_indexes = self.port_indexes a, b = np.meshgrid(port_indexes, port_indexes, indexing='ij') S_ext = self.s[:, a, b] S_ext = s2s(S_ext, self.port_z0, S_DEF_DEFAULT, 'traveling') return S_ext # shape (nb_frequency, nb_ports, nb_ports) @property def network(self) -> Network: """ Return the Network associated to external ports. Returns ------- ntw : :class:`~skrf.network.Network` Network associated to external ports """ ntw = Network() ntw.frequency = self.frequency ntw.z0 = self.port_z0 ntw.s = self.s_external ntw.name = self.name return ntw
[docs] def s_active(self, a: NumberLike) -> np.ndarray: r""" Return "active" s-parameters of the circuit's network for a defined wave excitation `a`. The "active" s-parameter at a port is the reflection coefficients when other ports are excited. It is an important quantity for active phased array antennas. Active s-parameters are defined by [#]_: .. math:: \mathrm{active}(s)_{mn} = \sum_i s_{mi} \frac{a_i}{a_n} Parameters ---------- a : complex array of shape (n_ports) forward wave complex amplitude (power-wave formulation [#]_) Returns ------- s_act : complex array of shape (n_freqs, n_ports) active s-parameters for the excitation a References ---------- .. [#] D. M. Pozar, IEEE Trans. Antennas Propag. 42, 1176 (1994). .. [#] D. Williams, IEEE Microw. Mag. 14, 38 (2013). """ return self.network.s_active(a)
[docs] def z_active(self, a: NumberLike) -> np.ndarray: r""" Return the "active" Z-parameters of the circuit's network for a defined wave excitation a. The "active" Z-parameters are defined by: .. math:: \mathrm{active}(z)_{m} = z_{0,m} \frac{1 + \mathrm{active}(s)_m}{1 - \mathrm{active}(s)_m} where :math:`z_{0,m}` is the characteristic impedance and :math:`\mathrm{active}(s)_m` the active S-parameter of port :math:`m`. Parameters ---------- a : complex array of shape (n_ports) forward wave complex amplitude Returns ------- z_act : complex array of shape (nfreqs, nports) active Z-parameters for the excitation a See Also -------- s_active : active S-parameters y_active : active Y-parameters vswr_active : active VSWR """ return self.network.z_active(a)
[docs] def y_active(self, a: NumberLike) -> np.ndarray: r""" Return the "active" Y-parameters of the circuit's network for a defined wave excitation a. The "active" Y-parameters are defined by: .. math:: \mathrm{active}(y)_{m} = y_{0,m} \frac{1 - \mathrm{active}(s)_m}{1 + \mathrm{active}(s)_m} where :math:`y_{0,m}` is the characteristic admittance and :math:`\mathrm{active}(s)_m` the active S-parameter of port :math:`m`. Parameters ---------- a : complex array of shape (n_ports) forward wave complex amplitude Returns ------- y_act : complex array of shape (nfreqs, nports) active Y-parameters for the excitation a See Also -------- s_active : active S-parameters z_active : active Z-parameters vswr_active : active VSWR """ return self.network.y_active(a)
[docs] def vswr_active(self, a: NumberLike) -> np.ndarray: r""" Return the "active" VSWR of the circuit's network for a defined wave excitation a. The "active" VSWR is defined by : .. math:: \mathrm{active}(vswr)_{m} = \frac{1 + |\mathrm{active}(s)_m|}{1 - |\mathrm{active}(s)_m|} where :math:`\mathrm{active}(s)_m` the active S-parameter of port :math:`m`. Parameters ---------- a : complex array of shape (n_ports) forward wave complex amplitude Returns ------- vswr_act : complex array of shape (nfreqs, nports) active VSWR for the excitation a See Also -------- s_active : active S-parameters z_active : active Z-parameters y_active : active Y-parameters """ return self.network.vswr_active(a)
@property def z0(self) -> np.ndarray: """ Characteristic impedances of "internal" ports. Returns ------- z0 : complex array of shape (nfreqs, nports) Characteristic impedances of both "inner" and "outer" ports """ z0s = [] for _cnx_idx, (ntw, ntw_port) in self.connections_list: z0s.append(ntw.z0[:,ntw_port]) return np.array(z0s).T @property def connections_pair(self) -> list: """ List the connections by pair. Each connection in the circuit is between a specific pair of two (networks, port, z0). Returns ------- connections_pair : list list of pair of connections """ return [self.connections_list[i:i+2] for i in range(0, len(self.connections_list), 2)] @property def _currents_directions(self) -> np.ndarray: """ Create a array of indices to define the sign of the current. The currents are defined positive when entering an internal network. Returns ------- directions : array of int (nports, 2) Note ---- This function is used in internal currents and voltages calculations. """ directions = np.zeros((self.dim,2), dtype='int') for cnx_pair in self.connections_pair: (cnx_idx_A, cnx_A), (cnx_idx_B, cnx_B) = cnx_pair directions[cnx_idx_A,:] = cnx_idx_A, cnx_idx_B directions[cnx_idx_B,:] = cnx_idx_B, cnx_idx_A return directions def _a(self, a_external: NumberLike) -> np.ndarray: """ Wave input array at "internal" ports. Parameters ---------- a_external : array power-wave input vector at ports Returns ------- a_internal : array Wave input array at internal ports """ # create a zero array and fill the values corresponding to ports a_internal = np.zeros(self.dim, dtype='complex') a_internal[self.port_indexes] = a_external return a_internal def _a_external(self, power: NumberLike, phase: NumberLike) -> np.ndarray: r""" Wave input array at Circuit's ports ("external" ports). The array is defined from power and phase by: .. math:: a = \sqrt(2 P_{in} ) e^{j \phi} The factor 2 is in order to deal with peak values. Parameters ---------- power : list or array Input power at external ports in Watts [W] phase : list or array Input phase at external ports in radian [rad] NB: the size of the power and phase array should match the number of ports Returns ------- a_external: array Wave input array at Circuit's ports """ if len(power) != len(self.port_indexes): raise ValueError('Length of power array does not match the number of ports of the circuit.') if len(phase) != len(self.port_indexes): raise ValueError('Length of phase array does not match the number of ports of the circuit.') return np.sqrt(2*np.array(power))*np.exp(1j*np.array(phase)) def _b(self, a_internal: NumberLike) -> np.ndarray: """ Wave output array at "internal" ports Parameters ---------- a_internal : array Wave input array at internal ports Returns ------- b_internal : array Wave output array at internal ports Note ---- Wave input array at internal ports can be derived from power and phase excitation at "external" ports using `_a(power, phase)` method. """ return self.s @ a_internal
[docs] def currents(self, power: NumberLike, phase: NumberLike) -> np.ndarray: """ Currents at internal ports. NB: current direction is defined as positive when entering a node. NB: external current sign are opposite than corresponding internal ones, as the internal currents are actually flowing into the "port" networks Parameters ---------- power : list or array Input power at external ports in Watts [W] phase : list or array Input phase at external ports in radian [rad] Returns ------- I : complex array of shape (nfreqs, nports) Currents in Amperes [A] (peak) at internal ports. """ # It is possible with Circuit to define connections between # multiple (>2) ports at the same time in the connection setup, like : # cnx = [ # [(ntw1, portA), (ntw2, portB), (ntw3, portC)], ... #] # Such a case is not supported with the present calculation method # which only works with pair connections between ports, ie like: # cnx = [ # [(ntw1, portA), (ntw2, portB)], # [(ntw2, portD), (ntw3, portC)], ... #] # It should not be a huge limitation (?), since it should be always possible # to add the proper splitting Network (such a "T" or hybrid or more) # and connect this splitting Network ports to other Network ports. # ie going from: # [ntwA] ---- [ntwB] # | # | # [ntwC] # to: # [ntwA] ------ [ntwD] ------ [ntwB] # | # | # [ntwC] for inter in self.intersections_dict.values(): if len(inter) > 2: raise NotImplementedError('Connections between more than 2 ports are not supported (yet?)') a = self._a(self._a_external(power, phase)) b = self._b(a) z0s = self.z0 directions = self._currents_directions Is = (b[:,directions[:,0]] - b[:,directions[:,1]])/np.sqrt(z0s) return Is
[docs] def voltages(self, power: NumberLike, phase: NumberLike) -> np.ndarray: """ Voltages at internal ports. Parameters ---------- power : list or array Input power at external ports in Watts [W] phase : list or array Input phase at external ports in radian [rad] Returns ------- V : complex array of shape (nfreqs, nports) Voltages in Amperes [A] (peak) at internal ports. """ # cf currents() for more details for inter in self.intersections_dict.values(): if len(inter) > 2: raise NotImplementedError('Connections between more than 2 ports are not supported (yet?)') a = self._a(self._a_external(power, phase)) b = self._b(a) z0s = self.z0 directions = self._currents_directions Vs = (b[:,directions[:,0]] + b[:,directions[:,1]])*np.sqrt(z0s) return Vs
[docs] def currents_external(self, power: NumberLike, phase: NumberLike) -> np.ndarray: """ Currents at external ports. NB: current direction is defined positive when "entering" into port. Parameters ---------- power : list or array Input power at external ports in Watts [W] phase : list or array Input phase at external ports in radian [rad] Returns ------- I : complex array of shape (nfreqs, nports) Currents in Amperes [A] (peak) at external ports. """ a = self._a(self._a_external(power, phase)) b = self._b(a) z0s = self.z0 Is = [] for port_idx in self.port_indexes: Is.append((a[port_idx] - b[:,port_idx])/np.sqrt(z0s[:,port_idx])) return np.array(Is).T
[docs] def voltages_external(self, power: NumberLike, phase: NumberLike) -> np.ndarray: """ Voltages at external ports Parameters ---------- power : list or array Input power at external ports in Watts [W] phase : list or array Input phase at external ports in radian [rad] Returns ------- V : complex array of shape (nfreqs, nports) Voltages in Volt [V] (peak) at ports """ a = self._a(self._a_external(power, phase)) b = self._b(a) z0s = self.z0 Vs = [] for port_idx in self.port_indexes: Vs.append((a[port_idx] + b[:,port_idx])*np.sqrt(z0s[:,port_idx])) return np.array(Vs).T
[docs] def plot_graph(self, **kwargs): """ Plot the graph of the circuit using networkx drawing capabilities. Customisation options with default values: :: 'network_shape': 's' 'network_color': 'gray' 'network_size', 300 'network_fontsize': 7 'inter_shape': 'o' 'inter_color': 'lightblue' 'inter_size', 300 'port_shape': '>' 'port_color': 'red' 'port_size', 300 'port_fontsize': 7 'edges_fontsize': 5 'network_labels': False 'edge_labels': False 'inter_labels': False 'port_labels': False 'label_shift_x': 0 'label_shift_y': 0 """ nx = self._get_nx() G = self.G # default values network_labels = kwargs.pop('network_labels', False) network_shape = kwargs.pop('network_shape', 's') network_color = kwargs.pop('network_color', 'gray') network_fontsize = kwargs.pop('network_fontsize', 7) network_size = kwargs.pop('network_size', 300) inter_labels = kwargs.pop('inter_labels', False) inter_shape = kwargs.pop('inter_shape', 'o') inter_color = kwargs.pop('inter_color', 'lightblue') inter_size = kwargs.pop('inter_size', 300) port_labels = kwargs.pop('port_labels', False) port_shape = kwargs.pop('port_shape', '>') port_color = kwargs.pop('port_color', 'red') port_size = kwargs.pop('port_size', 300) port_fontsize = kwargs.pop('port_fontsize', 7) edge_labels = kwargs.pop('edge_labels', False) edge_fontsize = kwargs.pop('edge_fontsize', 5) label_shift_x = kwargs.pop('label_shift_x', 0) label_shift_y = kwargs.pop('label_shift_y', 0) # sort between network nodes and port nodes all_ntw_names = [ntw.name for ntw in self.networks_list()] port_names = [ntw_name for ntw_name in all_ntw_names if 'port' in ntw_name] ntw_names = [ntw_name for ntw_name in all_ntw_names if 'port' not in ntw_name] # generate connecting nodes names int_names = ['X'+str(k) for k in range(self.connections_nb)] fig, ax = subplots(figsize=(10,8)) pos = nx.spring_layout(G) # draw Networks nx.draw_networkx_nodes(G, pos, port_names, ax=ax, node_size=port_size, node_color=port_color, node_shape=port_shape) nx.draw_networkx_nodes(G, pos, ntw_names, ax=ax, node_size=network_size, node_color=network_color, node_shape=network_shape) # draw intersections nx.draw_networkx_nodes(G, pos, int_names, ax=ax, node_size=inter_size, node_color=inter_color, node_shape=inter_shape) # labels shifts pos_labels = {} for node, coords in pos.items(): pos_labels[node] = (coords[0] + label_shift_x, coords[1] + label_shift_y) # network labels if network_labels: network_labels = {lab:lab for lab in ntw_names} nx.draw_networkx_labels(G, pos_labels, labels=network_labels, font_size=network_fontsize, ax=ax) # intersection labels if inter_labels: inter_labels = {'X'+str(k):'X'+str(k) for k in range(self.connections_nb)} nx.draw_networkx_labels(G, pos_labels, labels=inter_labels, font_size=network_fontsize, ax=ax) # port labels if port_labels: port_labels = {lab:lab for lab in port_names} nx.draw_networkx_labels(G, pos_labels, labels=port_labels, font_size=port_fontsize, ax=ax) # draw edges nx.draw_networkx_edges(G, pos, ax=ax) if edge_labels: edge_labels = self.edge_labels nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, label_pos=0.5, font_size=edge_fontsize, ax=ax) # remove x and y axis and labels ax.axis('off') fig.tight_layout()