Transient flicker noise modulation

ground 0 

load "res.va"
load "capacitor.osdi"

model resistor res
model isource isource
model capacitor capacitor

// Noiseless response
r0 (10 0) resistor r=1k
i0 (0 10) isource type="sine" sinedc=0.0 ampl=ampl freq=fmod

// Modulated shot noise
r1 (1 0) resistor r=1k pf=1e-20 ef=1
i1 (0 1) isource type="sine" sinedc=0.0 ampl=ampl freq=fmod

control
  abort always

  // print instance("r1")

  // Modulating current amplitude and frequency
  var ampl=2.0
  var fmod=1k

  // Transient noise frequency range
  var fmin=20.0
  var fmax=10k
  
  save p(r0,i) default
  options sweep_debug=0 op_debug=0 tran_debug=0 tran_noisedebug=0
  options rawfile="binary" strictsave=1
  options reltol=1e-3 vntol=1e-6 abstol=1e-12
  options tran_method="trap"
  analysis tran1 tran stop=40/fmin step=1m noisefmax=fmax noisefmin=fmin
  
  // Flicker noise, not accounting for modulation (I^2=1)
  alter instance("i1") sinedc=1
  elaborate changes
  analysis noise1 noise out="1" in="i1" from=fmin to=fmax mode="dec" points=200
  
  postprocess(PYTHON, "runme.py")
endc

embed "res.va" <<<FILE
`include "constants.vams"
`include "disciplines.vams"

module res(A,B);
    // Simple resistor with constant white, shot, and flicker noise
    
    // Terminals and internal nodes
    inout A, B;
    electrical A,B;
    
    branch (A,B) br;
	
	(*desc= "Resistance", type="instance", units = "Ohm"*) parameter real r = 1 from [0:inf];
    (*desc= "Noise switch", type="instance", units = ""*)  parameter integer noisy = 1;
    (*desc= "White noise factor (constant)", type="instance", units = ""*)  parameter real pw0 = 0;
    (*desc= "White noise factor (current)", type="instance", units = ""*)  parameter real pw1 = 0;
    (*desc= "Flicker noise power", type="instance", units = ""*)  parameter real pf = 0;
    (*desc= "Flicker noise exponent", type="instance", units = ""*)  parameter real ef = 1.0;
	(*desc= "Current", units ="A" *) real i;
    
    analog begin
        I(br) <+ V(br)/r;
        i = V(br) / r;
        if (noisy) begin
            I(br) <+ white_noise(pw0, "white_const");
            I(br) <+ white_noise(pw1*2*`P_Q*i, "white_shot");
            I(br) <+ flicker_noise((i>=0?1:-1)*pf*pow(abs(i),2), ef, "flicker");
        end
	end
endmodule

>>>FILE

embed "runme.py" <<<FILE
from rawfile import rawread
from vacaskpp import *
from runtest import *
import numpy as np
from scipy.signal import welch
import sys

tests=[]

plot = rawread('tran1.raw').get()
x = plot['time']
y = (plot['1'] - plot['10'])/1e3
print("Samples:", x.shape[0])

noise = rawread('noise1.raw').get()
reff = noise['frequency']
refpsd = noise['onoise']/1e3**2
fmin = reff.min()
fmax = reff.max()
# Extract current amplitude
ampl = plot['r0.i'].max()
# Modulation frequency (copy from simulation setup)
f0 = 1e3

# Compute modulated noise according to Coram, McAndrew, Gullapalli, and Kundert
# We simulate small-signal noise from fmin to fmax
# Consequently we can compute modulated noise PSD for frequency ranges
#   fmin..f0-fmin and f0+fmin..fmax-fmin

# Compute scale for LF range fmin..f0-fmin, interpolate refpsd
npts1 = int(np.ceil(np.log((f0-fmin)/fmin)/np.log(10)*200))
dxi1 = np.log((f0-fmin)/fmin)/npts1
reff1 = np.exp(np.arange(np.log(fmin), np.log(f0-fmin) + dxi1, dxi1))
# Compute scale for HF range f0+fmin..fmax-fmin
npts2 = int(np.ceil(np.log((fmax-fmin)/(f0+fmin))/np.log(10)*200))
dxi2 = np.log((fmax-fmin)/(f0+fmin))/npts2
reff2 = np.exp(np.arange(np.log(f0+fmin), np.log(fmax-fmin) + dxi2, dxi2))

# Scale with squared sine amplitude
# Compute reference modulated PSD, LF range
refpsd1 = ampl**2*(np.interp(np.abs(f0-reff1), reff, refpsd) + np.interp(np.abs(f0+reff1), reff, refpsd))/4
# Compute reference modulated PSD, HF range
refpsd2 = ampl**2*(np.interp(np.abs(f0-reff2), reff, refpsd) + np.interp(np.abs(f0+reff2), reff, refpsd))/4

# Average timestep should be <= timestep limit
dx = x.max()/x.shape[0]
xs = np.arange(x[0], x[-1] + dx, dx)
ys = np.interp(xs, x, y)

# Compute nperseg so that frequency resolution is fmin/2
N = xs.shape[0]
nperseg = int(np.ceil(1/dx/(fmin/2)))
print("Interpolated points:", N)
print("Segment length:", nperseg)
print("Segments:", int(np.floor(N/(nperseg/2)-1)))

f, Pxx = welch(
    ys,
    fs=1/dx,
    window='hann',
    nperseg=nperseg,      
    noverlap=nperseg/2,      
    return_onesided=True,
    scaling='density', 
    average='mean'
)

ferr1, err1 = errorWrtRef(reff1, refpsd1, f, Pxx, mode='db10')
ferr2, err2 = errorWrtRef(reff2, refpsd2, f, Pxx, mode='db10')

Pxxi1 = np.interp(reff1, f, Pxx)
Pxxi2 = np.interp(reff2, f, Pxx)
    
diff = np.abs(err1).max()
status = diff<3
print("max lf err [dB] =", diff, status)
tests.append(status)

diff = np.abs(err2).max()
status = diff<3
print("max hf err [dB] =", diff, status)
tests.append(status)

if isTest():
    sys.exit(not all(tests))
else:
    import matplotlib.pyplot as plt 

    fig1, ax1 = plt.subplots(3, 1, figsize=(6,8), dpi=100)
    fig1.axes[0].set_title('Modulated flicker noise, af=2, alpha=1, fmod=1kHz')
    fig1.axes[0].set_ylabel('In [A]')
    fig1.axes[0].set_xlabel('time [ms]')
    yndx = np.where(x<2e-3)
    fig1.axes[0].plot(x[yndx]*1000, y[yndx], label="V(1) noise")
    
    fig1.axes[1].set_title('Power spectral density')
    fig1.axes[1].set_ylabel('PSD [A^2/Hz]')
    fig1.axes[1].set_xlabel('f [Hz]')

    fig1.axes[1].loglog(f, Pxx, label="PSD")
    # fig1.axes[1].set_ylim(refpsd.min()/10, refpsd.max()*10)
    fig1.axes[1].loglog(reff1, refpsd1, 'r', label="PSD (reference, LF)")
    fig1.axes[1].loglog(reff2, refpsd2, 'r--', label="PSD (reference, HF)")
    fig1.axes[1].legend(loc='lower left')

    fig1.axes[2].set_title('Error')
    fig1.axes[2].set_ylabel('Error [dB]')
    fig1.axes[2].set_xlabel('f [Hz]')
    fig1.axes[2].semilogx(ferr1, err1, 'b', label="Error, LF")
    fig1.axes[2].semilogx(ferr2, err2, 'b--', label="Error, HF")
    fig1.axes[2].legend(loc='lower left')
    
    fig1.tight_layout()

    plt.show()
>>>FILE
