00001 // Build a two-stage low noise amplifier, and optimize the component values. 00002 // Shows how to use the optimizer and its associated classes 00003 00004 // Look at lna.cc first to get details on building and simulating the amplifier 00005 // circuit. 00006 00007 #include "supermix.h" 00008 00009 int main() 00010 { 00011 // Set the global temperature and normalization impedance. 00012 device::T = 4.2 * Kelvin; 00013 device::Z0 = 50. * Ohm; 00014 00015 // ========================================================================== 00016 // Building the circuit: 00017 00018 // Declare the components to be used to make the amplifier. Same as in lna.cc 00019 fhx13x t1, t2; // Field-effect transistors made by Fujitsu. 00020 t1.Ls.L = 0.05 * nHenry; // adjusting the source inductance values a bit... 00021 t2.Ls.L = 0.05 * nHenry; 00022 00023 inductor lg1, lg2; // Gate tuning inductors. 00024 resistor rd1, rd2; // Drain bias resistors. 00025 lg1.series(); // series is the default, but who can remember? 00026 lg1.series(); 00027 rd1.parallel(); 00028 rd2.parallel(); 00029 // The inductance and resistance values will be found by the optimizer 00030 00031 // Connect the components into a circuit. 00032 circuit amp; 00033 int input = amp.add_port(lg1, 1); 00034 amp.connect(lg1, 2, t1, 1); 00035 amp.connect(t1, 2, rd1, 1); 00036 amp.connect(rd1, 2, lg2, 1); 00037 amp.connect(lg2, 2, t2, 1); 00038 amp.connect(t2, 2, rd2, 1); 00039 int output = amp.add_port(rd2, 2); 00040 00041 // ========================================================================== 00042 // Setting up the error function for the optimization: 00043 00044 // error functions perform two tasks: 00045 // 00046 // (1) provide the interface between an optimization algorithm and the 00047 // actual variables to be optimized in your program 00048 // 00049 // (2) calculate an error function value for the optimization algorithm 00050 // by summing the values of one or more error terms 00051 // 00052 // The interface to the optimizer algorithms which all error functions 00053 // must provide is defined in optimizer.h, using class abstract_error_func. 00054 00055 // We'll be using class error_func, defined in error_func.h. It is derived 00056 // from class error_func_parameters, defined in simple_error_func.h. The 00057 // program variables it optimizes must be of class real_parameter, defined 00058 // in parameter/real_parameter.h. Essentially all of the real-valued variable 00059 // parameters in the various devices are of this type, so they can be 00060 // controlled by class error_func. The vary() member function gives control 00061 // of the parameters to the error function. vary() needs minimum, initial, 00062 // and maximum values for the parameter, along with units: 00063 00064 // we'll be doing a 4-variable optimization: 00065 error_func ef; 00066 lg1.L = ef.vary( 1.0, 3.0, 20.0, nHenry ); 00067 lg2.L = ef.vary( 0.2, 1.0, 10.0, nHenry ); 00068 rd1.R = ef.vary( 30.0, 100.0, 500.0, Ohm ); 00069 rd2.R = ef.vary( 30.0, 100.0, 150.0, Ohm ); 00070 00071 00072 // Sweepers are used to define ranges (in this case, frequency bands) for 00073 // sweeping some parameter under the control of an error function object. 00074 // The error function is calculated from the sum of the error function 00075 // values at each of the sweeper points. See sweeper.h for details. 00076 00077 // In this case, both of the sweepers will control the global frequency 00078 // variable, device::f, which determines the frequency for circuit response 00079 // calculations. The stability_band sweeper sweeps over a wider frequency 00080 // range. The last argument in the call to sweep() gives the step size. 00081 // Note the need for units! 00082 00083 sweeper gain_band, stability_band; 00084 00085 gain_band.sweep( device::f, 4.0*GHz, 8.0*GHz, 0.25*GHz ); 00086 stability_band.sweep( device::f, 0.1*GHz, 20.1*GHz, 0.50*GHz ); 00087 00088 00089 // Error terms are individual components in an error function. Their design 00090 // allows for the accumulation of an error value through the range of a 00091 // parameter sweep by a sweeper object. Several generic error term forms 00092 // are defined in error_func.h, all derived from class error_term. Error 00093 // terms based on these generic objects are easy to write from scratch, but 00094 // several concrete and useful error terms are already defined in 00095 // error_terms.h. We'll be using these objects, which take a circuit, and 00096 // maybe port numbers and a target mode and value. The target mode and 00097 // target value can be provided as additional constructor arguments, but 00098 // here we'll call member functions to do that job. 00099 00100 // The classes gain_dB, input_tn, amp_k, and amp_mag_delta are all defined 00101 // in error_terms.h; they are derived from error_term_mode defined in 00102 // error_func.h, which also describes the member functions we are calling 00103 // to set the mode and target for each error term. Error terms derived from 00104 // error_term_mode generally return a value proportional to the square of the 00105 // deviation from a target value. error_func averages the error term values 00106 // across the points of an associated sweeper (if any). 00107 00108 // Optimize for stability. 00109 amp_k stb1_term(amp); 00110 amp_mag_delta stb2_term(amp); 00111 00112 // Keep the gain high. 00113 gain_dB gain_term(amp, input, output); 00114 gain_term.above(20.0); // an error value results if gain is not above 20 dB 00115 00116 // Optimize for flat gain. 00117 gain_dB flatness_term(amp, input, output); 00118 flatness_term.flat(); // proportional to overall deviation from flatness 00119 00120 // Optimize the output match. 00121 gain_dB out_match_term(amp, output, output); 00122 out_match_term.below(-15.0); // want to be below the target of -15 dB 00123 00124 // Optimize the input match. 00125 gain_dB in_match_term(amp, input, input); 00126 in_match_term.below(-10.0); // want to be below the target of -10 dB 00127 00128 // Optimize for low noise. 00129 input_tn noise_term(amp, input, output); 00130 noise_term.match(0.0); // want the noise to match 0 Kelvin 00131 00132 00133 // Here's where we add the various error terms to the error function in 00134 // order to fully define it. add_term() takes a relative weighting factor 00135 // as its first argument, along with the error term and an optional sweeper. 00136 // add_term() is defined in error_func.h 00137 00138 ef.add_term( 10.0, stb1_term, stability_band ); 00139 ef.add_term( 10.0, stb2_term, stability_band ); 00140 ef.add_term( 1.5, gain_term, gain_band ); 00141 ef.add_term( 20.0, flatness_term, gain_band ); 00142 ef.add_term( 1.0, out_match_term, gain_band ); 00143 ef.add_term( 1.0, in_match_term, gain_band ); 00144 ef.add_term( 10.0, noise_term, gain_band ); 00145 00146 // ========================================================================== 00147 // Setting up the optimization algorithm: 00148 00149 // Optimization algorithms are derived from class minimizer, found in 00150 // optimizer.h; we'll be using Powell's algorithm, defined in powell.h 00151 // Minimizers must be given an error function at construction. They also 00152 // have member functions which control verbosity. Class powell has some 00153 // variables which can be adjusted to tweak the performance of the 00154 // algorithm; we adjust one of them from its default value. See powell.h 00155 // for details. 00156 00157 powell opt(ef); 00158 opt.verbose(); // see the iterations as they occur 00159 opt.FTOL = 0.00001; 00160 00161 // ========================================================================== 00162 // Perform the optimization, displaying the resulting optimized values: 00163 00164 cout << endl << "2 stage cryogenic low-noise amplifier" << endl << endl; 00165 cout << "Starting optimization:" << endl << endl; 00166 00167 // Calling the minimize() member function of the optimizer object causes it 00168 // to "do its thing", finding parameter values which minimize the error 00169 // function. minimize() returns the final error function value (a double). 00170 // 00171 // In the next lines we call minimize() and output the returned error 00172 // function value. Because we called opt.verbose() earlier, we'll get several 00173 // lines of output generated internally by the optimizer as it searches for a 00174 // solution. 00175 00176 double error_val = opt.minimize(); 00177 cout << endl << "Final error function value: " << error_val << endl ; 00178 00179 // We can get a breakdown of the contribution of each error term to the 00180 // final error function value by calling error_func's member function 00181 // get_func_breakdown(). It returns a real_vector object (see vector.h 00182 // -- the one that comes with supermix, NOT the one that comes with the 00183 // standard C++ library, known as the STL). The stream operator << can be 00184 // used to output the contents of a real_vector object, by default all on 00185 // one line. The contributions of the terms are displayed in the same 00186 // order as the order of the calls to ef.add_term() made previously, in this 00187 // case: stb1_term, stb2_term, gain_term, flatness_term, out_match_term, 00188 // in_match_term, noise_term. 00189 00190 cout << "Error function breakdown by term:" << endl; 00191 cout << ef.get_func_breakdown() << endl; 00192 00193 // Now that the optimization is complete, we can conveniently ask the error 00194 // function for the final values of the parameter assignments. By calling the 00195 // member function get_parms_user(), we get a real_vector object. The show() 00196 // member function of the real_vector class can also be used to display the 00197 // results, by default all on one line preceded and followed by a newline. 00198 // The parameters are displayed in the same order as the order of the calls 00199 // to ef.vary() made previously. In this case the order is lg1.L, lg2.L, 00200 // rd1.R, and rd2.R. They are displayed in terms of the units provided in 00201 // the vary() calls; in our case the inductors are in nHenry, the resistors 00202 // in Ohms. 00203 00204 cout << "Final parameters are:" << endl; 00205 cout << "lg1(nH)" << " " << "lg2(nH)" << " " << "rd1(O) " << " " << "rd2(O)"; 00206 00207 ef.get_parms_user().show() ; 00208 00209 00210 // ========================================================================== 00211 // Now display the response of our amplifier using the optimized values: 00212 00213 // Here you also see what you need to do to set a specific output field width 00214 cout << endl << "Response:" << endl << endl; 00215 cout << setw(8) << "Freq" << " " 00216 << setw(8) << "S21(dB)" << " " 00217 << setw(8) << "S22(dB)" << " " 00218 << setw(8) << "Tn(K)" << " " 00219 << setw(8) << "NF(dB)" << endl << endl; 00220 00221 // The scattering and noise matrices will be held in a special form 00222 // of sdata object, called ampdata. It is defined in ampdata.h and has 00223 // some additional member functions appropriate for displaying 00224 // amplifier characteristics. 00225 ampdata response; 00226 00227 for(double freq = 1.0; freq <=12.0; freq += 0.5) 00228 { 00229 // Set the frequency. 00230 device::f = freq * GHz; 00231 00232 // Calcuate the scattering and noise matrices, and hold them in response. 00233 response = amp.get_data(); 00234 00235 // Print out the frequency, gain, output match, noise temp, noise figure. 00236 cout << fixed << setprecision(4) 00237 << setw(8) << freq << " " 00238 << setw(8) << response.SdB(output,input) << " " 00239 << setw(8) << response.SdB(output,output) << " " 00240 << setw(8) << response.tn(output,input)/Kelvin << " " 00241 << setw(8) << response.NF(output,input) << " "; 00242 00243 // Also let user know if amp is unconditionally stable at this frequency 00244 if(response.unconditionally_stable()) cout << "Unconditionally Stable"; 00245 00246 cout << endl; 00247 } 00248 00249 cout << endl; 00250 }
Please direct comments and corrections to
supermix@submm.caltech.edu
Go to the supermix home page
Generated by
1.2.7