public static void Run(int pllproc) { double[] @params = new double[6]; double maxgrad = 0; double stpthrsh = 0; double maxeigen; double mineigen; double wr = 0; double[] alpha = { 0.50, 1.00 }; double rfMax = 2.75; int td = 0; int[] prtls = { -1, -1, -1, -1 }; int iterint = 1; long sn = (long)Math.Pow(10.0, 7); int prec = (int)Math.Pow(10.0, 4); string type = ""; string alg = ""; const string rootdir = Config.ConfigsRootDir; // Read setup details from control file. try { var getParams = File.ReadAllText(Path.Combine(rootdir, Config.ControlFile)); var s = getParams.Split(new[] { ' ', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); @params = s.Take(6).Select(i => double.Parse(i, CultureInfo.InvariantCulture)).ToArray(); td = int.Parse(s[6]); wr = double.Parse(s[7]); stpthrsh = double.Parse(s[8]); alg = s[9]; type = s[10]; if (type.Equals("sim")) { sn = int.Parse(s[11]); alpha[0] = double.Parse(s[12]); alpha[1] = double.Parse(s[13]); } else if (type.Equals("dp")) { prec = int.Parse(s[11]); rfMax = double.Parse(s[12]); } } catch (Exception ex) { Trace.Write("ERROR: Could not read file: "); Trace.WriteLine(Path.Combine(rootdir, Config.ControlFile) + $". {ex.Message}"); Trace.WriteLine("EXITING...main()..."); Console.Read(); Environment.Exit(1); } // Read initial glide-path from file. var gp = new double[td]; var gpPath = Path.Combine(rootdir, Config.InitGladepathFile); try { gp = File.ReadAllLines(gpPath).Select(double.Parse) .Take(td).ToArray(); } catch (Exception ex) { Trace.Write("ERROR: Could not read file: "); Trace.WriteLine(gpPath + ". Error: " + ex.Message); Trace.WriteLine("EXITING...main()..."); Console.Read(); Environment.Exit(1); } if (gp.Length != td) { Trace.Write("ERROR: File: "); Trace.Write(gpPath); Trace.Write(" needs "); Trace.Write(td); Trace.WriteLine(" initial asset allocations, but has fewer."); Trace.WriteLine("EXITING...main()..."); Console.Read(); Environment.Exit(1); } // Display optimization algorithm. Trace.Write(@"===> Optimization algorithm: "); if (alg == "nr") { Trace.WriteLine(@"Newton's Method"); } else if (alg == "ga") { Trace.WriteLine(@"Gradient Ascent"); } // Display estimation method. Trace.WriteLine(""); Trace.Write(@"===> Estimation method: "); if (type == "sim") { Trace.WriteLine(@"Simulation"); } else if (type == "dp") { Trace.WriteLine(@"Dynamic Program"); pllproc = 4 * pllproc; } // Declare variables that depend on data read from the control file for sizing. double[,] hess; double[] ngrdnt = new double[td]; EigenvalueDecomposition hevals; var grad = new double[td]; // Take some steps (1 full iteration but no more than 50 steps) in the direction of steepest ascent. This can move us off // the boundary region where computations may be unstable (infinite), especially when constructing the Hessian for Newton's method. // Also, this initial stepping usually makes improvements very quickly before proceeding with the optimization routine. double probnr = GetPNR.Run(type, @params, gp, td, wr, 4 * sn, (int)(rfMax * prec), prec, prtls, pllproc); Trace.WriteLine(""); Trace.WriteLine("Initial Glide-Path (w/Success Probability):"); WrtAry.Run(probnr, gp, "GP", td); for (int s = 1; s <= 2; ++s) { maxgrad = BldGrad.Run(type, @params, gp, td, wr, sn, (int)(rfMax * prec), prec, probnr, 4 * sn, alpha[1], pllproc, grad); if (maxgrad <= stpthrsh) { Trace.Write("The glide-path supplied satisfies the EPSILON convergence criteria: "); Trace.WriteLine($"{maxgrad:F15} vs. {stpthrsh:F15}"); s = s + 1; } else if (s != 2) { probnr = Climb.Run(type, @params, gp, td, wr, 2 * sn, (int)(rfMax * prec), prec, pllproc, maxgrad, probnr, 4 * sn, grad, alpha[0], 50); Trace.WriteLine(""); Trace.WriteLine("New (Post Initial Climb) Glide-Path (w/Success Probability):"); WrtAry.Run(probnr, gp, "GP", td); } else if (maxgrad <= stpthrsh) { Trace.Write("The glide-path supplied satisfies the EPSILON convergence criteria after intial climb without iterating: "); Trace.WriteLine($"{maxgrad:F15} vs. {stpthrsh:F15}"); } } // Negate the gradient if using NR method. if (alg == "nr") { for (int y = 0; y < td; ++y) { ngrdnt[y] = -1.00 * grad[y]; } } // If convergence is not achieved after initial climb then launch into full iteration mode. while (maxgrad > stpthrsh) { Trace.WriteLine(""); Trace.WriteLine("========================="); Trace.WriteLine($"Start Iteration #{iterint}"); Trace.WriteLine("========================="); if (alg == "nr") { // Record the probability before iterating. double strtpnr = probnr; // Build the Hessian matrix for this glide-path and derive its eigenvalues. (Display the largest & smallest value.) // This is required when method=nr. When either procedure ends with convergence we recompute the Hessian matrix to // ensure we are at a local/global maximum (done below after convergence). hess = DrvHess.Run(type, @params, gp, td, wr, sn, (int)(rfMax * prec), prec, pllproc, grad, probnr); //hevals.compute(hess, false); hevals = new EigenvalueDecomposition(hess); var reals = hevals.RealEigenvalues; maxeigen = reals.Max(); mineigen = reals.Min(); // Display the smallest/largest eigenvalues. Trace.WriteLine(""); Trace.Write("Min Hessian eigenvalue for this iteration (>=0.00 --> convex region): "); Trace.WriteLine(mineigen); Trace.WriteLine(""); Trace.Write("Max Hessian eigenvalue for this iteration (<=0.00 --> concave region): "); Trace.WriteLine(maxeigen); // Update the glidepath and recompute the probability using the new glidepath. //sol = hess.colPivHouseholderQr().solve(ngrdnt); var qr = new QrDecomposition(hess); var sol = qr.Solve(ngrdnt); for (int y = 0; y < td; ++y) { gp[y] += sol[y]; } probnr = GetPNR.Run(type, @params, gp, td, wr, 4 * sn, (int)(rfMax * prec), prec, prtls, pllproc); // If success probability has worsened alert the user. if (probnr < strtpnr) { Trace.WriteLine(""); Trace.WriteLine("NOTE: The success probability has worsened during the last iteration. This could happen for different reasons:"); Trace.WriteLine(" 1.) The difference in probabilities is beyond the system's ability to measure accurately (i.e., beyond 15 significant digits)."); Trace.WriteLine(" 2.) The difference is due to estimation/approximation error."); Trace.WriteLine(" 3.) You may be operating along the boundary region. In general the procedure is not well defined on the boundaries. (Try gradient ascent.)"); } } else if (alg == "ga") { // Update the glide-path and recompute the probability using the new glide-path. probnr = Climb.Run(type, @params, gp, td, wr, 2 * sn, (int)(rfMax * prec), prec, pllproc, maxgrad, probnr, 4 * sn, grad, alpha[0]); } // Display the new glide-path. Trace.WriteLine(""); Trace.Write("New Glide-Path:"); WrtAry.Run(probnr, gp, "GP", td); // Rebuild the gradient and negate it when using NR. maxgrad = BldGrad.Run(type, @params, gp, td, wr, 1 * sn, (int)(rfMax * prec), prec, probnr, 4 * sn, alpha[1], pllproc, grad); if (alg == "nr") { for (int y = 0; y < td; ++y) { ngrdnt[y] = -1.00 * grad[y]; } } // Report the convergence status. Trace.WriteLine(""); Trace.WriteLine($"EPSILON Convergence Criteria: {maxgrad:F15} vs. {stpthrsh:F15}"); if (maxgrad <= stpthrsh) { Trace.WriteLine(""); Trace.WriteLine("==========> EPSILON Convergence criteria satisfied. <=========="); } Trace.WriteLine(""); Trace.WriteLine(new String('=', 25)); Trace.Write("End Iteration #"); Trace.WriteLine(iterint); Trace.WriteLine(new String('=', 25)); iterint++; } // Build Hessian and confirm we are at a maximum, not a saddle-point or plateau for example. Trace.WriteLine(""); Trace.WriteLine("Convergence Achieved: Final step is to confirm we are at a local/global maximum. Hessian is being built."); hess = DrvHess.Run(type, @params, gp, td, wr, sn, (int)(rfMax * prec), prec, pllproc, grad, probnr); hevals = new EigenvalueDecomposition(hess); var r = hevals.RealEigenvalues; maxeigen = r.Max(); mineigen = r.Min(); // Display the smallest/largest eigenvalues. Trace.WriteLine(""); Trace.Write("Min Hessian eigenvalue at solution [>=0.00 --> convex region --> (local/global) minimum]: "); Trace.WriteLine(mineigen); Trace.WriteLine(""); Trace.Write("Max Hessian eigenvalue at solution [<=0.00 --> concave region --> (local/global) maximum]: "); Trace.WriteLine(maxeigen); // Write final GP to the output file. Trace.WriteLine(""); if (maxeigen <= 0 || mineigen >= 0) { Trace.Write("(Local/Global) Optimal "); } Trace.WriteLine("Glide-Path:"); WrtAry.Run(probnr, gp, "GP", td, Path.Combine(rootdir, Config.Outfile)); Trace.WriteLine(""); }
public static double Run(string type, double[] prms, double[] gpath, int fxTD, double rf0, long n, int nbuckets, int prec, int plproc, double mxgrd, double stdpnr, long npnr, double[] grdnt, double alpha, int nitrs = 0) { double maxpnr = stdpnr; double iter = 1.00; int[] prtls = { -1, -1, -1, -1 }; long maxn = npnr; int cont = 1; int fstimpr = 0; int iindx = 0; int tryup = 0; int origindx = 0; NormalDistribution normdist = new NormalDistribution(0.00, 1.00); // Get initial glidepath provided, assign to both original GP array and prior GP array. var prevGP = new double[fxTD]; var origGP = new double[fxTD]; for (int y = 0; y < fxTD; ++y) { origGP[y] = prevGP[y] = gpath[y]; } // Define the step size for climbing. The step size depends on the largest gradient element and grows exponentially. // This is a heuristic that has worked well and can be modified if desired. Better step sizes can reduce runtimes. for (int i = 1; i <= 10; ++i) { if (mxgrd >= 1.00 / (10.00 * Math.Pow(10.00, i)) && mxgrd < 1.00 / (10.00 * Math.Pow(10.00, i - 1))) { iindx = i; iter = Math.Pow(Math.Exp(Math.Log(5.00) / 4.00), iindx); } } // Output details for the current iteration. Trace.WriteLine(new String('=', 70)); Trace.WriteLine($"Iteration step size = {iter:F10}"); Trace.Write("Trying to improve on success probability = "); Trace.WriteLine(stdpnr); Trace.WriteLine(new String('=', 70)); // Climb in the direction of the gradient. for (int t = 0; cont == 1 || fstimpr == 0; ++t) // Iterate until no more progress is made { if (cont == 0 && fstimpr == 0) // If no progress is made reduce step size and try again. { // Set the original index value when entering this problematic scenario. if (origindx == 0) { origindx = iindx; } // Adjust glidepath back one iteration since it failed to improve the probability. for (int y = 0; y < fxTD; ++y) { gpath[y] = prevGP[y]; // Reverse final update, since no progress was made. } if (iindx != 0) { } Trace.WriteLine(""); Trace.Write("No Progress Made: Iteration step size changed from "); Trace.Write(iter); if (iindx == 0) { Trace.WriteLine(" to 0.00. (No additional climbing attempts will be made.)"); Trace.WriteLine(""); Trace.WriteLine("ERROR: No progress can be made, the procedure is stuck. (Step size has been reduced to 0.)"); Trace.WriteLine(" You may be operating along the boundary where the process is not well defined or your"); Trace.WriteLine(" estimation/approximation precision level is not adequate for your epsilon level."); Trace.WriteLine(""); Trace.WriteLine(""); Trace.Write("Current Glide-Path: "); WrtAry.Run(maxpnr, gpath, "GP", fxTD); Trace.WriteLine(""); Trace.WriteLine("EXITING...Climb()..."); Console.Read(); Environment.Exit(1); } else if (iindx == 1 && tryup == 5) { iindx = iindx - 1; iter = iter / 2.00; } else if (iindx > 1 && tryup == 5) { if (iindx == origindx + 5) { iindx = origindx - 1; } else { iindx = iindx - 1; } iter = Math.Pow(Math.Exp(Math.Log(5.00) / 4.00), iindx); } else if (iindx > 1 && tryup < 5) { iindx = iindx + 1; iter = Math.Pow(Math.Exp(Math.Log(5.00) / 4.00), iindx); tryup = tryup + 1; } Trace.Write(" to "); Trace.Write(iter); Trace.WriteLine(". (Attempting to climb again.)"); cont = 1; } for (int y = 0; y < fxTD; ++y) // Iterate over glide-path and update it { prevGP[y] = gpath[y]; // Reset the previous glide-path element gpath[y] = gpath[y] + (iter) * grdnt[y]; // Update each individual glide-path element if (gpath[y] < Funcs.mva(prms) + 0.0001) { gpath[y] = Funcs.mva(prms) + 0.0001; // Stay above MVA and consistent with ThrdPNRdyn() and ThrdPNRsim(). } else if (gpath[y] > 1.00) { gpath[y] = 1.00; // Consistent with ThrdPNRdyn() and ThrdPNRsim(). } } double newpnr = GetPNR.Run(type, prms, gpath, fxTD, rf0, n, nbuckets, prec, prtls, plproc); Trace.WriteLine(""); Trace.Write("Base Prob(NR) = "); Trace.Write(maxpnr); if (type == "sim") { Trace.Write(" (N="); Trace.Write(maxn); Trace.Write(")"); } Trace.WriteLine(""); Trace.Write("New Prob(NR) = "); Trace.Write(newpnr); if (type == "sim") { Trace.Write($" (N={n})"); } else if (newpnr > maxpnr) { Trace.Write(" (Better, CONTINUE climbing ...)"); } else { Trace.Write(" (Worse, STOP climbing ...)"); } Trace.WriteLine(""); // If using simulation, conduct a non-inferiority test of the new vs max base GP. // =====> Continue to climb if the new GP is at least as good as the max base GP. // Otherwise, compare new probability with old and climb while making progress. if (type == "sim") { double cmbvar = maxpnr * (1.00 - maxpnr) / maxn + newpnr * (1.00 - newpnr) / n; double ts = (newpnr - maxpnr) / Math.Sqrt(cmbvar); double pval = normdist.DistributionFunction(ts); Trace.Write("Test Statistic = "); Trace.WriteLine(ts); Trace.Write("P-Value = "); Trace.Write(pval); Trace.Write(" (Alpha="); Trace.Write(alpha); Trace.WriteLine(")"); if (pval > alpha) { fstimpr = 1; Trace.WriteLine("=====> Accept Ho (non-inferiority), CONTINUE climbing ..."); } else { cont = 0; Trace.WriteLine("=====> Reject Ho (non-inferiority), STOP climbing ..."); } // Update PNR and sample size for base GP. if (newpnr > maxpnr) { maxpnr = newpnr; maxn = n; } } else if (type == "dp") { if (newpnr > maxpnr) { fstimpr = 1; maxpnr = newpnr; } else { cont = 0; } } // For lengthy climbing, display the current glidepath at 100 iteration intervals. if ((t + 1) % 100 == 0) { Trace.WriteLine(""); Trace.Write("Current Glide-Path at Iteration: "); Trace.Write(t + 1); WrtAry.Run(newpnr, gpath, "GP", fxTD); } // Stop when maximum number of iterations has been reached, if specified. //========================================================================= if (nitrs > 0 && t + 1 == nitrs && cont == 1) { Trace.WriteLine(""); Trace.WriteLine($"Climbing limit reached at {nitrs} iterations."); cont = 2; } } // Adjust glidepath back one iteration since it failed to improve the probability. // (This is only done when climbing failed to improve, not when limit is reached.) if (cont != 2) { for (int y = 0; y < fxTD; ++y) { gpath[y] = prevGP[y]; } } if (type == "sim") { Trace.WriteLine(""); Trace.WriteLine("Resetting the probability (to remove any built-in upward sampling bias) ..."); maxpnr = ThrdPNRsim.Run(prms, gpath, fxTD, rf0, 2 * n, prtls, plproc); } // Return the max success probability. return(maxpnr); }
public static double[,] Run(string type, double[] prms, double[] a, int fxTD, double rf0, long n, int nbuckets, int prec, int plproc, double[] gradvctr, double stdpnr) { double[,] hess = new double[fxTD, fxTD]; // Derive the Hessian matrix. Trace.WriteLine(""); Trace.Write("Building Hessian "); for (int i = 0; i < fxTD; ++i) { var blnks = new String(' ', (i > 0 ? 17 : 0) + i); Trace.Write(blnks); for (int j = 0; j < fxTD; ++j) { if (j >= i) { Trace.Write("."); if (i < 0 || j < 0 || i >= fxTD || j >= fxTD) { Trace.Write("ERROR: Both i and j must be integers between 0 and "); Trace.Write(fxTD - 1); Trace.Write(", i="); Trace.Write(i); Trace.Write(" and j="); Trace.WriteLine(j); Trace.WriteLine("EXITING...DrvHess()..."); Console.Read(); Environment.Exit(1); } else if (i != j) // ***** Off-diagonal elements ***** // { // Define needed quantities and return the off-diagonal Hessian element. //======================================================================== double k1 = Funcs.vp(prms, a[i]) / (2.00 * Funcs.v(prms, a[i])) + Math.Pow(Funcs.mp(prms), 2) / (2.00 * Funcs.vp(prms, a[i])); double k2 = Funcs.vp(prms, a[j]) / (2.00 * Funcs.v(prms, a[j])) + Math.Pow(Funcs.mp(prms), 2) / (2.00 * Funcs.vp(prms, a[j])); int[] prtls = { i, j, -1, -1 }; hess[i, j] = k1 * k2 * (GetPNR.Run(type, prms, a, fxTD, rf0, n, nbuckets, prec, prtls, plproc) - gradvctr[i] / k1 - gradvctr[j] / k2 - stdpnr); } else // ***** Diagonal elements ***** // { // Define needed quantities and return the diagonal Hessian element. // [Note #1: K1 is for h1, K2 is for h2, and K3 is for f(). And the diagonal term is K1*h1() + K2*h2() + K3*f().] // [Note #2: The Hessian diagonals will divide by zero for one alpha, check for that value and exit if encountered.] if (Math.Abs(Funcs.v(prms, a[i]) * Funcs.vpp(prms) - 2.00 * Math.Pow(Funcs.vp(prms, a[i]), 2)) < 1e-15) { Trace.Write("ERROR: Hessian diagonal element does not exist for alpha value="); Trace.WriteLine($"{a[i]} encountered at time point t={i}."); Trace.WriteLine(""); Trace.WriteLine("EXITING...DrvHess()..."); Console.Read(); Environment.Exit(1); } else { double k1 = (Funcs.v(prms, a[i]) + Math.Pow(Funcs.kh1(prms, a[i]), 2)) * (Funcs.v(prms, a[i]) * Funcs.vpp(prms) - 2.00 * Math.Pow(Funcs.vp(prms, a[i]), 2)) / (2.00 * Math.Pow(Funcs.v(prms, a[i]), 3)); double k2 = (Math.Pow(Funcs.vp(prms, a[i]), 2) + 2.00 * Funcs.v(prms, a[i]) * Math.Pow(Funcs.mp(prms), 2)) / (2.00 * Math.Pow(Funcs.v(prms, a[i]), 2)); double k3 = -((Funcs.vpp(prms) * Funcs.v(prms, a[i]) - Math.Pow(Funcs.vp(prms, a[i]), 2) + 2.00 * Funcs.v(prms, a[i]) * Math.Pow(Funcs.mp(prms), 2)) / (2.00 * Math.Pow(Funcs.v(prms, a[i]), 2)) + (2.00 * Math.Pow(Funcs.vp(prms, a[i]), 2) * Math.Pow(Funcs.mp(prms), 2)) / (Math.Pow(Funcs.v(prms, a[i]), 2) * Funcs.vpp(prms) - 2.00 * Math.Pow(Funcs.vp(prms, a[i]), 2) * Funcs.v(prms, a[i]))); int[] h1Prtls = { -1, -1, i, -1 }; double h1 = GetPNR.Run(type, prms, a, fxTD, rf0, n, nbuckets, prec, h1Prtls, plproc); int[] h2Prtls = { -1, -1, -1, i }; double h2 = GetPNR.Run(type, prms, a, fxTD, rf0, n, nbuckets, prec, h2Prtls, plproc); hess[i, j] = k1 * h1 + k2 * h2 + k3 * stdpnr; } } } else { hess[i, j] = hess[j, i]; } } if (i == fxTD - 1) { Trace.Write(" (Done)"); } Trace.WriteLine(""); } Trace.WriteLine(""); // Return the Hessian matrix. return(hess); }
public static double Run(string type, double[] prms, double[] a, int fxTD, double rf0, long n, int nbuckets, int prec, double stdpnr, long npnr, double alpha, int plproc, double[] gradvctr) { int[] prtls = { -1, -1, -1, -1 }; double[] k = new double[fxTD]; double maxval = 0; NormalDistribution normdist = new NormalDistribution(0.00, 1.00); // Iterate over each time point deriving each gradient entry. Trace.WriteLine(""); Trace.Write("Building gradient "); for (int g = 0; g < fxTD; ++g) { // Construct the constant needed for gradient entries. k[g] = Funcs.vp(prms, a[g]) / (2.00 * Funcs.v(prms, a[g])) + Math.Pow(Funcs.mp(prms), 2) / (2.00 * Funcs.vp(prms, a[g])); // Populate the gradient vector for this time point. prtls[0] = g; double grdpnr = GetPNR.Run(type, prms, a, fxTD, rf0, n, nbuckets, prec, prtls, plproc); gradvctr[g] = k[g] * (grdpnr - stdpnr); Trace.Write("."); // Maximum effective absolute value of this gradient vector. if (a[g] + gradvctr[g] > 1.00) { if (maxval < 1.00 - a[g]) { maxval = 1.00 - a[g]; } } else if (a[g] + gradvctr[g] < Funcs.mva(prms) + 0.0001) { if (a[g] - (Funcs.mva(prms) + 0.0001) > maxval) { maxval = a[g] - (Funcs.mva(prms) + 0.0001); } } else if (Math.Abs(gradvctr[g]) > maxval) { maxval = Math.Abs(gradvctr[g]); } } Trace.WriteLine(" (Done)"); // Print the unadjusted gradient vector entries. Trace.WriteLine(""); Trace.Write("Gradient (no adjustment):"); WrtAry.Run(-1, gradvctr, "Grd", fxTD); // If using simulation, test each element for equality with zero. If test holds set the element to zero. if (type == "sim" && alpha < 1.00) { maxval = 0; for (int g = 0; g < fxTD; ++g) { double cmbpnr = (n * (stdpnr + gradvctr[g] / k[g]) + npnr * stdpnr) / (n + npnr); double ts = (stdpnr + gradvctr[g] / k[g] - stdpnr) / Math.Sqrt(cmbpnr * (1.00 - cmbpnr) * (1.00 / n + 1.00 / npnr)); double pval = 2.00 * Math.Min(normdist.DistributionFunction(ts), 1.00 - normdist.DistributionFunction(ts)); if (pval > alpha) { gradvctr[g] = 0.00; // The element is not different from zero at significance level alpha. } // Maximum effective absolute value of this gradient vector. if (a[g] + gradvctr[g] > 1.00) { if (maxval < 1.00 - a[g]) { maxval = 1.00 - a[g]; } } else if (a[g] + gradvctr[g] < Funcs.mva(prms) + 0.0001) { if (a[g] - (Funcs.mva(prms) + 0.0001) > maxval) { maxval = a[g] - (Funcs.mva(prms) + 0.0001); } } else if (Math.Abs(gradvctr[g]) > maxval) { maxval = Math.Abs(gradvctr[g]); } } // Print the adjusted gradient vector entries. Trace.WriteLine(""); Trace.Write("ADJUSTED gradient:"); WrtAry.Run(-1, gradvctr, "Adj-Grd", fxTD); } // Display the maximum effective absolute value of the gradient vector (used to determine convergence). Trace.WriteLine(""); Trace.WriteLine($"Maximum effective absolute value of this gradient: {maxval:F10}"); // This function returns the maximum absolute value of the gradient elements. // (Which is used to define the stopping/convergence criteria.) return(maxval); }