pascalkuthe / OpenVAF

An innovative Verilog-A compiler
https://openvaf.semimod.de/
GNU General Public License v3.0
113 stars 15 forks source link

`white_noise()` PSD contributions can be negative #116

Open topolarity opened 3 months ago

topolarity commented 3 months ago

First of all, thanks for a creating such a great tool.

I've been exploring some of the noise support recently and ran into an issue. It seems that the sign of the PSD contribution of white_noise() can be negative, depending on the direction of the terminals/flow.

Here's an example that I believe shows the problem:

`include "disciplines.vams"
`include "constants.vams"

module resistor_osdi(a, b);
    inout a, b;
    electrical a, b;

    parameter real R=1 from (0.0:inf);

    real G;
    analog begin
        G = 1 / R;
        I(a, b) <+ G * V(a, b);
        I(b, a) <+ white_noise(G * 4 * `P_K * $temperature, "thermal");
    end
endmodule

Compile this with openvaf resistor.va -o resistor.osdi, and then perform .noise analysis in a simple circuit:

* Resistor (OSDI) test

.model R_1_Ohm resistor_osdi R = 1

NR1 out 0 R_1_Ohm
NR2 src out R_1_Ohm
V1 0 src 1 AC = 1

.control
pre_osdi ./resistor.osdi
set sqrnoise
noise v(out) v1 dec 5 1k 10k
setplot noise1
print onoise_spectrum
.endc

.END

results in:

Note: can't find the initialization file spinit.
******
** ngspice-42 : Circuit level simulation program
** Compiled with KLU Direct Linear Solver
** The U. C. Berkeley CAD Group
** Copyright 1985-1994, Regents of the University of California.
** Copyright 2001-2023, The ngspice team.
** Please get your ngspice manual from https://ngspice.sourceforge.io/docs.html
** Please file your bug-reports at http://ngspice.sourceforge.net/bugrep.html
** Creation Date: Sun Feb  4 03:12:21 UTC 2024
******

Note: No compatibility mode selected!

Added device: URC

Circuit: * resistor (osdi) test

Doing analysis at TEMP = 27.000000 and TNOM = 27.000000

Using SPARSE 1.3 as Direct Linear Solver

No. of Data Rows : 6

No. of Data Rows : 1
                             * resistor (osdi) test
                             Noise Spectral Density Curves - (V^2 or A^2)/Hz  Mon Mar 25 10:54:18  2024
--------------------------------------------------------------------------------
Index   frequency       onoise_spectrum
--------------------------------------------------------------------------------
0       1.000000e+03    -8.28804e-21
1       1.584893e+03    -8.28804e-21
2       2.511886e+03    -8.28804e-21
3       3.981072e+03    -8.28804e-21
4       6.309573e+03    -8.28804e-21
5       1.000000e+04    -8.28804e-21

Tested with:

dwarning commented 3 months ago

Can confirm the problem. In a first guess I think we can fix it in the ngspice-osdi interface. @metroid120 : Your opinion? BTW, I am not sure about constraints of contribution directions. Found nothing in LRM.

gjcoram commented 3 months ago

That's weird - I don't see a minus sign in the code you provided. I wonder if openvaf changes the sign of the contribution if it sees the terminals have been swapped. Eg I(a,b) <+ value1; I(b,a) <+ value2; gets rewritten as I(a,b) <+ value1; I(a,b) <+ -value2; perhaps in order to simplify the matrix pointers.

Note that standard ac noise analysis uses the magnitude of the transfer function from the nodes where the noise is injected to the circuit noise output node(s), and thus should be insensitive to whether it's I(a,b) or I(b,a). The sign of the noise power can be used in PNoise or HBnoise analysis -- please see https://ieeexplore.ieee.org/document/8957705 -- but the simulator must handle the minus sign.

gjcoram commented 3 months ago

I'm not set up to run these experiments myself today, but I'd be interested to know what you get if you change the second line of the example to I(a, b) <+ -white_noise(G 4 P_K * $temperature, "thermal"); or I(a, b) <+ white_noise(-G * 4 *P_K * $temperature, "thermal"); Does openvaf put in an absolute value in either or both of those cases?

topolarity commented 3 months ago

The sign appears to behave the same for the explicit unary minus or for the flow reversal:

I(a, b) <+  white_noise(G * 4 * `P_K * $temperature, "thermal"); // PSD > 0
I(a, b) <+ -white_noise(G * 4 * `P_K * $temperature, "thermal"); // PSD < 0 (wrong)
I(b, a) <+  white_noise(G * 4 * `P_K * $temperature, "thermal"); // PSD < 0 (wrong)
I(b, a) <+ -white_noise(G * 4 * `P_K * $temperature, "thermal"); // PSD > 0
dwarning commented 3 months ago

There is a preliminary fix in ngspice git branch pre-master-43.

topolarity commented 3 months ago

Thanks for the fix! That does seem to help the case above, but I think there may still be a bug here for other transformations:

I(a, b) <+ 2 * white_noise(G * 4 * `P_K * $temperature, "thermal");

I would have expected this to quadruple the noise power (3.3152176e-20), but instead the power merely doubles (1.657609e-20)

dwarning commented 3 months ago

I am not sure, but I would expect factor of 2 for psd and a factor of sqrt(2) for voltage spectral density. An that is what I got.

topolarity commented 3 months ago

I believe that disagrees with the results from this circuit:

* Resistor (OSDI) test

.model R_1_Ohm resistor_osdi R = 1

NR1 src 0 R_1_Ohm
V1 src 0 1 AC = 1
H1 out 0 V1 1.0

.control
pre_osdi ./resistor.osdi
set sqrnoise
noise v(out) v1 dec 5 1k 10k
setplot noise1
print onoise_spectrum
.endc

.END

I would expect the onoise_spectrum to be the same (a) if I set H1 to a transconductance of 2.0 Ω and leave the model as above, or (b) if I leave it at a transconductance of 1.0 Ω and multiply all of the model currents by 2x.

However, the results disagree by a factor of 2x.

gjcoram commented 3 months ago

onoise_spectrum in ngspice is reported in V^2/Hz. The onoise_spectrum is computed as the PSD of the noise source times the magnitude-squared of the transfer function (H times its complex conjugate). I think I agree with topolarity: the factor of '2' in front of the call to white_noise() is effectively doubling the transfer function from the white_noise function to the output, so that the magnitude-squared gets multiplied by four, and thus the onoise_spectrum should be multiplied by four.

dwarning commented 3 months ago

Speaking about the original request fro topolarity, because last reply is different:

ngspice in default gives onoise_spectrum in V/sqrt(Hz), the voltage spectral density. By 'set sqrnoise' you get power spectral density in V**2/Hz.

    I(a, b) <+ white_noise(G * 4 * `P_K * $temperature, "thermal");

//spectral density (unset sqrnoise) //onoise ngspice: 9.103869e-11 V/sqrt(Hz) //power spectral density (PSD set sqrnoise) //onoise ngspice: 8.288044e-21 V^2/Hz Xyce: 8.28804375e-2 V^2/Hz

// I(a, b) <+ 2 white_noise(G 4 `P_K $temperature, "thermal"); //spectral density (unset sqrnoise) //onoise ngspice: 1.287482e-10 V/sqr(Hz) (= *sqrt(2)) //power spectral density (PSD set sqrnoise) //onoise ngspice: 1.657609e-20 V^2/Hz Xyce: error in compilation

// I(a, b) <+ white_noise(G 4 P_K * $temperature, "thermal"); // I(a, b) <+ white_noise(G * 4 *P_K $temperature, "thermal"); //spectral density (unset sqrnoise) //onoise ngspice: 1.287482e-10 V/sqr(Hz) (= sqr(2)) //power spectral density (PSD set sqrnoise) //onoise ngspice: 1.657609e-20 V^2/Hz Xyce: 1.65760875e-20 V^2/Hz

If we have a factor of 2 what is the same as two contributions to same branch we can assume it is a parallel circuit of two uncorrelated noise sources. The resulting equivalent noise current rising with sqrt(N). This was the trick we have used by adapting our low source impedance magnetic heads to our cheap transistor pre-amps in the 70' last century. I'm curious about to see results of the commercial simulators.

gjcoram commented 3 months ago

I was unaware of sqrnoise; I see that topolarity did set that option.

gjcoram commented 3 months ago

If we have a factor of 2 what is the same as two contributions to same branch we can assume it is a parallel circuit of two uncorrelated noise sources.

I am not sure this is a valid way to understand the case. In particular, it seems that the factor of two in fact makes two correlated noise sources.

gjcoram commented 3 months ago

I have now checked two commercial simulators using I(b, a) <+ ns1 * white_noise(ns2 * G * 4 * P_K * $temperature, "thermal");

I get different answers for (ns1=1, ns2=2) than for (ns1=2, ns2=1).

gjcoram commented 2 months ago

To clarify my previous post: when ns1=ns2=1, the noise for R=1 at T=27 C is ~ 1.66e-20 V^2/Hz For ns1=1, ns2=2, the noise is 3.32e-20 V^2/Hz, which is twice the baseline For ns1=2, ns2=1, the noise is 6.63e-20 V^2/Hz, which is four times the baseline Both commercial simulators agree with each other (and with an internal proprietary simulator).

ngspice agrees for the baseline and the first case, but the second case matches the first, that is, I get twice the baseline instead of four times.

dwarning commented 2 months ago

@gjcoram : Thank you for clarification. Last question, how this is handled by the reference simulators? Same as factor ns1 = 2? I(a, b) <+ white_noise(G 4 P_K $temperature, "thermal"); I(a, b) <+ white_noise(G 4 P_K $temperature, "thermal"); Then I should litter my Motchenbacher, p 246.

tcaduser commented 2 months ago

@gjcoram : Thank you for clarification. Last question, how this is handled by the reference simulators? Same as factor ns1 = 2? I(a, b) <+ white_noise(G 4 P_K $temperature, "thermal"); I(a, b) <+ white_noise(G 4 P_K $temperature, "thermal"); Then I should litter my Motchenbacher, p 246.

My reading of this page from the LRM is that noise processes with the same name would be summed together as uncorrelated noise powers.

If they were correlated, you would need to look at the noise amplitudes, but I don't think there is a way to specify them.

I think the only way to have correlated noise is to use noise source with a special circuit to contribute its value to different branches.

image

topolarity commented 2 months ago

I agree with the interpretations above. For this case:

I(a, b) <+ white_noise(G * 4 * P_K * $temperature, "thermal");
I(a, b) <+ white_noise(G * 4 * P_K * $temperature, "thermal");

is equivalent to:

I(a, b) <+ sqrt(2) * white_noise(G * 4 * P_K * $temperature, "thermal");

And if the contributions had been re-used elsewhere so that there are observable correlations, such as:

N1 = white_noise(G * 4 * P_K * $temperature, "thermal");
N2 = white_noise(G * 4 * P_K * $temperature, "thermal");
I(a, b) <+ N1 + N2;
I(b, c) <+ N1;

then this cannot be reduced to a single independent noise source at all.

gjcoram commented 2 months ago

@tcaduser is correct; the noise sources are uncorrelated. And @topolarity is also correct; having the two noise sources is equivalent to having sqrt(2) in front of a single noise source. This would be ns1 = sqrt(1), or ns2 = 2, in my earlier code.