From a40fe009242f47a30fb509326e9323a4f7ecc8be Mon Sep 17 00:00:00 2001 From: Meisam Date: Tue, 28 Jul 2026 17:16:27 +0200 Subject: [PATCH] pa-138 --- src/frontend/Makefile.am | 2 + src/frontend/com_commands.h | 1 + src/frontend/com_qpnoise.c | 82 +++++++++++++++++++ src/frontend/com_qpnoise.h | 7 ++ src/frontend/commands.c | 4 + src/include/ngspice/cktdefs.h | 1 + src/spicelib/analysis/dcpss.c | 149 ++++++++++++++++++++++++++++++++++ visualc/vngspice.vcxproj | 2 + 8 files changed, 248 insertions(+) create mode 100644 src/frontend/com_qpnoise.c create mode 100644 src/frontend/com_qpnoise.h diff --git a/src/frontend/Makefile.am b/src/frontend/Makefile.am index 3aac34495..07a77e90e 100644 --- a/src/frontend/Makefile.am +++ b/src/frontend/Makefile.am @@ -57,6 +57,8 @@ libfte_la_SOURCES = \ com_qpss.h \ com_qpac.c \ com_qpac.h \ + com_qpnoise.c \ + com_qpnoise.h \ com_hb.c \ com_hb.h \ com_checkpoint.c \ diff --git a/src/frontend/com_commands.h b/src/frontend/com_commands.h index aa29a374a..36db9e5f0 100644 --- a/src/frontend/com_commands.h +++ b/src/frontend/com_commands.h @@ -9,6 +9,7 @@ void com_alterparam(wordlist *wl); void com_optimize(wordlist *wl); /* Enhancement-130 */ void com_qpss(wordlist *wl); /* Enhancement-133 */ void com_qpac(wordlist *wl); /* Enhancement-137 */ +void com_qpnoise(wordlist *wl); /* Enhancement-138 */ void com_hb(wordlist *wl); /* Enhancement-134 */ void com_savestate(wordlist *wl); /* Enhancement-131 */ void com_loadstate(wordlist *wl); /* Enhancement-131 */ diff --git a/src/frontend/com_qpnoise.c b/src/frontend/com_qpnoise.c new file mode 100644 index 000000000..b1b1a4dd1 --- /dev/null +++ b/src/frontend/com_qpnoise.c @@ -0,0 +1,82 @@ +/********** +Enhancement-138: two-tone small-signal QPnoise (quasi-periodic noise) -- +`qpnoise `. + +Around the QPSS operating point retained by a prior `qpss hb`, folds +every device's noise through the ADJOINT of the 2-D conversion matrix over all sidebands +to the output at f_in -- the two-tone analogue of pnoise. A mixer/PA's device noise at +each sideband f_in + k1*f1 + k2*f2 is converted (folded) to the output. The engine is +QPnoiseAnalyze() (spicelib/analysis/dcpss.c); this command resolves the output node and +runs it. +**********/ + +#include "ngspice/ngspice.h" +#include "ngspice/cpdefs.h" +#include "ngspice/cktdefs.h" +#include "ngspice/ftedefs.h" +#include "ngspice/fteext.h" +#include "ngspice/wordlist.h" +#include "ngspice/cpextern.h" +#include "ngspice/ifsim.h" + +#include "com_qpnoise.h" + +static double qpnnum(const char *w) +{ + char *s = (char *) w; + double v = 0.0; + if (ft_numparse(&s, FALSE, &v) < 0) + v = atof(w); + return v; +} + +/* resolve a node name to its 1-based CKT node number via the name list, else 0 */ +static int qpn_node(CKTcircuit *ckt, const char *name) +{ + int numNames = 0, i, num = 0; + IFuid *nameList = NULL; + if (CKTnames(ckt, &numNames, &nameList) != OK || !nameList) + return 0; + for (i = 0; i < numNames; i++) + if (nameList[i] && strcmp((const char *) nameList[i], name) == 0) { + num = i + 1; + break; + } + tfree(nameList); + return num; +} + +void +com_qpnoise(wordlist *wl) +{ + CKTcircuit *ckt; + double f_in; + int outNode, verbose, err; + + if (!ft_curckt || !ft_curckt->ci_ckt) { + fprintf(cp_err, "Error: qpnoise: there is no circuit loaded.\n"); + return; + } + ckt = ft_curckt->ci_ckt; + + if (!wl || !wl->wl_next) { + fprintf(cp_err, "Usage: qpnoise " + "(run `qpss hb` first)\n"); + return; + } + outNode = qpn_node(ckt, wl->wl_word); + if (outNode <= 0) { + fprintf(cp_err, "Error: qpnoise: unknown output node '%s'.\n", wl->wl_word); + return; + } + f_in = qpnnum(wl->wl_next->wl_word); + if (f_in <= 0.0) { + fprintf(cp_err, "Error: qpnoise: need f_in > 0.\n"); + return; + } + + verbose = cp_getvar("qpnoise_verbose", CP_BOOL, NULL, 0); + err = QPnoiseAnalyze(ckt, outNode, f_in, verbose ? 1 : 0); + if (err != OK) + fprintf(cp_err, "qpnoise: quasi-periodic noise did not complete (error %d).\n", err); +} diff --git a/src/frontend/com_qpnoise.h b/src/frontend/com_qpnoise.h new file mode 100644 index 000000000..7152f3c1e --- /dev/null +++ b/src/frontend/com_qpnoise.h @@ -0,0 +1,7 @@ +#ifndef ngspice_COM_QPNOISE_H +#define ngspice_COM_QPNOISE_H + +/* Enhancement-138: two-tone small-signal QPnoise (quasi-periodic noise). */ +void com_qpnoise(wordlist *wl); + +#endif diff --git a/src/frontend/commands.c b/src/frontend/commands.c index 5c61229b7..5f279c42f 100644 --- a/src/frontend/commands.c +++ b/src/frontend/commands.c @@ -440,6 +440,10 @@ struct comm spcp_coms[] = { { 040, 040, 040, 040 }, E_DEFHMASK, 1, LOTS, NULL, "f_in : two-tone small-signal QPAC -- response at sidebands f_in+k1f1+k2f2 around the `qpss ... hb` operating point." }, + { "qpnoise", com_qpnoise, TRUE, FALSE, /* Enhancement-138 */ + { 040, 040, 040, 040 }, E_DEFHMASK, 2, LOTS, + NULL, + "output_node f_in : two-tone QPnoise -- output/input noise density at f_in, folding device noise over all sidebands around the `qpss ... hb` operating point." }, { "hb", com_hb, TRUE, FALSE, /* Enhancement-134 */ { 040, 040, 040, 040 }, E_DEFHMASK, 2, LOTS, NULL, diff --git a/src/include/ngspice/cktdefs.h b/src/include/ngspice/cktdefs.h index f6c695ab8..634e39ad6 100644 --- a/src/include/ngspice/cktdefs.h +++ b/src/include/ngspice/cktdefs.h @@ -478,6 +478,7 @@ struct hbspectrum { extern int HBanalyze(CKTcircuit *, double f0, int K, int P, int maxiter, double tol, int verbose, struct hbspectrum *out); /* E-134; E-209 out */ extern int QPSShb(CKTcircuit *, double f1, double f2, int K1, int K2, int P1, int P2, int maxiter, double tol, int verbose); /* E-136 */ extern int QPACanalyze(CKTcircuit *, double f_in, int verbose); /* E-137 */ +extern int QPnoiseAnalyze(CKTcircuit *, int outNode, double f_in, int verbose); /* E-138 */ #endif diff --git a/src/spicelib/analysis/dcpss.c b/src/spicelib/analysis/dcpss.c index eedfff318..8d92b32f3 100644 --- a/src/spicelib/analysis/dcpss.c +++ b/src/spicelib/analysis/dcpss.c @@ -2210,6 +2210,155 @@ QPACanalyze(CKTcircuit *ckt, double f_in, int verbose) } +/* ====================================================================== + * Enhancement-138: two-tone small-signal QPnoise (quasi-periodic noise). + * Around the QPSS operating point retained by `qpss ... hb`, fold every + * device's noise through the ADJOINT of the 2-D conversion matrix over + * all sidebands to the output at f_in -- the two-tone analogue of pnoise + * (E-124). Each device's noise routine computes S*|dTransimp|^2 reading + * the transimpedance from CKTrhs/CKTirhs, so loading the sideband-(k1,k2) + * adjoint transfer into CKTrhs and summing over all harmonics folds the + * noise exactly (mixer/PA folded noise). With no pump the conversion + * matrix is block-diagonal, so only sideband (0,0) contributes and the + * result reduces to ordinary .noise. + * ====================================================================== */ + +/* Solve the ADJOINT 2-D conversion system H^T Psi = e_{out,(0,0)}. Psi then + * holds, for every (node j, harmonic hi), the transfer from a unit injection at + * (j,hi) to the output at sideband (0,0). Reuses qp_build_matrix (forward H) and + * transposes in place. Returns 0 on success, 1 if singular. */ +static int +qp_solve_adjoint(struct qp_harm *hd, double f_in, int outNode, double *Psr, double *Psi) +{ + int Ntot = hd->Ntot, N = hd->N, i, j, i00, rc; + double *Ar, *Ai; + + Ar = TMALLOC(double, (size_t)Ntot * (size_t)Ntot); + Ai = TMALLOC(double, (size_t)Ntot * (size_t)Ntot); + qp_build_matrix(hd, f_in, Ar, Ai); + for (i = 0; i < Ntot; i++) /* transpose in place: H -> H^T */ + for (j = i + 1; j < Ntot; j++) { + size_t ij = (size_t)i*(size_t)Ntot + (size_t)j; + size_t ji = (size_t)j*(size_t)Ntot + (size_t)i; + double t; + t = Ar[ij]; Ar[ij] = Ar[ji]; Ar[ji] = t; + t = Ai[ij]; Ai[ij] = Ai[ji]; Ai[ji] = t; + } + memset(Psr, 0, (size_t)Ntot * sizeof(double)); + memset(Psi, 0, (size_t)Ntot * sizeof(double)); + i00 = hd->K1 * (2*hd->K2 + 1) + hd->K2; + Psr[(size_t)i00 * (size_t)N + (size_t)(outNode - 1)] = 1.0; + rc = pss_csolve(Ntot, Ar, Ai, Psr, Psi); + FREE(Ar); FREE(Ai); + return rc; +} + +int +QPnoiseAnalyze(CKTcircuit *ckt, int outNode, double f_in, int verbose) +{ + struct qp_harm *hd = qpss_hb_saved; + int N, Nh, Ntot, i, j, hi, i00; + double *Psr, *Psi, *Xr, *Xi; + double onoise = 0.0, inoise, gain2 = 1.0; + NOISEAN nj; + Ndata data; + JOB *oldJob; + + if (!hd) { + fprintf(stderr, "qpnoise: no QPSS operating point -- run `qpss hb` first.\n"); + return E_NOTFOUND; + } + N = hd->N; Nh = hd->Nh; Ntot = hd->Ntot; + if (outNode <= 0 || outNode > N) { + fprintf(stderr, "qpnoise: bad output node.\n"); + return E_PARMVAL; + } + i00 = hd->K1 * (2*hd->K2 + 1) + hd->K2; + + /* bias the devices at the QPSS operating point (phase (0,0) sample: v = sum of + * all harmonic coefficients) so each noise PSD is at the periodic bias. */ + for (j = 1; j <= N; j++) { + double v = 0.0; + for (hi = 0; hi < Nh; hi++) v += hd->Vr[(size_t)hi*(size_t)N + (size_t)(j-1)]; + ckt->CKTrhsOld[j] = v; + } + ckt->CKTrhsOld[0] = 0.0; + ckt->CKTmode = (ckt->CKTmode & MODEUIC) | MODEDCOP | MODEINITSMSIG; + CKTload(ckt); + + /* minimal NOISEAN context (device noise routines cast CKTcurJob to NOISEAN*) */ + memset(&nj, 0, sizeof(nj)); + nj.NstartFreq = f_in; + nj.NstopFreq = f_in; + nj.NnumSteps = 1; + nj.NstpType = 0; + nj.NStpsSm = 0; + nj.JOBname = "qpnoise"; + memset(&data, 0, sizeof(data)); + data.prtSummary = FALSE; + oldJob = ckt->CKTcurJob; + ckt->CKTcurJob = (JOB *) &nj; + for (i = 0; i < DEVmaxnum; i++) + if (DEVices[i] && DEVices[i]->DEVnoise && ckt->CKThead[i]) { + double dummy = 0.0; + DEVices[i]->DEVnoise(N_DENS, N_OPEN, ckt->CKThead[i], ckt, &data, &dummy); + } + + Psr = TMALLOC(double, Ntot); Psi = TMALLOC(double, Ntot); + Xr = TMALLOC(double, Ntot); Xi = TMALLOC(double, Ntot); + + /* adjoint transfer from every (node, harmonic) to the output at (0,0), then + * fold each device's noise density over all sidebands. */ + data.freq = f_in; data.delFreq = 0.0; data.prtSummary = FALSE; + if (qp_solve_adjoint(hd, f_in, outNode, Psr, Psi) == 0) { + for (hi = 0; hi < Nh; hi++) { + double dens = 0.0; + size_t blk = (size_t)hi * (size_t)N; + for (j = 1; j <= N; j++) { + ckt->CKTrhs[j] = Psr[blk + (size_t)(j-1)]; + ckt->CKTirhs[j] = Psi[blk + (size_t)(j-1)]; + } + ckt->CKTrhs[0] = 0.0; ckt->CKTirhs[0] = 0.0; + for (i = 0; i < DEVmaxnum; i++) + if (DEVices[i] && DEVices[i]->DEVnoise && ckt->CKThead[i]) + DEVices[i]->DEVnoise(N_DENS, N_CALC, ckt->CKThead[i], ckt, &data, &dens); + onoise += dens; /* sum device noise over sidebands */ + } + } + + /* input-referred: divide by the source->output conversion gain at (0,0) */ + if (hd->has_src) { + double *Ar = TMALLOC(double, (size_t)Ntot*(size_t)Ntot); + double *Ai = TMALLOC(double, (size_t)Ntot*(size_t)Ntot); + qp_build_matrix(hd, f_in, Ar, Ai); + memset(Xr, 0, (size_t)Ntot*sizeof(double)); + memset(Xi, 0, (size_t)Ntot*sizeof(double)); + for (i = 0; i < N; i++) { + Xr[(size_t)i00*(size_t)N + (size_t)i] = hd->B0r[i]; + Xi[(size_t)i00*(size_t)N + (size_t)i] = hd->B0i[i]; + } + if (pss_csolve(Ntot, Ar, Ai, Xr, Xi) == 0) { + size_t oidx = (size_t)i00*(size_t)N + (size_t)(outNode-1); + gain2 = Xr[oidx]*Xr[oidx] + Xi[oidx]*Xi[oidx]; + } + FREE(Ar); FREE(Ai); + } + inoise = onoise / MAX(gain2, N_MINGAIN); + + ckt->CKTcurJob = oldJob; + (void) verbose; + fprintf(stdout, + "\nQPnoise: two-tone output noise at f_in = %g Hz (folding %d sidebands, " + "f1 = %g, f2 = %g)\n" + " onoise density = %.6e V^2/Hz (%.6e V/sqrt(Hz))\n" + " inoise density = %.6e (gain^2 = %.6e)\n", + f_in, Nh, hd->f1, hd->f2, onoise, sqrt(onoise), inoise, gain2); + + FREE(Psr); FREE(Psi); FREE(Xr); FREE(Xi); + return OK; +} + + int DCpss(CKTcircuit *ckt, int restart) /* forced restart flag */ diff --git a/visualc/vngspice.vcxproj b/visualc/vngspice.vcxproj index 4a49b3b71..36d4afae3 100644 --- a/visualc/vngspice.vcxproj +++ b/visualc/vngspice.vcxproj @@ -896,6 +896,7 @@ + @@ -1515,6 +1516,7 @@ +