Source code for skrf.networkSet

"""
.. module:: skrf.networkSet

========================================
networkSet (:mod:`skrf.networkSet`)
========================================

Provides a class representing an un-ordered set of n-port microwave networks.


Frequently one needs to make calculations, such as mean or standard
deviation, on an entire set of n-port networks. To facilitate these
calculations the :class:`NetworkSet` class provides convenient
ways to make such calculations.

Another usage is to interpolate a set of Networks which depend of
an parameter (like a knob, or a geometrical parameter).

The results are returned in :class:`~skrf.network.Network` objects,
so they can be plotted and saved in the same way one would do with a
:class:`~skrf.network.Network`.

The functionality in this module is provided as methods and
properties of the :class:`NetworkSet` Class.


NetworkSet Class
================

.. autosummary::
   :toctree: generated/

   NetworkSet

NetworkSet Utilities
====================

.. autosummary::
   :toctree: generated/

   func_on_networks
   getset


"""
from __future__ import annotations

import zipfile
from io import BytesIO
from numbers import Number
from typing import Any, Mapping, TextIO

import numpy as npy
from scipy.interpolate import interp1d

from . import mathFunctions as mf
from .network import COMPONENT_FUNC_DICT, PRIMARY_PROPERTIES, Frequency, Network
from .util import copy_doc, now_string_2_dt

try:
    from numpy.typing import ArrayLike
except ImportError:
    ArrayLike = Any

from . import plotting as skrf_plt


[docs] class NetworkSet: """ A set of Networks. This class allows functions on sets of Networks, such as mean or standard deviation, to be calculated conveniently. The results are returned in :class:`~skrf.network.Network` objects, so that they may be plotted and saved in like :class:`~skrf.network.Network` objects. This class also provides methods which can be used to plot uncertainty bounds for a set of :class:`~skrf.network.Network`. The names of the :class:`NetworkSet` properties are generated dynamically upon initialization, and thus documentation for individual properties and methods is not available. However, the properties do follow the convention:: >>> my_network_set.function_name_network_property_name For example, the complex average (mean) :class:`~skrf.network.Network` for a :class:`NetworkSet` is:: >>> my_network_set.mean_s This accesses the property 's', for each element in the set, and **then** calculates the 'mean' of the resultant set. The order of operations is important. Results are returned as :class:`~skrf.network.Network` objects, so they may be plotted or saved in the same way as for :class:`~skrf.network.Network` objects:: >>> my_network_set.mean_s.plot_s_mag() >>> my_network_set.mean_s.write_touchstone('mean_response') If you are calculating functions that return scalar variables, then the result is accessible through the Network property .s_re. For example:: >>> std_s_deg = my_network_set.std_s_deg This result would be plotted by:: >>> std_s_deg.plot_s_re() The operators, properties, and methods of NetworkSet object are dynamically generated by private methods * :func:`~NetworkSet.__add_a_operator` * :func:`~NetworkSet.__add_a_func_on_property` * :func:`~NetworkSet.__add_a_element_wise_method` * :func:`~NetworkSet.__add_a_plot_uncertainty` thus, documentation on the individual methods and properties are not available. """
[docs] def __init__(self, ntwk_set: list | dict = None, name: str = None): """ Initialize for NetworkSet. Parameters ---------- ntwk_set : list of :class:`~skrf.network.Network` objects the set of :class:`~skrf.network.Network` objects name : string the name of the NetworkSet, given to the Networks returned from properties of this class. """ if ntwk_set is None: ntwk_set = [] if not isinstance(ntwk_set, (list, dict)): raise ValueError('NetworkSet requires a list as argument') # dict is authorized for convenience # but if a dict is passed instead of a list -> list if isinstance(ntwk_set, dict): ntwk_set = list(ntwk_set.values()) # did they pass a list of Networks? if not all([isinstance(ntwk, Network) for ntwk in ntwk_set]): raise(TypeError('input must be list of Network types')) # do all Networks have the same # ports? if len (set([ntwk.number_of_ports for ntwk in ntwk_set])) > 1: raise(ValueError('All elements in list of Networks must have same number of ports')) # is all frequency information the same? if not npy.all([(ntwk_set[0].frequency == ntwk.frequency) for ntwk in ntwk_set]): raise(ValueError('All elements in list of Networks must have same frequency information')) ## initialization # we are good to go self.ntwk_set: list[Network] = ntwk_set self.name = name # extract the dimensions of the set try: self.dims = self.ntwk_set[0].params.keys() except (AttributeError, IndexError): # .params is None self.dims = dict() # extract the coordinates of the set try: self.coords = {p: [] for p in self.dims} for k in self.ntwk_set: for p in self.dims: self.coords[p].append(k.params[p]) # keep only unique terms for p in self.coords.keys(): self.coords[p] = list(set(self.coords[p])) except TypeError: # .params is None self.coords = None # create list of network properties, which we use to dynamically # create a statistical properties of this set network_property_list = [k+'_'+l \ for k in PRIMARY_PROPERTIES \ for l in COMPONENT_FUNC_DICT.keys()] + \ ['passivity','s'] # dynamically generate properties. this is slick. max, min = npy.max, npy.min max.__name__ = 'max' min.__name__ = 'min' for network_property_name in network_property_list: for func in [npy.mean, npy.std, max, min]: self.__add_a_func_on_property(func, network_property_name) if 'db' not in network_property_name:# != 's_db' and network_property_name != 's': # db uncertainty requires a special function call see # plot_uncertainty_bounds_s_db self.__add_a_plot_uncertainty(network_property_name) self.__add_a_plot_minmax(network_property_name) self.__add_a_element_wise_method('plot_'+network_property_name) self.__add_a_element_wise_method('plot_s_db') self.__add_a_element_wise_method('plot_s_db_time') for network_method_name in \ ['write_touchstone','interpolate','plot_s_smith']: self.__add_a_element_wise_method(network_method_name) for operator_name in \ ['__pow__','__floordiv__','__mul__','__truediv__','__add__','__sub__']: self.__add_a_operator(operator_name)
[docs] @classmethod def from_zip(cls, zip_file_name: str, sort_filenames: bool = True, *args, **kwargs): r""" Create a NetworkSet from a zipfile of touchstones. Parameters ---------- zip_file_name : string name of zipfile sort_filenames: Boolean sort the filenames in the zip file before constructing the NetworkSet \*args, \*\*kwargs : arguments passed to NetworkSet constructor Examples -------- >>> import skrf as rf >>> my_set = rf.NetworkSet.from_zip('myzip.zip') """ z = zipfile.ZipFile(zip_file_name) filename_list = z.namelist() ntwk_list = [] if sort_filenames: filename_list.sort() for filename in filename_list: # try/except block in case not all files are touchstones try: # Ascii files (Touchstone, etc) n = Network.zipped_touchstone(filename, z) ntwk_list.append(n) continue except Exception: pass try: # Binary files (pickled Network) fileobj = BytesIO(z.open(filename).read()) fileobj.name = filename n = Network(fileobj) ntwk_list.append(n) continue except Exception: pass return cls(ntwk_list)
[docs] @classmethod def from_dir(cls, dir: str = '.', *args, **kwargs): r""" Create a NetworkSet from a directory containing Networks. This just calls :: rf.NetworkSet(rf.read_all_networks(dir), *args, **kwargs) Parameters ---------- dir : str directory containing Network files. \*args, \*\*kwargs : passed to NetworkSet constructor Examples -------- >>> my_set = rf.NetworkSet.from_dir('./data/') """ from .io.general import read_all_networks return cls(read_all_networks(dir), *args, **kwargs)
[docs] @classmethod def from_s_dict(cls, d: dict, frequency: Frequency, *args, **kwargs): r""" Create a NetworkSet from a dictionary of s-parameters The resultant elements of the NetworkSet are named by the keys of the dictionary. Parameters ------------- d : dict dictionary of s-parameters data. values of this should be :class:`numpy.ndarray` assignable to :attr:`skrf.network.Network.s` frequency: :class:`~skrf.frequency.Frequency` object frequency assigned to each network \*args, \*\*kwargs : passed to Network.__init__ for each key/value pair of d Returns ---------- ns : NetworkSet See Also ---------- NetworkSet.to_s_dict """ return cls([Network(s=d[k], frequency=frequency, name=k, **kwargs) for k in d])
[docs] @classmethod def from_mdif(cls, file: str | TextIO) -> NetworkSet: """ Create a NetworkSet from a MDIF file. Parameters ---------- file : str or file-object MDIF file to load Returns ------- ns : :class: `~skrf.networkSet.NetworkSet` See Also -------- Mdif : MDIF Object write_mdif : Convert a NetworkSet to a Generalized MDIF file. """ from .io import Mdif return Mdif(file).to_networkset()
[docs] @classmethod def from_citi(cls, file: str | TextIO) -> NetworkSet: """ Create a NetworkSet from a CITI file. Parameters ---------- file : str or file-object CITI file to load Returns ------- ns : :class: `~skrf.networkSet.NetworkSet` See Also -------- Citi """ from .io import Citi return Citi(file).to_networkset()
def __add_a_operator(self, operator_name): """ Add an operator method to the NetworkSet. this is made to take either a Network or a NetworkSet. if a Network is passed to the operator, each element of the set will operate on the Network. If a NetworkSet is passed to the operator, and is the same length as self. then it will operate element-to-element like a dot-product. """ def operator_func(self, other): if isinstance(other, NetworkSet): if len(other) != len(self): raise(ValueError('Network sets must be of same length to be cascaded')) return NetworkSet([ getattr(self.ntwk_set[k], operator_name)(other.ntwk_set[k]) for k in range(len(self)) ]) elif isinstance(other, Network): return NetworkSet([getattr(ntwk, operator_name)(other) for ntwk in self.ntwk_set]) else: raise(TypeError('NetworkSet operators operate on either Network, or NetworkSet types')) setattr(self.__class__,operator_name,operator_func) def __str__(self): """ """ return f'{len(self.ntwk_set)}-Networks NetworkSet: '+self.ntwk_set.__str__() def __repr__(self): return self.__str__() def __getitem__(self, key): """ Return an element of the network set. """ if isinstance(key, str): # if they pass a string then slice each network in this set return NetworkSet([k[key] for k in self.ntwk_set], name = self.name) else: return self.ntwk_set[key] def __len__(self) -> int: """ Return the number of Networks in a NetworkSet. Return ------ len: int Number of Networks in a NetworkSet """ return len(self.ntwk_set) def __eq__(self, other: NetworkSet) -> bool: """ Compare the NetworkSet with another NetworkSet. Two NetworkSets are considered equal of their Networks are all equals (in the same order) Returns ------- is_equal: bool """ # of course they should have equal lengths if len(self) != len(other): return False # compare all networks in the order of the list # return False as soon as 2 networks are different for (ntwk, ntwk_other) in zip(self.ntwk_set, other): if ntwk != ntwk_other: return False return True def __add_a_element_wise_method(self, network_method_name: str): def func(self, *args, **kwargs): return self.element_wise_method(network_method_name, *args, **kwargs) setattr(self.__class__,network_method_name,func) def __add_a_func_on_property(self, func, network_property_name: str): """ Dynamically add a property to this class (NetworkSet). this is mostly used internally to genrate all of the classes properties. Parameters ---------- func: a function to be applied to the network_property across the first axis of the property's output network_property_name: str a property of the Network class, which must have a matrix output of shape (f, n, n) example ------- >>> my_ntwk_set.add_a_func_on_property(mean, 's') """ def fget(self): return fon(self.ntwk_set, func, network_property_name, name=self.name) setattr(self.__class__,func.__name__+'_'+network_property_name,\ property(fget)) def __add_a_plot_uncertainty(self, network_property_name: str): """ Add a plot uncertainty to a Network property. Parameter --------- network_property_name: str A property of the Network class, which must have a matrix output of shape (f, n, n) Parameter --------- >>> my_ntwk_set.__add_a_plot_uncertainty('s') """ def plot_func(self,*args, **kwargs): kwargs.update({'attribute':network_property_name}) self.plot_uncertainty_bounds_component(*args,**kwargs) setattr(self.__class__,'plot_uncertainty_bounds_'+\ network_property_name,plot_func) setattr(self.__class__,'plot_ub_'+\ network_property_name,plot_func) def __add_a_plot_minmax(self, network_property_name: str): """ Parameter --------- network_property_name: str A property of the Network class, which must have a matrix output of shape (f, n, n) Example ------- >>> my_ntwk_set.__add_a_plot_minmax('s') """ def plot_func(self,*args, **kwargs): kwargs.update({'attribute':network_property_name}) self.plot_minmax_bounds_component(*args,**kwargs) setattr(self.__class__,'plot_minmax_bounds_'+\ network_property_name,plot_func) setattr(self.__class__,'plot_mm_'+\ network_property_name,plot_func)
[docs] def to_dict(self) -> dict: """ Return a dictionary representation of the NetworkSet. Return ------ d : dict The returned dictionary has the Network names for keys, and the Networks as values. """ return {k.name: k for k in self.ntwk_set}
[docs] def to_s_dict(self): """ Converts a NetworkSet to a dictionary of s-parameters. The resultant keys of the dictionary are the names of the Networks in NetworkSet Returns ------- s_dict : dictionary contains s-parameters in the form of complex numpy arrays See Also -------- NetworkSet.from_s_dict """ d = self.to_dict() for k in d: d[k] = d[k].s return d
[docs] def element_wise_method(self, network_method_name: str, *args, **kwargs) -> NetworkSet: """ Call a given method of each element and returns the result as a new NetworkSet if the output is a Network. Parameter --------- network_property_name: str A property of the Network class, which must have a matrix output of shape (f, n, n) Return ------ ns: :class: `~skrf.networkSet.NetworkSet` """ output = [getattr(ntwk, network_method_name)(*args, **kwargs) for ntwk in self.ntwk_set] if isinstance(output[0],Network): return NetworkSet(output) else: return output
[docs] def copy(self) -> NetworkSet: """ Copie each network of the network set. Return ------ ns: :class: `~skrf.networkSet.NetworkSet` """ return NetworkSet([k.copy() for k in self.ntwk_set])
[docs] def sort(self, key=lambda x: x.name, inplace: bool = True, **kwargs) -> None | NetworkSet: r""" Sort this network set. Parameters ---------- key: inplace: bool Sort the NetworkSet object directly if True, return the sorted NetworkSet if False. Default is True. \*\*kwargs : dict keyword args passed to builtin sorted acting on self.ntwk_set Return ------ ns: None if inplace=True, NetworkSet if False Examples -------- >>> ns = rf.NetworkSet.from_dir('mydir') >>> ns.sort() Sort by other property: >>> ns.sort(key= lambda x: x.voltage) Returns a new NetworkSet: >>> sorted_ns = ns.sort(inplace=False) """ sorted_ns = sorted(self.ntwk_set, key = key, **kwargs) if inplace: self.ntwk_set = sorted_ns else: return sorted_ns
[docs] def rand(self, n: int = 1): """ Return `n` random samples from this NetworkSet. Parameters ---------- n : int number of samples to return (default is 1) """ idx = npy.random.default_rng().randint(0,len(self), n) out = [self.ntwk_set[k] for k in idx] if n ==1: return out[0] else: return out
[docs] def filter(self, s: str) -> NetworkSet: """ Filter NetworkSet based on a string in `Network.name`. Notes ----- This is just `NetworkSet([k for k in self if s in k.name])` Parameters ---------- s: str string contained in network elements to be filtered Returns -------- ns : :class: `skrf.NetworkSet` Examples ----------- >>> ns.filter('monday') """ return NetworkSet([k for k in self if s in k.name])
[docs] def scalar_mat(self, param: str = 's') -> npy.ndarray: """ Return a scalar ndarray representing `param` data vs freq and element idx. output is a 3d array with axes (freq, ns_index, port/ri) ports is a flattened re/im components of port index (`len = 2*nports**2`) Parameter --------- param : str name of the parameter to export. Default is 's'. Return ------ x : :class: npy.ndarray """ ntwk = self[0] nfreq = len(ntwk) # x will have the axes (frequency, observations, ports) x = npy.array([[mf.flatten_c_mat(getattr(k, param)[f]) \ for k in self] for f in range(nfreq)]) return x
[docs] def cov(self, **kw) -> npy.ndarray: """ Covariance matrix. shape of output will be (nfreq, 2*nports**2, 2*nports**2) """ smat=self.scalar_mat(**kw) return npy.array([npy.cov(k.T) for k in smat])
@property def mean_s_db(self) -> Network: """ Return Network of mean magnitude in dB. Return ------ ntwk : :class: `~skrf.network.Network` Network of the mean magnitude in dB Note ---- The mean is taken on the magnitude before converted to db, so `magnitude_2_db(mean(s_mag))` which is NOT the same as `mean(s_db)` """ ntwk = self.mean_s_mag ntwk.s = ntwk.s_db return ntwk @property def std_s_db(self) -> Network: """ Return the Network of the standard deviation magnitude in dB. Return ------ ntwk : :class: `~skrf.network.Network` Network of the mean magnitude in dB Note ---- The standard deviation is taken on the magnitude before converted to db, so `magnitude_2_db(std(s_mag))` which is NOT the same as `std(s_db)` """ ntwk= self.std_s_mag ntwk.s = ntwk.s_db return ntwk @property def inv(self) -> NetworkSet: """ Return the NetworkSet of inverted Networks (Network.inv()). Returns ------- ntwkSet : :class: `~skrf.networkSet.NetworkSet` NetworkSet of inverted Networks """ return NetworkSet( [ntwk.inv for ntwk in self.ntwk_set])
[docs] def add_polar_noise(self, ntwk: Network) -> Network: """ Parameters ---------- ntwk : :class: `~skrf.network.Network` Returns ------- ntwk : :class: `~skrf.network.Network` """ from numpy import frompyfunc from scipy import stats def gimme_norm(x): return stats.norm(loc=0, scale=x).rvs(1)[0] ugimme_norm = frompyfunc(gimme_norm,1,1) s_deg_rv = npy.array(map(ugimme_norm, self.std_s_deg.s_re), dtype=float) s_mag_rv = npy.array(map(ugimme_norm, self.std_s_mag.s_re), dtype=float) mag = ntwk.s_mag + s_mag_rv deg = ntwk.s_deg + s_deg_rv ntwk.s = mag * npy.exp(1j*npy.pi/180*deg) return ntwk
[docs] def set_wise_function(self, func, a_property: str, *args, **kwargs): """ Calls a function on a specific property of the Networks in this NetworkSet. Parameters ---------- func : callable a_property : str Example ------- >>> my_ntwk_set.set_wise_func(mean,'s') """ return fon(self.ntwk_set, func, a_property, *args, **kwargs)
[docs] def uncertainty_ntwk_triplet(self, attribute: str, n_deviations: int = 3) -> (Network, Network, Network): """ Return a 3-tuple of Network objects which contain the mean, upper_bound, and lower_bound for the given Network attribute. Used to save and plot uncertainty information data. Note that providing 's' and 's_mag' as attributes will provide different results. For those who want to directly find uncertainty on dB performance, use 's_mag'. Parameters ---------- attribute : str Attribute to operate on. n_deviations : int, optional Number of standard deviation. The default is 3. Returns ------- ntwk_mean : :class: `~skrf.network.Network` Network of the averaged attribute lower_bound : :class: `~skrf.network.Network` Network of the lower bound of N*sigma deviation. upper_bound : :class: `~skrf.network.Network` Network of the upper bound of N*sigma deviation. Example ------- >>> (ntwk_mean, ntwk_lb, ntwk_ub) = my_ntwk_set.uncertainty_ntwk_triplet('s') >>> (ntwk_mean, ntwk_lb, ntwk_ub) = my_ntwk_set.uncertainty_ntwk_triplet('s_mag') """ ntwk_mean = getattr(self, 'mean_'+attribute) ntwk_std = getattr(self, 'std_'+attribute) ntwk_std.s = n_deviations * ntwk_std.s upper_bound = (ntwk_mean + ntwk_std) lower_bound = (ntwk_mean - ntwk_std) return (ntwk_mean, lower_bound, upper_bound)
[docs] def datetime_index(self) -> list: """ Create a datetime index from networks names. this is just: `[rf.now_string_2_dt(k.name ) for k in self]` """ return [now_string_2_dt(k.name ) for k in self]
# io
[docs] def write(self, file=None, *args, **kwargs): r""" Write the NetworkSet to disk using :func:`~skrf.io.general.write` Parameters ---------- file : str or file-object filename or a file-object. If left as None then the filename will be set to Calibration.name, if its not None. If both are None, ValueError is raised. \*args, \*\*kwargs : arguments and keyword arguments passed through to :func:`~skrf.io.general.write` Notes ----- If the self.name is not None and file is can left as None and the resultant file will have the `.ns` extension appended to the filename. Examples --------- >>> ns.name = 'my_ns' >>> ns.write() See Also --------- skrf.io.general.write skrf.io.general.read """ # this import is delayed until here because of a circular dependency from .io.general import write if file is None: if self.name is None: raise (ValueError('No filename given. You must provide a filename, or set the name attribute')) file = self.name write(file, self, *args, **kwargs)
[docs] def write_spreadsheet(self, *args, **kwargs): """ Write contents of network to a spreadsheet, for your boss to use. Example ------- >>> ns.write_spreadsheet() # the ns.name attribute must exist >>> ns.write_spreadsheet(file_name='testing.xlsx') See Also --------- skrf.io.general.network_2_spreadsheet """ from .io.general import networkset_2_spreadsheet networkset_2_spreadsheet(self, *args, **kwargs)
[docs] def write_mdif(self, filename: str, values: dict | None = None, data_types: dict | None = None, comments = None): """Convert a scikit-rf NetworkSet object to a Generalized MDIF file. Parameters ---------- filename : string Output MDIF file name. values : dictionary or None. Default is None. The keys of the dictionnary are MDIF variables and its values are a list of the parameter values. If None, then the values will be set to the NetworkSet names and the datatypes will be set to "string". data_types: dictionary or None. Default is None. The keys are MDIF variables and the value are datatypes specified by the following strings: "int", "double", and "string" comments: list of strings Comments to add to output_file. Each list items is a separate comment line See Also -------- from_mdif : Create a NetworkSet from a MDIF file. params_values : parameters values params_types : parameters types """ from .io import Mdif if comments is None: comments = [] Mdif.write(ns=self, filename=filename, values=values, data_types=data_types, comments=comments)
[docs] def ntwk_attr_2_df(self, attr='s_db',m=0, n=0, *args, **kwargs): """ Converts an attributes of the Networks within a NetworkSet to a Pandas DataFrame. Examples -------- >>> df = ns.ntwk_attr_2_df('s_db', m=1, n=0) >>> df.to_excel('output.xls') # see Pandas docs for more info """ from pandas import DataFrame, Index, Series index = Index( self[0].frequency.f_scaled, name='Freq(%s)'%self[0].frequency.unit ) df = DataFrame( {'%s'%(k.name): Series(getattr(k, attr)[:,m,n],index=index) for k in self}, index = index, ) return df
[docs] def interpolate_from_network(self, ntw_param: ArrayLike, x: float, interp_kind: str = 'linear'): """ Interpolate a Network from a NetworkSet, as a multi-file N-port network. Assumes that the NetworkSet contains N-port networks with same number of ports N and same number of frequency points. These networks differ from an given array parameter `interp_param`, which is used to interpolate the returned Network. Length of `interp_param` should be equal to the length of the NetworkSet. Parameters ---------- ntw_param : (N,) array_like A 1-D array of real values. The length of ntw_param must be equal to the length of the NetworkSet x : real Point to evaluate the interpolated network at interp_kind: str Specifies the kind of interpolation as a string: 'linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'. See :class:`scipy.interpolate.interp1d` for detailed description. Default is 'linear'. Returns ------- ntw : class:`~skrf.network.Network` Network interpolated at x Example ------- Assuming that `ns` is a NetworkSet containing 3 Networks (length=3) : >>> param_x = [1, 2, 3] # a parameter associated to each Network >>> x0 = 1.5 # parameter value to interpolate for >>> interp_ntwk = ns.interpolate_from_network(param_x, x0) """ ntw = self[0].copy() # Interpolating the scattering parameters s = npy.array([self[idx].s for idx in range(len(self))]) f = interp1d(ntw_param, s, axis=0, kind=interp_kind) ntw.s = f(x) return ntw
[docs] def has_params(self) -> bool: """ Check is all Networks in the NetworkSet have a similar params dictionnary. Returns ------- bool True is all Networks have a .params dictionnay (of same size), False otherwise """ # does all networks have a params property? if not all(hasattr(ntwk, 'params') for ntwk in self.ntwk_set): return False # are all params property been set? if any(ntwk.params is None for ntwk in self.ntwk_set): return False # are they all of the same size? params_len = len(self.ntwk_set[0].params) if not all(len(ntwk.params) == params_len for ntwk in self.ntwk_set): return False # are all the dict keys the same? params_keys = self.ntwk_set[0].params.keys() if not all(ntwk.params.keys() == params_keys for ntwk in self.ntwk_set): return False # then we are all good return True
@property def params(self) -> list: """ Return the list of parameter names stored in the Network of the NetworkSet. Similar to the `dims` property, except it returns a list instead of a view. Returns ------- list: list list of the parameter names if any. Empty list if no parameter found. """ return list(self.dims) @property def params_values(self) -> dict | None: """ Return a dictionnary containing all parameters and their values. Returns ------- values : dict or None. Dictionnary of all parameters names and their values (into a list). Return None if no parameters are defined in the NetworkSet. """ if self.has_params(): # creating a dict of empty lists for each of the param keys values = {key: [] for key in self.dims} for ntwk in self.ntwk_set: for key, value in ntwk.params.items(): values[key].append(value) return values else: return None @property def params_types(self) -> dict | None: """ Return a dictionnary describing the data type of each parameters. Returns ------- data_types : dict or None. Dictionnary of the (guessed) type of each parameters. Return None if no parameters are defined in the NetworkSet. """ # for each parameter, scan all the value and try to guess the type # If is not a int, and not a float (double), then it's a string if self.has_params(): data_types = {} values = self.params_values for key in values: try: _ = [int(v) for v in values[key]] data_types[key] = 'int' except ValueError: # not an int try: _ = [float(v) for v in values[key]] data_types[key] = 'double' except ValueError: # not a float -> then a string data_types[key] = 'string' return data_types else: return None
[docs] def sel(self, indexers: Mapping[Any, Any] = None) -> NetworkSet: """ Select Network(s) in the NetworkSet from a given value of a parameter. Parameters ---------- indexers : dict, optional A dict with keys matching dimensions and values given by scalars, or arrays of parameters. Default is None, which returns the entire NetworkSet Returns ------- ns : NetworkSet NetworkSet containing the selected Networks or empty NetworkSet if no match found Example ------- Creating a dummy example: >>> params = [ {'a':0, 'X':10, 'c':'A'}, {'a':1, 'X':10, 'c':'A'}, {'a':2, 'X':10, 'c':'A'}, {'a':1, 'X':20, 'c':'A'}, {'a':0, 'X':20, 'c':'A'}, ] >>> freq1 = rf.Frequency(75, 110, 101, 'ghz') >>> ntwks_params = [rf.Network(frequency=freq1, s=np.random.rand(len(freq1),2,2), name=f'ntwk_{m}', comment=f'ntwk_{m}', params=params) \ for (m, params) in enumerate(params) ] >>> ns = rf.NetworkSet(ntwks_params) Selecting the sub-NetworkSet matching scalar parameters: >>> ns.sel({'a': 1}) # len == 2 >>> ns.sel({'a': 0, 'X': 10}) # len == 1 Selectong the sub-NetworkSet matching a range of parameters: >>> ns.sel({'a': 0, 'X': [10,20]}) # len == 2 >>> ns.sel({'a': [0,1], 'X': [10,20]}) # len == 4 If using a parameter name of value that does not exist, returns empty NetworkSet: >>> ns.sel({'a': -1}) # len == 0 >>> ns.sel({'duh': 0}) # len == 0 """ from collections.abc import Iterable if not indexers: # None or {} return self.copy() if not self.has_params(): return NetworkSet() if not isinstance(indexers, dict): raise TypeError('indexers should be a dictionnary.') for p in indexers.keys(): if p not in self.dims: return NetworkSet() ntwk_list = [] for k in self.ntwk_set: match_list = [k.params[p] in (v if isinstance(v, Iterable) else [v]) for (p, v) in indexers.items()] if all(match_list): ntwk_list.append(k) if ntwk_list: return NetworkSet(ntwk_list) else: # no match found return NetworkSet()
[docs] def interpolate_from_params(self, param: str, x: float, sub_params: dict=None, interp_kind: str = 'linear'): """ Interpolate a Network from given parameters of NetworkSet's Networks. Parameters ---------- param : string Name of the parameter to interpolate the NetworkSet with x : float Point to evaluate the interpolated network at sub_params : dict, optional Dictionnary of parameter/values to filter the NetworkSet, if necessary to avoid an ambiguity. Default is empty dict. interp_kind: str Specifies the kind of interpolation as a string: 'linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'. Cf :class:`scipy.interpolate.interp1d` for detailed description. Default is 'linear'. Returns ------- ntw : class:`~skrf.network.Network` Network interpolated at x Raises ------ ValueError : if the interpolating param/value are incorrect or ambiguous Example ------- Creating a dummy example: >>> params = [ {'a':0, 'X':10, 'c':'A'}, {'a':1, 'X':10, 'c':'A'}, {'a':2, 'X':10, 'c':'A'}, {'a':1, 'X':20, 'c':'A'}, {'a':0, 'X':20, 'c':'A'}, ] >>> freq1 = rf.Frequency(75, 110, 101, 'ghz') >>> ntwks_params = [rf.Network(frequency=freq1, s=np.random.rand(len(freq1),2,2), name=f'ntwk_{m}', comment=f'ntwk_{m}', params=params) \ for (m, params) in enumerate(params) ] >>> ns = rf.NetworkSet(ntwks_params) Interpolated Network for a=1.2 within X=10 Networks: >>> ns.interpolate_from_params('a', 1.2, {'X': 10}) """ # checking interpolating param and values if sub_params is None: sub_params = {} if param not in self.params: raise ValueError(f'Parameter {param} is not found in the NetworkSet params.') if isinstance(x, Number): if not (min(self.coords[param]) < x < max(self.coords[param])): ValueError(f'Out of bound values: {x} is not inside {self.coords[param]}. Cannot interpolate.') else: raise ValueError('Cannot interpolate between string-based parameters.') # checking sub-parameters if sub_params: for (p, v) in sub_params.items(): # of course it should exist if p not in self.dims: raise ValueError(f'Parameter {p} is not found in the NetworkSet params.') # check if each sub-param exist in the parameters if v not in self.coords[p]: # also deals with string case raise ValueError(f'Parameter {p} value {v} is not found in the NetworkSet params.') # interpolating the sub-NetworkSet matching the passed sub-parameters sub_ns = self.sel(sub_params) interp_ntwk = sub_ns.interpolate_from_network(sub_ns.coords[param], x, interp_kind) return interp_ntwk
[docs] @copy_doc(skrf_plt.animate) def animate(self, *args, **kwargs): skrf_plt.animate(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_uncertainty_bounds_component) def plot_uncertainty_bounds_component(self, *args, **kwargs): skrf_plt.plot_uncertainty_bounds_component(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_minmax_bounds_component) def plot_minmax_bounds_component(self, *args, **kwargs): skrf_plt.plot_minmax_bounds_component(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_uncertainty_bounds_s_db) def plot_uncertainty_bounds_s_db(self, *args, **kwargs): skrf_plt.plot_uncertainty_bounds_s_db(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_minmax_bounds_s_db) def plot_minmax_bounds_s_db(self, *args, **kwargs): skrf_plt.plot_minmax_bounds_s_db(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_minmax_bounds_s_db10) def plot_minmax_bounds_s_db10(self, *args, **kwargs): skrf_plt.plot_minmax_bounds_s_db10(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_uncertainty_bounds_s_time_db) def plot_uncertainty_bounds_s_time_db(self, *args, **kwargs): skrf_plt.plot_uncertainty_bounds_s_time_db(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_minmax_bounds_s_time_db) def plot_minmax_bounds_s_time_db(self, *args, **kwargs): skrf_plt.plot_minmax_bounds_s_time_db(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_uncertainty_decomposition) def plot_uncertainty_decomposition(self, *args, **kwargs): skrf_plt.plot_uncertainty_decomposition(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.plot_logsigma) def plot_logsigma(self, *args, **kwargs): skrf_plt.plot_logsigma(self, *args, **kwargs)
[docs] @copy_doc(skrf_plt.signature) def signature(self, *args, **kwargs): skrf_plt.signature(self, *args, **kwargs)
[docs] def func_on_networks(ntwk_list, func, attribute='s',name=None, *args,\ **kwargs): r""" Applies a function to some attribute of a list of networks. Returns the result in the form of a Network. This means information that may not be s-parameters is stored in the s-matrix of the returned Network. Parameters ------------- ntwk_list : list of :class:`~skrf.network.Network` objects list of Networks on which to apply `func` to func : function function to operate on `ntwk_list` s-matrices attribute : string attribute of Network's in ntwk_list for func to act on \*args,\*\*kwargs : arguments and keyword arguments passed to func Returns --------- ntwk : :class:`~skrf.network.Network` Network with s-matrix the result of func, operating on ntwk_list's s-matrices Examples ---------- averaging can be implemented with func_on_networks by >>> func_on_networks(ntwk_list, mean) """ data_matrix = npy.array([getattr(ntwk, attribute) for ntwk in ntwk_list]) new_ntwk = ntwk_list[0].copy() new_ntwk.s = func(data_matrix,axis=0,**kwargs) if name is not None: new_ntwk.name = name return new_ntwk
# short hand name for convenience fon = func_on_networks
[docs] def getset(ntwk_dict, s, *args, **kwargs): r""" Creates a :class:`NetworkSet`, of all :class:`~skrf.network.Network`s objects in a dictionary that contain `s` in its key. This is useful for dealing with the output of :func:`~skrf.io.general.load_all_touchstones`, which contains Networks grouped by some kind of naming convention. Parameters ------------ ntwk_dict : dictionary of Network objects network dictionary that contains a set of keys `s` s : string string contained in the keys of ntwk_dict that are to be in the NetworkSet that is returned \*args,\*\*kwargs : passed to NetworkSet() Returns -------- ntwk_set : NetworkSet object A NetworkSet that made from values of ntwk_dict with `s` in their key Examples --------- >>>ntwk_dict = rf.load_all_touchstone('my_dir') >>>set5v = getset(ntwk_dict,'5v') >>>set10v = getset(ntwk_dict,'10v') """ ntwk_list = [ntwk_dict[k] for k in ntwk_dict if s in k] if len(ntwk_list) > 0: return NetworkSet( ntwk_list,*args, **kwargs) else: print('Warning: No keys in ntwk_dict contain \'%s\''%s) return None
def tuner_constellation(name='tuner', singlefreq=76, Z0=50, r_lin = 9, phi_lin=21, TNWformat=True): r = npy.linspace(0.1,0.9,r_lin) a = npy.linspace(0,2*npy.pi,phi_lin) r_, a_ = npy.meshgrid(r,a) c_ = r_ *npy.exp(1j * a_) g= c_.flatten() x = npy.real(g) y = npy.imag(g) if TNWformat : TNL = dict() # for ii, gi in enumerate(g) : for ii, gi in enumerate(g) : TNL['pos'+str(ii)] = Network(f = [singlefreq ], s=[[[gi]]], z0=[[Z0]], name=name +'_' + str(ii)) TNW = NetworkSet(TNL, name=name) return TNW, x,y,g else : return x,y,g