Source code for zero.analysis.ac.noise

import logging
import numpy as np

from .signal import AcSignalAnalysis
from ...data import NoiseDensity, MultiNoiseDensity, Series

LOGGER = logging.getLogger(__name__)


[docs]class AcNoiseAnalysis(AcSignalAnalysis): """Small signal circuit analysis""" DEFAULT_INPUT_IMPEDANCE = 50 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._noise_sink = None @property def noise_sink(self): return self._noise_sink @noise_sink.setter def noise_sink(self, sink): if not hasattr(sink, "name"): # This is an element name. Get the object. We use the user-supplied circuit here because # the copy may not have been created by this point. sink = self.circuit.get_element(sink) self._noise_sink = sink
[docs] def calculate(self, input_type, sink, impedance=None, incoherent_sum=False, input_refer=False, **kwargs): """Calculate noise from circuit elements at a particular element. Parameters ---------- input_type : str Input type, either "voltage" or "current". sink : str or :class:`.Component` or :class:`.Node` The element to calculate noise at. impedance : float or :class:`.Quantity`, optional Input impedance. If None, the default is used. incoherent_sum : :class:`bool` or :class:`dict`, optional Incoherent sum specification. If True, the incoherent sum of all noise in the circuit at the sink is calculated and added to the solution. Alternatively, this parameter can be specified as a dict containing labels as keys and sequences of noise sources as values. The noise sources can be either :class:`.NoiseDensity` objects or noise specifier strings as supported by :meth:`.Solution.get_noise`. The values may alternatively be the strings "all", "allop" or "allr" to compute noise from all components, all op-amps and all resistors, respectively. Sums are plotted in shades of grey determined by the plotting configuration's ``sum_greyscale_cycle_start``, ``sum_greyscale_cycle_stop`` and ``sum_greyscale_cycle_count`` values. input_refer : bool, optional Refer the noise to the input. Other Parameters ---------------- frequencies : :class:`np.ndarray` or sequence The frequency vector to calculate the response with. node, node_p, node_n : :class:`.Node` The node or nodes to make the input. The `node` parameter sets a single, grounded input, whereas `node_p` and `node_n` together create a floating input. print_equations : :class:`bool`, optional Print the circuit equations. print_matrix : :class:`bool`, optional Print the circuit matrix. Returns ------- :class:`~.solution.Solution` Solution containing noise spectra at the specified sink (or projected sink). """ self.noise_sink = sink if impedance is None: LOGGER.warning(f"assuming default input impedance of {self.DEFAULT_INPUT_IMPEDANCE}") impedance = self.DEFAULT_INPUT_IMPEDANCE self._do_calculate(input_type, impedance=impedance, is_noise=True, **kwargs) if incoherent_sum: self._compute_sums(incoherent_sum) if input_refer: self._refer_sink_noise_to_input() return self.solution
[docs] def circuit_matrix(self, *args, **kwargs): """Calculate and return matrix used to solve for circuit noise at a \ given frequency. Returns ------- :class:`scipy.sparse.spmatrix` The circuit matrix. """ # Return the transpose of the response matrix. return super().circuit_matrix(*args, **kwargs).T
@property def right_hand_side_index(self): """Right hand side excitation component index""" return self.noise_element_index def _build_solution(self, noise_matrix): # empty noise sources empty = [] # loop over circuit's noise sources for noise in self._current_circuit.noise_sources: # get this element's noise spectral density spectral_density = noise.spectral_density(frequencies=self.frequencies) if np.all(spectral_density) == 0: # null noise source empty.append(noise) if noise.element_type == "component": # noise is from a component; use its matrix index index = self.component_matrix_index(noise.component) elif noise.element_type == "node": # noise is from a node; use its matrix index index = self.node_matrix_index(noise.node) else: raise ValueError("unrecognised noise source present in circuit") # get response from this element to every other response = noise_matrix[index, :] # multiply response from element to noise output element by noise entering # at that element, for all frequencies projected_noise = np.abs(response * spectral_density) # create series series = Series(x=self.frequencies, y=projected_noise) # add noise function to solution self.solution.add_noise(NoiseDensity(source=noise, sink=self.noise_sink, series=series)) if empty: empty_sources = ", ".join([str(response) for response in empty]) LOGGER.debug(f"empty noise sources: {empty_sources}") def _compute_sums(self, sum_spec): """Compute incoherent noise sums and add them to the solution. Parameters ---------- sum_spec : :class:`bool` or :class:`dict` Incoherent sum specification. If True, the incoherent sum of all noise in the circuit at the sink is calculated and added to the solution. Alternatively, this parameter can be specified as a dict containing labels as keys and sequences of noise sources as values. The noise sources can be either :class:`.NoiseDensity` objects or noise specifier strings as supported by :meth:`.Solution.get_noise`. The values may alternatively be the strings "all", "allop" or "allr" to compute noise from all components, all op-amps and all resistors, respectively. Sums are plotted in shades of grey determined by the plotting configuration's ``sum_greyscale_cycle_start``, ``sum_greyscale_cycle_stop`` and ``sum_greyscale_cycle_count`` values. """ if sum_spec is True: # Sum using all noise and the default MultiNoiseDensity label. sum_spec = {None: self.solution.noise[self.solution.DEFAULT_GROUP_NAME]} for label, spectra in sum_spec.items(): if spectra is None: raise ValueError("noise sum spectra cannot be empty") if isinstance(spectra, str): identifier = spectra.lower() if identifier == "all": constituents = self.solution.noise[self.solution.DEFAULT_GROUP_NAME] elif identifier == "allop": constituents = self.solution.opamp_noise[self.solution.DEFAULT_GROUP_NAME] elif identifier == "allr": constituents = self.solution.resistor_noise[self.solution.DEFAULT_GROUP_NAME] else: raise ValueError(f"unrecognised noise collection '{spectra}'") else: constituents = [] for spectrum in spectra: if not isinstance(spectrum, NoiseDensity): spectrum = self.solution.get_noise(source=spectrum, sink=self.noise_sink) constituents.append(spectrum) self.solution.add_noise_sum(MultiNoiseDensity(constituents=constituents, sink=self.noise_sink, label=label)) def _refer_sink_noise_to_input(self): """Project the calculated noise to the input.""" LOGGER.info("projecting noise to input") input_component = self._current_circuit.input_component if self.input_type == "voltage": input_element = input_component.node2 else: input_element = input_component projection_analysis = self.to_signal_analysis() # Grab the input nodes from the noise circuit. node_n, node_p = input_component.nodes projection = projection_analysis.calculate(frequencies=self.frequencies, input_type=self.input_type, node_n=node_n, node_p=node_p) # Transfer function from input to noise sink. input_response = projection.get_response(source=input_element, sink=self.noise_sink) for __, noise_spectra in self.solution.noise.items(): for noise in noise_spectra: self.solution.replace(noise, noise * input_response.inverse()) for __, noise_sums in self.solution.noise_sums.items(): for noise in noise_sums: self.solution.replace(noise, noise * input_response.inverse())
[docs] def to_signal_analysis(self): """Return a new signal analysis using the settings defined in the current analysis.""" return AcSignalAnalysis(self.circuit, print_progress=self.print_progress, stream=self.stream)
@property def noise_element_index(self): """Noise element matrix index""" try: return self.component_matrix_index(self.noise_sink) except ValueError: pass try: return self.node_matrix_index(self.noise_sink) except ValueError: pass raise ValueError(f"noise output element '{self.noise_sink}' is not in the circuit")