SLiCAP noise analysis — SLiCAP 4.0.7 documentation

# SLiCAP noise analysis

SLiCAP noise analysis helps to create design equations for the noise performance of a circuit.

Noise can be assigned to independent voltage and current sources and to resistors. SLiCAP also has buit-in subcircuits with noise sources.

SLiCAP output displayed on this manual page, is generated with the script: `noise.py`, imported by `Manual.py`.

    1#!/usr/bin/env python3
    2# -*- coding: utf-8 -*-
    3
    4"""
    5noise.py: SLiCAP scripts for the HTML help file
    6"""
    7import SLiCAP as sl
    8import sympy as sp
    9import numpy as np

## Noise parameters

During noise analysis, SLiCAP uses the `noise` parameter of independent voltage sources and independent current sources as uncorrelated noise sources. It inserts noise current sources in parallel with resistors with their noise spectrum determined by the parameters `noisetemp` and `noiseflow`.

    10# Create a circuit object
    11cir = sl.makeCircuit("kicad/noiseSources/noiseSources.kicad_sch")

    1noiseSources
    2C1 out 0 C value={C_a} vinit=0
    3I1 0 out I value=0 noise={S_i} dc=0 dcvar=0
    4L1 1 out L value={L_a} iinit=0
    5R1 1 2 R value={R_a} noisetemp={T} noiseflow={f_ell} dcvar=0 dcvarlot=0
    6V1 2 0 V value=0 noise={S_v} dc=0 dcvar=0
    7.end

### Noise voltage sources

The noise parameter of independent voltage sources must be specified in \(\mathrm{\left[ \frac{V^2}{Hz} \right] }\)

In the circuit above, the spectral density of the voltage noise from `V1` equals \(S_v\) \(\mathrm{\left[ \frac{V^2}{Hz} \right] }\).

### Noise current sources

The noise parameter of independent current sources must be specified in \(\mathrm{\left[ \frac{A^2}{Hz} \right] }\)

In the circuit above, the spectral density of the curent noise from `I1` equals \(S_i\) \(\mathrm{\left[ \frac{A^2}{Hz} \right] }\).

### Resistors

The noise contributed by resistors is governed by two parameters:

- `noisetemp`: The noise temperature of the resistor

- `noiseflow`: The flicker noise corner frequency

Addition of noise current sources

Before executing a `doNoise()` instruction, SLiCAP adds noise current sources in parallel with resistors that have a nonzero positive noise temperature. These current sources obtain the reference designator `I_noise_<resID>`, where `resID` is the reference designator of the resistor. After the noise analysis, all independent current sources with reference designators starting with `I_noise_` will be removed from the circuit.

The noise current spectral density \(S_\mathrm{I\_noise\_Rxx}\) of a resistor `Rxx` is set to:

\[S_\mathrm{I\_noise\_Rxx} = \frac{4kT_{noise_\mathrm{Rxx}}}{R_\mathrm{Rxx}} \left( 1 + \frac{f_{\ell_\mathrm{Rxx}}}{f}\right) \, \mathrm{\left[ \frac{A^2}{Hz}\right]}\]

where:

- \(k\) is Boltzmann’s constant in [J/K]

- \(T_{noise_\mathrm{Rxx}}\) is the noise temperature `noisetemp` of resistor `Rxx` in [K]

- \(R_\mathrm{Rxx}\) is the resistance `value` of `Rxx` in [Ohm]

- \(f_{\ell_\mathrm{Rxx}}\) is the flicker noise corner frequency `flow` of `Rxx` in [Hz]

In the circuit above:

- the noise temperature of the resistor `R1` is set to the gobal parameter \(T\)

- the flicker noise corner frequency equals \(f_{\ell}\)

Hence, SLiCAP will insert a noise current source in parallel with `R1` with a spectral density:

\[S_\mathrm{I\_noise\_R1} = \frac{4kT}{R_a} \left( 1 + \frac{f_\ell}{f} \right) \, \mathrm{\left[ \frac{A^2}{Hz} \right]}\]

### Global parameters

Global parameters are defined in the file `SLiCAPmodels.lib` in the folder given by `ini.main_lib_path`. If global parameters are found in circuit element expressions or in circuit parameter definitions, SLiCAP automatically adds their definition to the circuit parameter definitions.

In the circuit above, the global parameter \(T\) is found in the `noisetemp` parameter of `R1`. Its definition with a default value of 300 K is added to the circuit parameter definitions:

    13for par in cir.parDefs:
    14    print(par, cir.parDefs[par])

    T 300

## Noise Analysis

At the beginning of the noise analysis, SLiCAP adds resistor noise current sources to the circuit (see above). This also adds the Boltzmann constant \(k\) to the circuit parameter definitions:

    16noiseResult = sl.doNoise(cir, detector="V_out", source="V1")
    17
    18for par in cir.parDefs:
    19    print(par, cir.parDefs[par])

This yields:

    T 300
    k 34516213/2500000000000000000000000000000

Important

The function doNoise() sets the noise attributes `onoise`, `snoiseTerms`, and `onoiseTerms` of the returned instruction object. The `inoise` and `inoiseTerms` attributes are only set if the circuit has a source specification.

### Detector-referred noise

    21onoise      = noiseResult.onoise
    22print(onoise)

This yields:

    4*T*k*(f + f_ell)*Abs(R_a**2)/(R_a*f*(16*pi**4*C_a**2*L_a**2*f**4 + 4*pi**2*C_a**2*R_a**2*f**2 - 8*pi**2*C_a*L_a*f**2 + 1)) + S_i*(4*pi**2*L_a**2*f**2 + R_a**2)*Abs(R_a**2)/(R_a**2*(16*pi**4*C_a**2*L_a**2*f**4 + 4*pi**2*C_a**2*R_a**2*f**2 - 8*pi**2*C_a*L_a*f**2 + 1)) + S_v*Abs(R_a**2)/(R_a**2*(16*pi**4*C_a**2*L_a**2*f**4 + 4*pi**2*C_a**2*R_a**2*f**2 - 8*pi**2*C_a*L_a*f**2 + 1))

Formatted output:

(1)\[\begin{split}\begin{align} S_{vno} = & \frac{4 R_{a} T k \left(f + f_{\ell}\right)}{f \left(16 \pi^{4} C_{a}^{2} L_{a}^{2} f^{4} + 4 \pi^{2} C_{a}^{2} R_{a}^{2} f^{2} - 8 \pi^{2} C_{a} L_{a} f^{2} + 1\right)} \nonumber \\ & + \frac{S_{i} \left(4 \pi^{2} L_{a}^{2} f^{2} + R_{a}^{2}\right)}{16 \pi^{4} C_{a}^{2} L_{a}^{2} f^{4} + 4 \pi^{2} C_{a}^{2} R_{a}^{2} f^{2} - 8 \pi^{2} C_{a} L_{a} f^{2} + 1} \nonumber \\ & + \frac{S_{v}}{16 \pi^{4} C_{a}^{2} L_{a}^{2} f^{4} + 4 \pi^{2} C_{a}^{2} R_{a}^{2} f^{2} - 8 \pi^{2} C_{a} L_{a} f^{2} + 1} \,\left[\mathrm{\frac{V^2}{Hz}}\right] \end{align}\end{split}\]

### Source-referred noise

    24inoise      = noiseResult.inoise
    25print(inoise)

This yields:

> 4*R_a*T*k*(f + f_ell)/f + S_i*(4*pi**2*L_a**2*f**2 + R_a**2) + S_v

Formatted output:

(2)\[S_{vni} = \frac{4 R_{a} T k \left(f + f_{\ell}\right)}{f} + S_{i} \left(4 \pi^{2} L_{a}^{2} f^{2} + R_{a}^{2}\right) + S_{v}\,\,\left[\mathrm{\frac{V^2}{Hz}}\right]\]

### Contributions to detector-referred and source-referred noise

    27snoiseTerms = noiseResult.snoiseTerms
    28onoiseTerms = noiseResult.onoiseTerms
    29inoiseTerms = noiseResult.inoiseTerms
    30for term in snoiseTerms:
    31    print("\n= " + term + " =")
    32    print("value        :", snoiseTerms[term])
    33    print("det-referred :", onoiseTerms[term])
    34    print("src-referred :", inoiseTerms[term])

This yields:

    = I1 =
    value        : S_i
    det-referred : S_i*(4*pi**2*L_a**2*f**2 + R_a**2)*Abs(R_a**2)/(R_a**2*(16*pi**4*C_a**2*L_a**2*f**4 + 4*pi**2*C_a**2*R_a**2*f**2 - 8*pi**2*C_a*L_a*f**2 + 1))
    src-referred : S_i*(4*pi**2*L_a**2*f**2 + R_a**2)

    = V1 =
    value        : S_v
    det-referred : S_v*Abs(R_a**2)/(R_a**2*(16*pi**4*C_a**2*L_a**2*f**4 + 4*pi**2*C_a**2*R_a**2*f**2 - 8*pi**2*C_a*L_a*f**2 + 1))
    src-referred : S_v

    = I_noise_R1 =
    value        : 4*T*k*(1 + f_ell/f)/R_a
    det-referred : 4*T*k*(f + f_ell)*Abs(R_a**2)/(R_a*f*(16*pi**4*C_a**2*L_a**2*f**4 + 4*pi**2*C_a**2*R_a**2*f**2 - 8*pi**2*C_a*L_a*f**2 + 1))
    src-referred : 4*R_a*T*k*(f + f_ell)/f

Formatted output:

| Value | Units
--- | --- | ---
I1: Source value | \(S_{i}\) | \(\mathrm{\frac{A^2}{Hz}}\)
I1: Source-referred | \(S_{i} \left(4 \pi^{2} L_{a}^{2} f^{2} + R_{a}^{2}\right)\) | \(\mathrm{\frac{V^2}{Hz}}\)
I1: Detector-referred | \(\frac{S_{i} \left(4 \pi^{2} L_{a}^{2} f^{2} + R_{a}^{2}\right)}{16 \pi^{4} C_{a}^{2} L_{a}^{2} f^{4} + 4 \pi^{2} C_{a}^{2} R_{a}^{2} f^{2} - 8 \pi^{2} C_{a} L_{a} f^{2} + 1}\) | \(\mathrm{\frac{V^2}{Hz}}\)
V1: Source value | \(S_{v}\) | \(\mathrm{\frac{V^2}{Hz}}\)
V1: Source-referred | \(S_{v}\) | \(\mathrm{\frac{V^2}{Hz}}\)
V1: Detector-referred | \(\frac{S_{v}}{16 \pi^{4} C_{a}^{2} L_{a}^{2} f^{4} + 4 \pi^{2} C_{a}^{2} R_{a}^{2} f^{2} - 8 \pi^{2} C_{a} L_{a} f^{2} + 1}\) | \(\mathrm{\frac{V^2}{Hz}}\)
I_noise_R1: Source value | \(\frac{4 T k \left(1 + \frac{f_{\ell}}{f}\right)}{R_{a}}\) | \(\mathrm{\frac{A^2}{Hz}}\)
I_noise_R1: Source-referred | \(\frac{4 R_{a} T k \left(f + f_{\ell}\right)}{f}\) | \(\mathrm{\frac{V^2}{Hz}}\)
I_noise_R1: Detector-referred | \(\frac{4 R_{a} T k \left(f + f_{\ell}\right)}{f \left(16 \pi^{4} C_{a}^{2} L_{a}^{2} f^{4} + 4 \pi^{2} C_{a}^{2} R_{a}^{2} f^{2} - 8 \pi^{2} C_{a} L_{a} f^{2} + 1\right)}\) | \(\mathrm{\frac{V^2}{Hz}}\)

## Subcircuits with noise

### Built-in subcircuits

Below an overview of subcircuits and symbols for noise analysis. Subcircuits are defined in the library `SLiCAP.lib` in the folder indicated by `ini.main_lib_path`.

subcircuit name | description | parameters | KiCAD | gschem/Lepton-EDA | LTspice
--- | --- | --- | --- | --- | ---
N_noise | Nullor with equivalent-input noise sources | si, sv | N_noise | N_noise | SLN_noise
O_noise | Nullor with equivalent-input noise sources | si, sv | O_noise | O_noise | SLO_noise
MN18_noise | NMOS 180nm equivalent-input noise EKV model | ID, IG, W, L | M_noise | M_noise | SLM_noise
MP18_noise | PMOS 180nm equivalent-input noise EKV model | ID, IG, W, L | M_noise | M_noise | SLM_noise
MN18_noisyNullor | Nullor with NMOS 180nm equivalent-input noise EKV model | ID, IG, W, L | XM_noisyNullor | XM_noisyNullor | SLM_noisyNullor
MP18_noisyNullor | Nullor with PMOS 180nm equivalent-input noise EKV model | ID, IG, W, L | XM_noisyNullor | XM_noisyNullor | SLM_noisyNullor
MN18_noisyNullor_simple | Nullor with NMOS 180nm equivalent-input noise EKV model | ID, IG, W, L | XM_noisyNullor | XM_noisyNullor | SLM_noisyNullor
MP18_noisyNullor_simple | Nullor with PMOS 180nm equivalent-input noise EKV model | ID, IG, W, L | XM_noisyNullor | XM_noisyNullor | SLM_noisyNullor
J_noise | MOS/JFET equivalent-input noise sources | ID, IG, W, L | J_noise | M_noise | SLM_noise
Q_noise | BJT equivalent-input noise sources, r_b=0 | IC, VCE | Q_noise | Q_noise | SLQ_noise

Wide table: slide below the table!

The subcircuits `MN18_noisyNullor_simple` and `MP18_noisyNullor_simple` have their noise parameters `KF` and `Gamma`, as well as their input capacitance `c_iss` modeled independent of the inversion coefficient.

### Create noise elements

The user can create noisy circuits or subcircuits using independent noise sources, independend current sources, resistors and predefined noisy subcircuits from above.

## Noise post processing functions

### RMS noise

SLiCAP can perform symbolic and numeric integration and calculate the source-referred or detector-referred rmsNoise .

rmsNoise() calculates the RMS value by integrating each of the noise terms in the `onoiseTerms` or the `inoiseTerms` attribute of the noise analysis result. For each term it tries to find the best integration method. However, success is not always guaranteed:

- Symbolic integration of functions with many variables, may take very long

- Symbolic integration of functions is not always possible or implemented in Sympy.

The following may help:

- Keep the circuit model as simple as possible and use only one or two symbolic design parameters.

- Calculate the variance instead of the RMS noise (standard deviation), and try to obtain expressions with symbolic variables as coefficients of numeric integrals (see below).

Below an example how to express the RMS noise in terms of the source spectral densities \(S_v\) and \(S_i\).

    36params = {"R_a"  : "1k",
    37          "C_a"  : "10u",
    38          "L_a"  : "1m",
    39          "f_ell": 100}
    40
    41cir.defPars(params)
    42
    43numNoise = sl.doNoise(cir, detector="V_out", pardefs="circuit", numeric=True)
    44
    45f_min = 1e3
    46f_max = 1e6
    47
    48RMS = sl.rmsNoise(numNoise, 'onoise', f_min, f_max)
    49print(RMS)

This yields:

    711.447029712076*sqrt(S_i + 4.95576373211838e-7*S_v + 7.6162627945633e-24)

Typesetted:

\[V_{nRMS} = 711.4 \left(S_{i} + 4.956 \cdot 10^{-7} S_{v} + 7.616 \cdot 10^{-24}\right)^{0.5}\,\,\left[\mathrm{V}\right]\]

### Noise Figure

The noise figure of a system is a measure for the deterioration of the signal-to-noise ratio by that system. It can be found as the ratio of the total weighted output noise power (variance) and the contribution of the signal source noise to it. Alternatively, one can take the ratio of the total weighted source-referred noise and the weighted source noise.

    51# noise figure; V1 is signal source, with associated noise spectrum S_v
    52o_var   = RMS**2
    53src_var = sl.rmsNoise(numNoise, 'onoise', f_min, f_max, source="V1")**2
    54noiseF  = sp.expand(o_var/src_var)
    55print(noiseF)

This yields:

    2017852.45232533*S_i/S_v + 1.0 + 1.53684945575637e-17/S_v

Commonly the noise figure is expressed in [dB]:

    56F       = 10*sp.log(noiseF)/sp.log(10)
    57print(F)

This yields:

    10*log(2017852.45232533*S_i/S_v + 1.0 + 1.53684945575637e-17/S_v)/log(10)

Typesetted:

\[F = \frac{10 \log{\left(\frac{2.018 \cdot 10^{6} S_{i}}{S_{v}} + 1 + \frac{1.537 \cdot 10^{-17}}{S_{v}} \right)}}{\log{\left(10 \right)}}\,\,\left[\mathrm{dB}\right]\]

### Noise weighting functions

#### DIN A

SLiCAP has the frequency weighting function DIN_A(), which is often used for spectral weighting of noise and distortion components in audio applications. See WiKi R_A(f).

    59print(sl.DIN_A())

    18719114681919*f**4/(100000*sqrt((f**2 + 1159929/100)*(f**2 + 54449641/100))*(f**2 + 10609/25)*(f**2 + 148693636))

    61# Plot the DIN A weighting curve
    62frequencies = np.geomspace(10, 20e3, 200, endpoint=True)
    63din_A_func  = sp.lambdify(sl.ini.frequency, 20*sp.log(sl.DIN_A())/sp.log(10))
    64din_A_num   = din_A_func(frequencies)
    65din_A_trace = sl.trace([frequencies, din_A_num])
    66din_A_trace.label = "DIN A"
    67traceDict   = {"DINA": din_A_trace}
    68sl.plot("DINA", "DIN A weighting curve", "semilogx", traceDict,
    69        xName="Frequency", xUnits="Hz", yUnits="dB", show=False)

#### Correlated Double Sampling

Correlated Double Sampling is a technique that suppresses low-frequency noise by periodically taking the difference between two signal samples at a fixed time difference. It is applied in switched integrators used in optical detectors and image sensors.

The function doCDS(), takes a noise spectrum and a time delay as argument and returns the CDS weighted noise spectrum.

    71S_v, tau     = sp.symbols("S_v, tau")
    72cds_weighted = sl.doCDS(S_v, tau)
    73print(cds_weighted)

This yields:

    4*S_v*sin(pi*f*tau)**2

Typesetted:

\[S_{vCDS} = 4 S_{v} \sin^{2}{\left(\pi f \tau \right)}\,\,\left[\mathrm{\frac{V^2}{Hz}}\right]\]

Important

The function doCDS() multiplies the input spectrum with the squared magnitude of the CDS transfer function. Hence, the input spectrum must be in \(\mathrm{\left[ \frac{V^2}{Hz}\right]}\), \(\mathrm{\left[ \frac{A^2}{Hz}\right]}\) or \(\mathrm{\left[ \frac{W}{Hz}\right]}\). Use the `onoise` or `inoise` attribute of the noise analysis result!

    75# Plot the CDS weighting curve for tau = 1 us
    76freqs       = np.geomspace(5e3, 4.5e6, 1000)
    77cds_func  = sp.lambdify(sl.ini.frequency, 10*sp.log(sl.doCDS(sp.N(1), 1e-6))/sp.log(10))
    78cds_num   = cds_func(freqs)
    79cds_trace = sl.trace([freqs, cds_num])
    80cds_trace.label = "CDS"
    81traceDict   = {"CDS": cds_trace}
    82sl.plot("CDS", "CDS weighting curve", "semilogx", traceDict,
    83        xName="Frequency", xUnits="Hz", yUnits="dB", show=False)

The function doCDSint performs both CDS filtering and integration over frequency.

Below the script that calculates the variance of the output voltage of a switched transimpedance integrator with a gain \(Z_t=\frac{1}{2\pi f C_i}\) and an input noise current with a spectral density \(S_i\) to which CDS filtering of the output signal with a time delay \(\tau\) is applied.

    85S_v = sp.sympify("S_i/(2*pi*f*C_i)^2")
    86var = sl.doCDSint(S_v, tau, 0, sp.oo)
    87print(var)

SLiCAP uses \(\_\phi=\pi f \tau\) rather than \(f\) as integration variable. Integration is not always possible because the integral may depend on the domain of the variables:

    S_i*tau*Integral(sin(_phi)**2/_phi**2, (_phi, 0, oo*tau))/(pi*C_i**2)

As shown above, the integral cannot be evaluated (undefined when \(\tau=0\). However, it can be done with the aid of assumptions.

### Use assumptions

Below the script to obtain the integral from above.

    89var = sl.assumePosParams(var).doit()
    90var = sl.clearAssumptions(var)
    91print(var)

With \(\tau>0\) the above integral can be evaluated:

    S_i*tau/(2*C_i**2)

Typesetted:

\[\sigma^{2} = \frac{0.5 S_{i} \tau}{C_{i}^{2}}\,\,\left[\mathrm{V^2}\right]\]

The following SLiCAP functions can be used to change the domain of variables:

1. assumePosParams()

2. assumeRealParams()

3. clearAssumptions()

### Obtain symbolic coefficients of numeric intergals

Sometimes numeric integration over frequency becomes possible after symbolic design parameters have been elimated from the integral.

The function integrate_monomial_coeffs() returns an experssion in which monomials of two selected parameters have been taken out of the integral:

    93Si, Sv = sp.symbols("S_i, S_v")
    94integrated_noise   = sl.integrate_monomial_coeffs(numNoise.onoise, [Si, Sv],
    95                                                  sl.ini.frequency, f_min, f_max,
    96                                                  doit=True)

This yields:

    253078.438043065*S_i + 0.250839388927001*S_v + 3.85502378354722e-18

Typesetted:

\[v_{n}^{2} = 2.531 \cdot 10^{5} S_{i} + 0.2508 S_{v} + 3.855 \cdot 10^{-18}\,\,\left[\mathrm{V^2}\right]\]

The function integrated_monomial_coeffs() returns a dictionary of which the keys are monomials consisting of two variables, and the coefficients of these monomials are numeric integrals.

     99integrated_noise_coeffs = sl.integrated_monomial_coeffs(numNoise.onoise, [Si, Sv],
    100                                       sl.ini.frequency, f_min, f_max, doit=False)

Typesetted output:

Table 20 Numeric integrals as coeffs of \(S_v\) and \(S_i\)
\(S_{i}\) | \(\int\limits_{1000}^{1.0 \cdot 10^{6}} \frac{1.0 \cdot 10^{38} \left(3.948 \cdot 10^{29} f^{2} + 1.0 \cdot 10^{40}\right)}{1.559 \cdot 10^{59} f^{4} + 3.947 \cdot 10^{69} f^{2} + 1.0 \cdot 10^{72}}\, df\)
\(S_{v}\) | \(\int\limits_{1000}^{1.0 \cdot 10^{6}} \frac{1.0 \cdot 10^{72}}{1.559 \cdot 10^{59} f^{4} + 3.947 \cdot 10^{69} f^{2} + 1.0 \cdot 10^{72}}\, df\)
\(1\) | \(\int\limits_{1000}^{1.0 \cdot 10^{6}} \frac{1.657 \cdot 10^{55} \left(f + 100\right)}{f \left(1.559 \cdot 10^{59} f^{4} + 3.947 \cdot 10^{69} f^{2} + 1.0 \cdot 10^{72}\right)}\, df\)

## Display equations on HTML pages and in LaTeX documents

The report module Create reports, discusses how HTML snippets and LaTeX snippets can be created for variables, expressions, equations and tables.

As a matter of fact, all equations, tables and figures on this page are created with this module:

    102# Create an RST formatter
    103rst = sl.RSTformatter()
    104# Save expressions in the sphinx/SLiCAPdata folder of the project directory
    105rst.eqn("S_vno", onoise, multiline=1, label="eqn-Svno", units="V**2/Hz").save("eqn-Svno")
    106rst.eqn("S_vni", inoise, label="eqn-Svni", units="V**2/Hz").save("eqn-Svni")
    107rst.noiseContribs(noiseResult, "Noise contributions").save("table-noiseContribs")
    108rst.eqn("V_nRMS", RMS, units="V").save("eqn-RMS")
    109rst.eqn("F", F, units="dB").save("eqn-F")
    110rst.eqn("S_vCDS", cds_weighted, units="V**2/Hz").save("eqn-CDSweighting")
    111rst.eqn("sigma^2", var, units="V**2").save("eqn-CDSint")
    112rst.eqn("v_n^2", integrated_noise, units="V**2").save("eqn-intCoeffs")
    113rst.dictTable(integrated_noise_coeffs,
    114              caption="Numeric integrals as coeffs of :math:`S_v` and :math:`S_i`").save("table-coeffsNoise")

The typesetted tables and equations are shown on this page.

## Plot noise spectrum

SLiCAP plot functions are discussed in Create plots.