Thermal-FIST 1.6.1
Package for hadron resonance gas model applications
Loading...
Searching...
No Matches
ThermalModelCanonical.cpp
Go to the documentation of this file.
1/*
2 * Thermal-FIST package
3 *
4 * Copyright (c) 2015-2019 Volodymyr Vovchenko
5 *
6 * GNU General Public License (GPLv3 or later)
7 */
9
10#include <iostream>
11#include <cmath>
12#include <cstdlib>
13#include <algorithm>
14#include <cassert>
15
17#include "HRGBase/xMath.h"
19
20#include <Eigen/Dense>
21
22using namespace std;
23
24// ============================================================================
25// Contents (search the "==== N." banners below to jump to a section):
26// 1. Construction & configuration
27// 2. Primordial densities & validation
28// 3. Gauss-Legendre quadrature (canonical partition functions)
29// 4. Fluctuations & thermodynamics
30// 5. Helpers & charge predicates
31// 6. GCE model management (shared by the GL contour shift and saddle point)
32// 7. Saddle-point methods (SaddlePoint / SaddlePointNLO / SaddlePointLO)
33// ============================================================================
34
35namespace thermalfist {
36
37 // ==========================================================================
38 // 1. Construction & configuration
39 // ==========================================================================
40
43 m_BCE(1),
44 m_QCE(1),
45 m_SCE(1),
46 m_CCE(1),
49 {
50
51 m_TAG = "ThermalModelCanonical";
52
53 m_Ensemble = CE;
54 m_InteractionModel = Ideal;
55
56 m_modelgce = NULL;
57 m_modelgce_default = new ThermalModelIdeal(m_TPS, m_Parameters);
58 m_modelgce_ext = NULL;
59
60 m_Banalyt = false;
63
67 m_NLOPressure = 0.0;
69 }
70
71
72
77
83
85 {
86 m_BMAX = 0;
87 m_QMAX = 0;
88 m_SMAX = 0;
89 m_CMAX = 0;
90
91
92 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
93 ThermalParticle &part = m_TPS->Particle(i);
94
95 if (part.Statistics() != 0 && IsParticleCanonical(part)) {
97
98 m_BMAX = max(m_BMAX, abs(part.BaryonCharge() * part.ClusterExpansionOrder()));
99 m_QMAX = max(m_QMAX, abs(part.ElectricCharge() * part.ClusterExpansionOrder()));
100 m_SMAX = max(m_SMAX, abs(part.Strangeness() * part.ClusterExpansionOrder()));
101 m_CMAX = max(m_CMAX, abs(part.Charm() * part.ClusterExpansionOrder()));
102 }
103 else {
104 m_BMAX = max(m_BMAX, abs(part.BaryonCharge()));
105 m_QMAX = max(m_QMAX, abs(part.ElectricCharge()));
106 m_SMAX = max(m_SMAX, abs(part.Strangeness()));
107 m_CMAX = max(m_CMAX, abs(part.Charm()));
108 }
109 }
110
115
116 if (computeFluctuations) {
117 m_BMAX *= 2;
118 m_QMAX *= 2;
119 m_SMAX *= 2;
120 m_CMAX *= 2;
121 }
122
123 // Some charges may be treated grand-canonically
124 m_BMAX *= m_BCE;
125 m_QMAX *= m_QCE;
126 m_SMAX *= m_SCE;
127 m_CMAX *= m_CCE;
128
129 m_QNMap.clear();
130 m_QNvec.resize(0);
131
132 m_Corr.resize(0);
133 m_PartialZ.resize(0);
134
135 int ind = 0;
136 for (int iB = -m_BMAX; iB <= m_BMAX; ++iB)
137 for (int iQ = -m_QMAX; iQ <= m_QMAX; ++iQ)
138 for (int iS = -m_SMAX; iS <= m_SMAX; ++iS)
139 for (int iC = -m_CMAX; iC <= m_CMAX; ++iC) {
140
141 QuantumNumbers qn(iB, iQ, iS, iC);
142 m_QNMap[qn] = ind;
143 m_QNvec.push_back(qn);
144
145 m_PartialZ.push_back(0.);
146 m_Corr.push_back(1.);
147
148 ind++;
149 }
150
151 m_PartialZCalculatedFlucts = computeFluctuations;
152 }
153
155 m_QuantumStats = stats;
156 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
157 m_TPS->Particle(i).UseStatistics(stats);
158
159 // Only cluster expansion method supported for particles with canonically conserved charges
160 if (stats && IsParticleCanonical(m_TPS->Particle(i)))
161 m_TPS->Particle(i).SetCalculationType(IdealGasFunctions::ClusterExpansion);
162 }
163 m_PartialZ.clear();
164 //CalculateQuantumNumbersRange();
165 }
166
167 // Solve non-canonical chemical potentials using a temporary GCE model.
168 // This avoids running the expensive (and potentially unstable at large muB)
169 // canonical partition function integration during each Broyden iteration.
171 {
172 // Build a temporary GCE model and solve for the non-canonical chemical potentials
173 ThermalModelIdeal tempGCE(m_TPS, m_Parameters);
174 tempGCE.SetUseWidth(m_UseWidth);
175
176 // Only constrain charges that are NOT canonical (canonical mus will be set to 0 later)
177 bool cB = m_ConstrainMuB && !m_BCE;
178 bool cQ = m_ConstrainMuQ && !m_QCE;
179 bool cS = m_ConstrainMuS && !m_SCE;
180 bool cC = m_ConstrainMuC && !m_CCE;
181
182 if (!cB && !cQ && !cS && !cC) {
183 // Nothing to solve for
184 return;
185 }
186
187 if (resetInitialValues) {
188 // Use reasonable initial guesses
189 if (cS)
190 tempGCE.SetStrangenessChemicalPotential(m_Parameters.muB / 3.);
191 if (cQ)
192 tempGCE.SetElectricChemicalPotential(-m_Parameters.muB / 30.);
193 }
194
195 // Copy constraint settings
196 tempGCE.ConstrainMuB(cB);
197 tempGCE.ConstrainMuQ(cQ);
198 tempGCE.ConstrainMuS(cS);
199 tempGCE.ConstrainMuC(cC);
200 tempGCE.SetQoverB(QoverB());
201 tempGCE.SetSoverB(SoverB());
202
203 if (resetInitialValues)
204 tempGCE.FixParameters();
205 else
206 tempGCE.FixParametersNoReset();
207
208 // Copy solved chemical potentials back
209 m_Parameters.muB = tempGCE.Parameters().muB;
210 m_Parameters.muQ = tempGCE.Parameters().muQ;
211 m_Parameters.muS = tempGCE.Parameters().muS;
212 m_Parameters.muC = tempGCE.Parameters().muC;
213 }
214
216 {
217 // Initial guess for gammaC if constrained
218 if (m_ConstrainGammaC) {
219 m_Parameters.gammaC = 1.0;
220 }
221
222 m_ConstrainMuB &= !m_BCE;
223 m_ConstrainMuQ &= !m_QCE;
224 m_ConstrainMuS &= !m_SCE;
225 m_ConstrainMuC &= !m_CCE;
226
227 // Step 1: GCE solve for stable initial chemical potentials.
228 // This avoids the CE Broyden solver wandering into extreme mu values
229 // where the GL integration may break down.
231
232 // Step 2: CE refinement using the base class Broyden solver,
233 // starting from the GCE-solved values.
234 // Each Broyden step calls CalculatePrimordialDensities(),
235 // which accounts for canonical corrections in the constraint equations.
236 // Since we start near the solution, the Broyden converges quickly.
237 // Note: For GL at finite muB, the integration may still be inaccurate.
238 // Use the SaddlePoint method for best results at finite muB.
240 }
241
243 {
244 m_ConstrainMuB &= !m_BCE;
245 m_ConstrainMuQ &= !m_QCE;
246 m_ConstrainMuS &= !m_SCE;
247 m_ConstrainMuC &= !m_CCE;
248
249 // Step 1: GCE solve for stable initial chemical potentials.
251
252 // Step 2: CE refinement using the base class Broyden solver.
254 }
255
256
257 // ==========================================================================
258 // 2. Primordial densities & validation
259 // ==========================================================================
260
262 assert(m_IGFExtraConfig.MagneticField.B == 0.); // No magnetic field supported currently
263
264 m_FluctuationsCalculated = false;
265
266 if (m_PartialZ.size() == 0)
268
269 // Non-ideal interaction models are only supported with SaddlePointLO; any
270 // other method (incl. GaussLegendre and plain SaddlePoint) falls back to it.
271 EnforceSaddlePointLOForNonIdeal();
272
273 // SaddlePointNLO/LO: compute means directly from CGF expansion, bypass partition functions
275 m_Banalyt = false;
276 ComputeAnalyticCumulants(false);
277 m_Calculated = true;
279 return;
280 }
281
282 if (m_Method == SaddlePoint) {
283 // Saddle-point method: chemical potentials will be handled inside
284 // ComputeSaddlePointPartitionFunctions() — zeroed after saddle point is found
285 m_Banalyt = false;
286 }
287 else if (m_BMAX_list == 1 && m_BCE && m_QCE && m_SCE && m_CCE && !UsePartialChemicalEquilibrium()) {
288 m_Banalyt = true;
289 m_Parameters.muB = 0.0;
290 m_Parameters.muQ = 0.0;
291 m_Parameters.muS = 0.0;
292 m_Parameters.muC = 0.0;
293 }
294 else {
295 m_Banalyt = false;
296 if (m_BCE)
297 m_Parameters.muB = 0.0;
298 if (m_QCE)
299 m_Parameters.muQ = 0.0;
300 if (m_SCE)
301 m_Parameters.muS = 0.0;
302 if (m_CCE)
303 m_Parameters.muC = 0.0;
304
305 //PrepareModelGCE(); // Plan B, may work better when quantum numbers are large
306 }
307
309
310 for (size_t i = 0; i < m_densities.size(); ++i) {
311 ThermalParticle &tpart = m_TPS->Particle(i);
312 m_densities[i] = 0.;
313
314 if (!IsParticleCanonical(tpart)) {
315 m_densities[i] = tpart.Density(m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
316 }
317 else if (tpart.Statistics() == 0
319 {
320 int ind = m_QNMap[QuantumNumbers(m_BCE * tpart.BaryonCharge(), m_QCE * tpart.ElectricCharge(), m_SCE * tpart.Strangeness(), m_CCE * tpart.Charm())];
321
322 if (ind < static_cast<int>(m_Corr.size()))
323 m_densities[i] = m_Corr[ind] * tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
324 else {
325 cout << "ThermalModelCanonical::CalculatePrimordialDensities: Warning! No canonical partition function for this particle!" << endl;
326 cout << "B = " << m_BCE * tpart.BaryonCharge() << "\tQ = " << m_QCE * tpart.ElectricCharge() << "\tS = " << m_SCE * tpart.Strangeness() << "\tC = " << m_CCE * tpart.Charm() << endl;
327 }
328 }
329 else {
330 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
331 int ind = m_QNMap[QuantumNumbers(m_BCE*n*tpart.BaryonCharge(), m_QCE*n*tpart.ElectricCharge(), m_SCE*n*tpart.Strangeness(), m_CCE*n*tpart.Charm())];
332 if (ind < static_cast<int>(m_Corr.size()))
333 m_densities[i] += m_Corr[ind] * tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
334 }
335 }
336 }
337
338 m_Calculated = true;
340 }
341
343 {
345
346 char cc[1000];
347
348 double TOL = 1.e-4;
349
350 // Checking that the CE calculation is valid
351 if (m_BCE && m_BMAX != 0) {
352 double totB = CalculateBaryonDensity() * m_Parameters.SVc;
353 if (fabs(m_Parameters.B - totB) > TOL) {
354
355 sprintf(cc, "**WARNING** ThermalModelCanonical: Inaccurate calculation of total baryon number.\
356\n\
357Expected: %d\n\
358Obtained: %lf\n\
359\n", m_Parameters.B, totB);
360
361 printf("%s", cc);
362
363 m_ValidityLog.append(cc);
364
365 m_LastCalculationSuccessFlag = false;
366 }
367 }
368
369
370 if (m_QCE && m_QMAX != 0) {
371 double totQ = CalculateChargeDensity() * m_Parameters.SVc;
372 if (fabs(m_Parameters.Q - totQ) > TOL) {
373 sprintf(cc, "**WARNING** ThermalModelCanonical: Inaccurate calculation of total electric charge.\
374\n\
375Expected: %d\n\
376Obtained: %lf\n\
377\n", m_Parameters.Q, totQ);
378
379 printf("%s", cc);
380
381 m_ValidityLog.append(cc);
382
383 m_LastCalculationSuccessFlag = false;
384 }
385 }
386
387
388 if (m_SCE && m_SMAX != 0) {
389 double totS = CalculateStrangenessDensity() * m_Parameters.SVc;
390 if (fabs(m_Parameters.S - totS) > TOL) {
391 sprintf(cc, "**WARNING** ThermalModelCanonical: Inaccurate calculation of total strangeness.\
392\n\
393Expected: %d\n\
394Obtained: %lf\n\
395\n", m_Parameters.S, totS);
396
397 printf("%s", cc);
398
399 m_ValidityLog.append(cc);
400
401 m_LastCalculationSuccessFlag = false;
402 }
403 }
404
405
406 if (m_CCE && m_CMAX != 0) {
407 double totC = CalculateCharmDensity() * m_Parameters.SVc;
408 if (fabs(m_Parameters.C - totC) > TOL) {
409 sprintf(cc, "**WARNING** ThermalModelCanonical: Inaccurate calculation of total charm.\
410\n\
411Expected: %d\n\
412Obtained: %lf\n\
413\n", m_Parameters.C, totC);
414
415 printf("%s", cc);
416
417 m_ValidityLog.append(cc);
418
419 m_LastCalculationSuccessFlag = false;
420 }
421 }
422 }
423
424 // ==========================================================================
425 // 3. Gauss-Legendre quadrature (canonical partition functions)
426 // ==========================================================================
427
429 {
430 // This method is public, so a direct external call must not bypass the
431 // non-ideal -> SaddlePointLO fallback that guards CalculateDensities() and
432 // CalculateFluctuations(). SaddlePointLO/NLO do not use precomputed
433 // partition functions (means and fluctuations come from
434 // ComputeAnalyticCumulants()), so there is nothing to compute here for them.
435 EnforceSaddlePointLOForNonIdeal();
437 return;
438
439 if (Vc < 0.0)
440 Vc = m_Parameters.SVc;
441
444 else {
445 // Partial chemical equilibrium canonical ensemble currently works only if there is particle-antiparticle symmetry
446 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
447 int i2 = m_TPS->PdgToId(-m_TPS->Particle(i).PdgId());
448 if (i2 != -1) {
449 if (std::abs(m_Chem[i] - m_Chem[i2]) > 1.e-8) {
450 throw std::runtime_error("ThermalModelCanonical::CalculatePartitionFunctions: Partial chemical equilibrium canonical ensemble only supported if particle-antiparticle fugacities are symmetric!");
451 }
452 }
453 }
454 }
455
456 // Saddle-point: self-contained, no need for Fourier coefficients
457 if (m_Method == SaddlePoint) {
458 ComputeSaddlePointPartitionFunctions();
459
460 m_Corr.resize(m_PartialZ.size());
461 for (size_t iN = 0; iN < m_PartialZ.size(); ++iN) {
462 m_Corr[iN] = m_PartialZ[iN] / m_PartialZ[m_QNMap[QuantumNumbers(0, 0, 0, 0)]];
463 }
465 return;
466 }
467
468 // Contour shift: set canonical chemical potentials to their
469 // GCE saddle-point values for dramatically improved GL convergence.
470 // This is mathematically exact (equivalent to shifting the Fourier
471 // contour through the saddle point). The fugacity factors from the
472 // shifted densities and the Z-tilde ratios cancel, giving the
473 // correct canonical result for all observables.
474 //
475 // Not used with m_Banalyt (analytical baryon fugacity via Bessel
476 // functions), which assumes N_+ = N_- symmetry that breaks at mu != 0.
477 // m_Banalyt is only active when ALL four charges are canonical, where
478 // the GL integrand is already well-behaved at mu = 0.
479 if (!m_Banalyt) {
480 PrepareModelGCE();
481 CleanModelGCE();
484 }
485
486 // --- Gauss-Legendre quadrature ---
487
488 bool AllMuZero = true;
489 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
490 ThermalParticle &tpart = m_TPS->Particle(i);
491 if (IsParticleCanonical(tpart) && m_Chem[i] != 0.0)
492 {
493 AllMuZero = false;
494 break;
495 }
496 }
497
498 vector<double> Nsx(m_PartialZ.size(), 0.);
499 vector<double> Nsy(m_PartialZ.size(), 0.);
500
501 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
502 ThermalParticle &tpart = m_TPS->Particle(i);
503
504 if (!IsParticleCanonical(tpart)) {
505 int ind = m_QNMap[QuantumNumbers(m_BCE * tpart.BaryonCharge(), m_QCE * tpart.ElectricCharge(), m_SCE * tpart.Strangeness(), m_CCE * tpart.Charm())];
506 if (ind != m_QNMap[QuantumNumbers(0, 0, 0, 0)]) {
507 throw std::invalid_argument("ThermalModelCanonical: neutral particle cannot have non-zero ce charges");
508 }
509 if (ind < static_cast<int>(Nsx.size()))
510 Nsx[ind] += tpart.Density(m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
511 }
512 else if (tpart.Statistics() == 0
514 int ind = m_QNMap[QuantumNumbers(m_BCE * tpart.BaryonCharge(), m_QCE * tpart.ElectricCharge(), m_SCE * tpart.Strangeness(), m_CCE * tpart.Charm())];
515
516 if (ind < static_cast<int>(Nsx.size())) {
518 double tdens = tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, 0.);
519 Nsx[ind] += tdens * cosh(m_Chem[i] / m_Parameters.T);
520 Nsy[ind] += tdens * sinh(m_Chem[i] / m_Parameters.T);
521 }
522 // Currently only works at mu = 0!!
523 else {
524 double tdens = tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
525 Nsx[ind] += tdens;
526 }
527 }
528 }
529 else {
530 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
531 int ind = m_QNMap[QuantumNumbers(m_BCE * n * tpart.BaryonCharge(), m_QCE * n * tpart.ElectricCharge(), m_SCE * n * tpart.Strangeness(), m_CCE * n * tpart.Charm())];
532 if (ind < static_cast<int>(Nsx.size())) {
534 double tdens = tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, 0.) / static_cast<double>(n); // TODO: Check
535 Nsx[ind] += tdens * cosh(n * m_Chem[i] / m_Parameters.T);
536 Nsy[ind] += tdens * sinh(n * m_Chem[i] / m_Parameters.T);
537 }
538 // Currently only works at mu = 0!!
539 else {
540 double tdens = tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]) / static_cast<double>(n); // TODO: Check
541 Nsx[ind] += tdens;
542 }
543 }
544 }
545 }
546 }
547
548 for (int i = 0; i < static_cast<int>(Nsx.size()); ++i) {
549 Nsx[i] *= Vc;
550 Nsy[i] *= Vc;
551 m_PartialZ[i] = 0.;
552 }
553
554 {
555 // --- Gauss-Legendre quadrature ---
556
557 int nmax = max(3, (int)sqrt((double)(m_Parameters.B*m_Parameters.B + m_Parameters.Q*m_Parameters.Q + m_Parameters.S*m_Parameters.S + m_Parameters.C*m_Parameters.C)));
558 if (m_Parameters.B == 0 && m_Parameters.Q == 0 && m_Parameters.S == 0 && m_Parameters.C == 0)
559 nmax = 4;
560
562
563
564 m_MultExp = 0.;
565 m_MultExpBanalyt = 0.;
566 for (size_t i = 0; i < m_PartialZ.size(); ++i) {
567 if (!m_Banalyt || m_QNvec[i].B == 0)
568 m_MultExp += Nsx[i];
569 if (m_Banalyt && (m_QNvec[i].B == 1 || m_QNvec[i].B == -1))
570 m_MultExpBanalyt += Nsx[i];
571 }
572
573 double dphiB = xMath::Pi() / nmax;
574 int maxB = 2 * nmax;
575 if (m_BMAX == 0 || m_Banalyt)
576 maxB = 1;
577
578 for (int iB = 0; iB < maxB; ++iB) {
579
580 vector<double> xlegB, wlegB;
581
582 if (m_BMAX != 0 && !m_Banalyt) {
583 double aB = iB * dphiB;
584 if (iB >= nmax) aB = xMath::Pi() - (iB + 1) * dphiB;
585 double bB = aB + dphiB;
587 }
588 else {
589 xlegB.resize(1);
590 xlegB[0] = 0.;
591 wlegB.resize(1);
592 wlegB[0] = 1.;
593 }
594
595
596 double dphiS = xMath::Pi() / nmax;
597 int maxS = 2 * nmax;
598 if (m_SMAX == 0)
599 maxS = 1;
600
601 for (int iS = 0; iS < maxS; ++iS) {
602 vector<double> xlegS, wlegS;
603
604 if (m_SMAX != 0) {
605 double aS = iS * dphiS;
606 if (iS >= nmax) aS = xMath::Pi() - (iS + 1) * dphiS;
607 double bS = aS + dphiS;
609 }
610 else {
611 xlegS.resize(1);
612 xlegS[0] = 0.;
613 wlegS.resize(1);
614 wlegS[0] = 1.;
615 }
616
617 double dphiQ = xMath::Pi() / nmax;
618 int maxQ = nmax;
619 if (m_QMAX == 0)
620 maxQ = 1;
621
622 for (int iQ = 0; iQ < maxQ; ++iQ) {
623 vector<double> xlegQ, wlegQ;
624
625 if (m_QMAX != 0) {
626 double aQ = iQ * dphiQ;
627 double bQ = aQ + dphiQ;
629 }
630 else {
631 xlegQ.resize(1);
632 xlegQ[0] = 0.;
633 wlegQ.resize(1);
634 wlegQ[0] = 1.;
635 }
636
637 double dphiC = xMath::Pi() / nmax;
638 int maxC = 2 * nmax;
639 if (m_CMAX == 0)
640 maxC = 1;
641
642 for (int iC = 0; iC < maxC; ++iC) {
643 vector<double> xlegC, wlegC;
644 if (m_CMAX != 0) {
645 double aC = iC * dphiC;
646 if (iC >= nmax) aC = xMath::Pi() - (iC + 1) * dphiC;
647 double bC = aC + dphiC;
649 }
650 else {
651 xlegC.resize(1);
652 xlegC[0] = 0.;
653 wlegC.resize(1);
654 wlegC[0] = 1.;
655 }
656
657 for (size_t iBt = 0; iBt < xlegB.size(); ++iBt) {
658 for (size_t iSt = 0; iSt < xlegS.size(); ++iSt) {
659 for (size_t iQt = 0; iQt < xlegQ.size(); ++iQt) {
660 for (size_t iCt = 0; iCt < xlegC.size(); ++iCt) {
661 vector<double> cosph(m_PartialZ.size(), 0.), sinph(m_PartialZ.size(), 0.);
662 double wx = 0., wy = 0., mx = 0., my = 0.;
663 for (size_t i = 0; i < m_PartialZ.size(); ++i) {
664 int tB = m_QNvec[i].B;
665 int tQ = m_QNvec[i].Q;
666 int tS = m_QNvec[i].S;
667 int tC = m_QNvec[i].C;
668
669 if (m_Banalyt) {
670 cosph[i] = cos(tS*xlegS[iSt] + tQ * xlegQ[iQt] + tC * xlegC[iCt]);
671 sinph[i] = sin(tS*xlegS[iSt] + tQ * xlegQ[iQt] + tC * xlegC[iCt]);
672
673 if (m_QNvec[i].B == 1) {
674 wx += Nsx[i] * cosph[i];
675 wy += Nsx[i] * sinph[i];
676 }
677 else if (m_QNvec[i].B == 0) {
678 mx += Nsx[i] * (cosph[i] - 1.);
679 }
680 }
681 else {
682 cosph[i] = cos(tB*xlegB[iBt] + tS * xlegS[iSt] + tQ * xlegQ[iQt] + tC * xlegC[iCt]);
683 mx += Nsx[i] * (cosph[i] - 1.);
684
685 if (!AllMuZero) {
686 sinph[i] = sin(tB*xlegB[iBt] + tS * xlegS[iSt] + tQ * xlegQ[iQt] + tC * xlegC[iCt]);
687 my += Nsy[i] * sinph[i];
688 }
689 }
690 }
691
692 double wmod = 0.;
693 double warg = 0.;
694
695 if (m_Banalyt) {
696 wmod = sqrt(wx*wx + wy * wy);
697 warg = atan2(wy, wx);
698 }
699
700 for (size_t iN = 0; iN < m_PartialZ.size(); ++iN) {
701 int tBg = m_Parameters.B - m_QNvec[iN].B;
702 int tQg = m_Parameters.Q - m_QNvec[iN].Q;
703 int tSg = m_Parameters.S - m_QNvec[iN].S;
704 int tCg = m_Parameters.C - m_QNvec[iN].C;
705
706 if (m_Banalyt) {
707 //m_PartialZ[iN] += wlegB[iBt] * wlegS[iSt] * wlegQ[iQt] * wlegC[iCt] * cos(tBg*xlegB[iBt] + tSg * xlegS[iSt] + tQg * xlegQ[iQt] + tCg * xlegC[iCt] - tBg * warg) * exp(mx) * xMath::BesselI(tBg, 2. * wmod);
708 m_PartialZ[iN] += wlegB[iBt] * wlegS[iSt] * wlegQ[iQt] * wlegC[iCt] *
709 cos(tBg * xlegB[iBt] + tSg * xlegS[iSt] + tQg * xlegQ[iQt] + tCg * xlegC[iCt] - tBg * warg) *
710 exp(mx + 2. * wmod - m_MultExpBanalyt) *
711 xMath::BesselIexp(tBg, 2. * wmod);
712 }
713 else {
714 if (AllMuZero)
715 m_PartialZ[iN] += wlegB[iBt] * wlegS[iSt] * wlegQ[iQt] * wlegC[iCt] * cos(tBg*xlegB[iBt] + tSg * xlegS[iSt] + tQg * xlegQ[iQt] + tCg * xlegC[iCt]) * exp(mx);
716 else
717 m_PartialZ[iN] += wlegB[iBt] * wlegS[iSt] * wlegQ[iQt] * wlegC[iCt] * exp(mx)
718 * (cos(tBg*xlegB[iBt] + tSg * xlegS[iSt] + tQg * xlegQ[iQt] + tCg * xlegC[iCt]) * cos(my)
719 + sin(tBg*xlegB[iBt] + tSg * xlegS[iSt] + tQg * xlegQ[iQt] + tCg * xlegC[iCt]) * sin(my));
720 }
721 }
722 }
723 }
724 }
725 }
726
727 //int cind = iB * maxS * maxQ * maxC + iS * maxQ * maxC + iQ * maxC + iC;
728 //int tot = maxB * maxS * maxQ * maxC;
729 }
730 }
731 }
732 }
733
734 for (size_t iN = 0; iN < m_PartialZ.size(); ++iN) {
735 if (m_BMAX != 0 && !m_Banalyt)
736 m_PartialZ[iN] /= 2. * xMath::Pi();
737 if (m_QMAX != 0) {
738 m_PartialZ[iN] /= 2. * xMath::Pi();
739 m_PartialZ[iN] *= 2.; // Factor 2 from half-range [0,pi] integration for phi_Q
740 }
741 if (m_SMAX != 0)
742 m_PartialZ[iN] /= 2. * xMath::Pi();
743 if (m_CMAX != 0)
744 m_PartialZ[iN] /= 2. * xMath::Pi();
745 }
746
747 } // end GL quadrature block
748
749 m_Corr.resize(m_PartialZ.size());
750 for (size_t iN = 0; iN < m_PartialZ.size(); ++iN) {
751 m_Corr[iN] = m_PartialZ[iN] / m_PartialZ[m_QNMap[QuantumNumbers(0, 0, 0, 0)]];
752 }
753
755 }
756
757 // ==========================================================================
758 // 4. Fluctuations & thermodynamics
759 // ==========================================================================
760
762 {
763 ThermalParticle &tpart = m_TPS->Particle(part);
764 double ret1 = 0., ret2 = 0., ret3 = 0.;
765
766 if (!IsParticleCanonical(tpart)) {
767 return tpart.ScaledVariance(m_Parameters, m_UseWidth, m_Chem[part]);
768 }
769 else if (tpart.Statistics() == 0
771 {
772 int ind = m_QNMap[QuantumNumbers(m_BCE * tpart.BaryonCharge(), m_QCE * tpart.ElectricCharge(), m_SCE * tpart.Strangeness(), m_CCE * tpart.Charm())];
773 int ind2 = m_QNMap[QuantumNumbers(m_BCE * 2 * tpart.BaryonCharge(), m_QCE * 2 * tpart.ElectricCharge(), m_SCE * 2 * tpart.Strangeness(), m_CCE * 2 * tpart.Charm())];
774
775 ret1 = 1.;
776 if (ind < static_cast<int>(m_Corr.size()) && ind2 < static_cast<int>(m_Corr.size()))
777 ret2 = m_Corr[ind2] / m_Corr[ind] * m_Parameters.SVc * tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[part]);
778
779 if (ind < static_cast<int>(m_Corr.size()))
780 ret3 = -m_Corr[ind] * m_Parameters.SVc * tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[part]);
781 }
782 else {
783 double ret1num = 0., ret1zn = 0.;
784 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
785 int ind = m_QNMap[QuantumNumbers(m_BCE*n*tpart.BaryonCharge(), m_QCE*n*tpart.ElectricCharge(), m_SCE*n*tpart.Strangeness(), m_CCE*n*tpart.Charm())];
786
787 double densityClusterN = tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[part]);
788
789 if (ind < static_cast<int>(m_Corr.size())) {
790 ret1num += m_Corr[ind] * n * densityClusterN;
791 ret1zn += m_Corr[ind] * densityClusterN;
792 }
793
794 for (int n2 = 1; n2 <= tpart.ClusterExpansionOrder(); ++n2) {
795 if (m_QNMap.count(QuantumNumbers(m_BCE * (n + n2)*tpart.BaryonCharge(), m_QCE * (n + n2)*tpart.ElectricCharge(), m_SCE * (n + n2)*tpart.Strangeness(), m_CCE * (n + n2)*tpart.Charm())) != 0) {
796 int ind2 = m_QNMap[QuantumNumbers(m_BCE * (n + n2)*tpart.BaryonCharge(), m_QCE * (n + n2)*tpart.ElectricCharge(), m_SCE * (n + n2)*tpart.Strangeness(), m_CCE * (n + n2)*tpart.Charm())];
797 if (ind < static_cast<int>(m_Corr.size()) && ind2 < static_cast<int>(m_Corr.size()))
798 ret2 += densityClusterN * m_Corr[ind2] * m_Parameters.SVc * tpart.DensityCluster(n2, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[part]);
799 }
800 }
801 }
802
803 if (ret1zn == 0.0)
804 return 1.;
805
806 ret1 = ret1num / ret1zn;
807 ret2 = ret2 / ret1zn;
808 ret3 = -ret1zn * m_Parameters.SVc;
809 }
810 return ret1 + ret2 + ret3;
811 }
812
814 int NN = m_densities.size();
815
816 vector<double> yld(NN, 0);
817 vector<double> ret1num(NN, 0);
818 vector< vector<double> > ret2num(NN, vector<double>(NN, 0.));
819
820 for (int i = 0; i < NN; ++i)
821 yld[i] = m_densities[i] * m_Parameters.SVc;
822
823 for (int i = 0; i < NN; ++i) {
824 ThermalParticle &tpart = m_TPS->Particle(i);
825 if (!IsParticleCanonical(tpart)) {
826 ret1num[i] = tpart.ScaledVariance(m_Parameters, m_UseWidth, m_Chem[i]) * yld[i];
827 }
828 else if (tpart.Statistics() == 0
830 {
831 ret1num[i] = yld[i];
832 }
833 else {
834 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
835 int ind = m_QNMap[QuantumNumbers(m_BCE * n * tpart.BaryonCharge(), m_QCE * n * tpart.ElectricCharge(), m_SCE * n * tpart.Strangeness(), m_CCE * n * tpart.Charm())];
836
837 double densityClusterN = tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
838
839 if (ind < static_cast<int>(m_Corr.size()))
840 ret1num[i] += m_Corr[ind] * n * densityClusterN * m_Parameters.SVc;
841 }
842 }
843 }
844
845 for (int i = 0; i < NN; ++i) {
846 for (int j = 0; j < NN; ++j) {
847 ThermalParticle &tpart1 = m_TPS->Particle(i);
848 ThermalParticle &tpart2 = m_TPS->Particle(j);
849
850 int n1max = tpart1.ClusterExpansionOrder();
851 int n2max = tpart2.ClusterExpansionOrder();
852
853 if (tpart1.Statistics() == 0 || tpart1.CalculationType() != IdealGasFunctions::ClusterExpansion)
854 n1max = 1;
855 if (tpart2.Statistics() == 0 || tpart2.CalculationType() != IdealGasFunctions::ClusterExpansion)
856 n2max = 1;
857
858 if (!IsParticleCanonical(tpart1) || !IsParticleCanonical(tpart2)) {
859 ret2num[i][j] = yld[i] * yld[j];
860 }
861 else {
862 for (int n1 = 1; n1 <= n1max; ++n1) {
863 for (int n2 = 1; n2 <= n2max; ++n2) {
864 int ind = m_QNMap[QuantumNumbers(
865 m_BCE*(n1*tpart1.BaryonCharge() + n2 * tpart2.BaryonCharge()),
866 m_QCE*(n1*tpart1.ElectricCharge() + n2 * tpart2.ElectricCharge()),
867 m_SCE*(n1*tpart1.Strangeness() + n2 * tpart2.Strangeness()),
868 m_CCE*(n1*tpart1.Charm() + n2 * tpart2.Charm()))];
869
870 double densityClusterN1 = tpart1.DensityCluster(n1, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
871 double densityClusterN2 = tpart2.DensityCluster(n2, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[j]);
872
873 if (ind < static_cast<int>(m_Corr.size()))
874 ret2num[i][j] += m_Corr[ind] * densityClusterN1 * densityClusterN2 * m_Parameters.SVc * m_Parameters.SVc;
875 }
876 }
877 }
878 }
879 }
880
881
882 m_PrimCorrel.resize(NN);
883 for (int i = 0; i < NN; ++i)
884 m_PrimCorrel[i].resize(NN);
885 m_TotalCorrel = m_PrimCorrel;
886
887 for (int i = 0; i < NN; ++i) {
888 for (int j = 0; j < NN; ++j) {
889 m_PrimCorrel[i][j] = 0.;
890 if (i == j)
891 m_PrimCorrel[i][j] += ret1num[i] / yld[i];
892 m_PrimCorrel[i][j] += ret2num[i][j] / yld[i];
893 m_PrimCorrel[i][j] += -yld[j];
894 m_PrimCorrel[i][j] *= yld[i] / m_Parameters.SVc / m_Parameters.T;
895 if (yld[i] == 0.0)
896 m_PrimCorrel[i][j] = 0.;
897 }
898 }
899
900 for (int i = 0; i < NN; ++i) {
901 m_wprim[i] = m_PrimCorrel[i][i];
902 if (m_densities[i] > 0.) m_wprim[i] *= m_Parameters.T / m_densities[i];
903 else m_wprim[i] = 1.;
904 if (m_wprim[i] != m_wprim[i]) m_wprim[i] = 1.;
905 }
906
907 m_TwoParticleCorrelationsCalculated = true;
908 }
909
911 EnforceSaddlePointLOForNonIdeal();
913 // SaddlePointNLO/LO: compute means + covariance in one shot
914 if (m_PartialZ.size() == 0)
916 ComputeAnalyticCumulants(true);
917 m_Calculated = true;
918 // Skip CalculateTwoParticleCorrelations — already filled by ComputeAnalyticCumulants
919 }
920 else {
924 }
926 }
927
932
933 m_FluctuationsCalculated = true;
934
935 for (size_t i = 0; i < m_wprim.size(); ++i) {
936 m_skewprim[i] = 1.;
937 m_kurtprim[i] = 1.;
938 m_skewtot[i] = 1.;
939 m_kurttot[i] = 1.;
940 }
941 }
942
944 if (!m_Calculated) CalculateDensities();
945
946 // SaddlePointNLO/LO: energy density from saddle-point expansion.
947 // NLO: ε = ε^GCE(μ*) - (1/2) Σ_j E_j G_jj
948 // LO: ε = ε^GCE(μ*)
949 // Computed and stored by ComputeAnalyticCumulants().
951 return m_NLOEnergyDensity;
952
953 double ret = 0.;
954
955 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
956 ThermalParticle &tpart = m_TPS->Particle(i);
957 {
958 if (!IsParticleCanonical(tpart)) {
959 ret += tpart.Density(m_Parameters, IdealGasFunctions::EnergyDensity, m_UseWidth, m_Chem[i]);
960 }
961 else if (tpart.Statistics() == 0
963 int ind = m_QNMap[QuantumNumbers(m_BCE * tpart.BaryonCharge(), m_QCE * tpart.ElectricCharge(), m_SCE * tpart.Strangeness(), m_CCE * tpart.Charm())];
964
965 if (ind < static_cast<int>(m_Corr.size()))
966 ret += m_Corr[ind] * tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::EnergyDensity, m_UseWidth, m_Chem[i]);
967 }
968 else {
969 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
970 int ind = m_QNMap[QuantumNumbers(m_BCE * n * tpart.BaryonCharge(), m_QCE * n * tpart.ElectricCharge(), m_SCE * n * tpart.Strangeness(), m_CCE * n * tpart.Charm())];
971 if (ind < static_cast<int>(m_Corr.size()))
972 ret += m_Corr[ind] * tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::EnergyDensity, m_UseWidth, m_Chem[i]);
973 }
974 }
975 }
976 }
977
978 return ret;
979 }
980
982 if (!m_Calculated) CalculateDensities();
983
984 // SaddlePointNLO/LO: pressure from saddle-point expansion.
985 // NLO: P = p^GCE(μ*) - d·T/(2·Vc)
986 // LO: P = p^GCE(μ*)
987 // Computed and stored by ComputeAnalyticCumulants().
989 return m_NLOPressure;
990
991 double ret = 0.;
992
993 for (int i = 0; i < m_TPS->ComponentsNumber(); ++i) {
994 ThermalParticle &tpart = m_TPS->Particle(i);
995 {
996 if (!IsParticleCanonical(tpart)) {
997 ret += tpart.Density(m_Parameters, IdealGasFunctions::Pressure, m_UseWidth, m_Chem[i]);
998 }
999 else if (tpart.Statistics() == 0
1001 int ind = m_QNMap[QuantumNumbers(m_BCE * tpart.BaryonCharge(), m_QCE * tpart.ElectricCharge(), m_SCE * tpart.Strangeness(), m_CCE * tpart.Charm())];
1002
1003 if (ind < static_cast<int>(m_Corr.size()))
1004 ret += m_Corr[ind] * tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::Pressure, m_UseWidth, m_Chem[i]);
1005 }
1006 else {
1007 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
1008 int ind = m_QNMap[QuantumNumbers(m_BCE * n * tpart.BaryonCharge(), m_QCE * n * tpart.ElectricCharge(), m_SCE * n * tpart.Strangeness(), m_CCE * n * tpart.Charm())];
1009
1010 if (ind < static_cast<int>(m_Corr.size()))
1011 ret += m_Corr[ind] * tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::Pressure, m_UseWidth, m_Chem[i]);
1012 }
1013 }
1014 }
1015 }
1016
1017 return ret;
1018 }
1019
1021 {
1022 if (!m_Calculated) CalculateDensities();
1023
1024 // SaddlePointLO: entropy is the GCE entropy at the saddle point μ*,
1025 // consistent with the LO pressure and energy density (the ln det Sigma
1026 // Gaussian term is an NLO effect). Stored by ComputeAnalyticCumulants().
1027 // NLO and the other methods use the ln Z_SP form below.
1028 if (m_Method == SaddlePointLO)
1029 return m_NLOEntropyDensity;
1030
1031 double ret = (CalculateEnergyDensity() / m_Parameters.T) + (m_MultExp + m_MultExpBanalyt + log(m_PartialZ[m_QNMap[QuantumNumbers(0, 0, 0, 0)]])) / m_Parameters.SVc;
1032
1033 if (m_BCE)
1034 ret += -m_Parameters.muB / m_Parameters.T * m_Parameters.B / m_Parameters.SVc;
1035 else
1036 ret += -m_Parameters.muB * CalculateBaryonDensity() / m_Parameters.T;
1037
1038 if (m_QCE)
1039 ret += -m_Parameters.muQ / m_Parameters.T * m_Parameters.Q / m_Parameters.SVc;
1040 else
1041 ret += -m_Parameters.muQ * CalculateChargeDensity() / m_Parameters.T;
1042
1043 if (m_SCE)
1044 ret += -m_Parameters.muS / m_Parameters.T * m_Parameters.S / m_Parameters.SVc;
1045 else
1046 ret += -m_Parameters.muS * CalculateStrangenessDensity() / m_Parameters.T;
1047
1048 if (m_CCE)
1049 ret += -m_Parameters.muC / m_Parameters.T * m_Parameters.C / m_Parameters.SVc;
1050 else
1051 ret += -m_Parameters.muC * CalculateCharmDensity() / m_Parameters.T;
1052
1053 return ret;
1054 }
1055
1056 // ==========================================================================
1057 // 5. Helpers & charge predicates
1058 // ==========================================================================
1059
1061 {
1062 return m_TPS->Particles()[i].Density(m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_Chem[i]);
1063 }
1064
1066 {
1067 return !(
1068 (part.BaryonCharge() == 0 || m_BCE == 0)
1069 && (part.ElectricCharge() == 0 || m_QCE == 0)
1070 && (part.Strangeness() == 0 || m_SCE == 0)
1071 && (part.Charm() == 0 || m_CCE == 0)
1072 );
1073 }
1074
1076 {
1077 if (charge == ConservedCharge::BaryonCharge)
1078 return (m_BCE != 0);
1079 else if (charge == ConservedCharge::ElectricCharge)
1080 return (m_QCE != 0);
1081 else if (charge == ConservedCharge::StrangenessCharge)
1082 return (m_SCE != 0);
1083 else if (charge == ConservedCharge::CharmCharge)
1084 return (m_CCE != 0);
1085 return 0;
1086 }
1087
1088 // PrepareModelGCE() selects and configures the GCE model used to evaluate
1089 // densities, susceptibilities, and two-particle correlations at the
1090 // saddle-point chemical potentials.
1091 //
1092 // Uses the external model (set via SetModelGCE) if available,
1093 // otherwise uses the persistent default ThermalModelIdeal.
1094 // Both models share the same TPS, so statistics and width settings
1095 // are already in sync. Only thermal parameters need to be updated.
1096 // ==========================================================================
1097 // 6. GCE model management (shared by the GL contour shift and saddle point)
1098 // ==========================================================================
1099
1100 void ThermalModelCanonical::PrepareModelGCE()
1101 {
1103
1104 // Sync thermal parameters (T, V, mu, gammas, etc.)
1105 m_modelgce->SetParameters(m_Parameters);
1106
1107 if (m_BCE)
1108 m_Parameters.muB = 0.0;
1109
1110 if (!m_BCE && m_SCE)
1111 m_Parameters.muS = m_Parameters.muB / 3.;
1112
1113 if (!m_BCE && m_QCE)
1114 m_Parameters.muQ = -m_Parameters.muB / 30.;
1115
1116 // Only reset muC when charm is canonical; otherwise muC is a fixed
1117 // grand-canonical input and must be preserved through the GCE solve.
1118 if (m_CCE)
1119 m_Parameters.muC = 0.;
1120
1121 m_modelgce->SolveChemicalPotentials(m_Parameters.B, m_Parameters.Q, m_Parameters.S, m_Parameters.C,
1122 m_Parameters.muB, m_Parameters.muQ, m_Parameters.muS, m_Parameters.muC,
1123 static_cast<bool>(m_BCE),
1124 static_cast<bool>(m_QCE),
1125 static_cast<bool>(m_SCE),
1126 static_cast<bool>(m_CCE));
1127
1128 m_Parameters.muB = m_modelgce->Parameters().muB;
1129 m_Parameters.muQ = m_modelgce->Parameters().muQ;
1130 m_Parameters.muS = m_modelgce->Parameters().muS;
1131 m_Parameters.muC = m_modelgce->Parameters().muC;
1132 }
1133
1134 void ThermalModelCanonical::CleanModelGCE()
1135 {
1136 m_modelgce = NULL;
1137 }
1138
1140 {
1141 m_modelgce_ext = model;
1142 if (model != NULL)
1143 m_InteractionModel = model->InteractionModel();
1144 else
1145 m_InteractionModel = Ideal;
1146 m_PartialZCalculated = false;
1147 }
1148
1149 void ThermalModelCanonical::EnforceSaddlePointLOForNonIdeal()
1150 {
1151 // Non-ideal equations of state are only supported by the SaddlePointLO
1152 // method. Every other method (GaussLegendre, SaddlePoint, SaddlePointNLO)
1153 // assumes an ideal gas, so silently mixing a non-ideal GCE model with them
1154 // would be inconsistent (ratios/thermodynamics from the interacting model,
1155 // densities from ideal-gas cluster sums). Fall back to SaddlePointLO.
1156 if (m_InteractionModel != Ideal && m_Method != SaddlePointLO) {
1157 std::cerr << "**WARNING** " << m_TAG
1158 << ": Non-ideal interaction model is only supported with the SaddlePointLO method. "
1159 << "Falling back to SaddlePointLO." << std::endl;
1161 }
1162 }
1163
1164 // ==========================================================================
1165 // 7. Saddle-point methods (SaddlePoint / SaddlePointNLO / SaddlePointLO)
1166 // ==========================================================================
1167
1168 void ThermalModelCanonical::SolveSaddlePointEquations()
1169 {
1170 // Use PrepareModelGCE to solve the GCE constraint equations:
1171 // <Q_a>_GCE(mu*/T) = N_a / V_c
1172 // This IS the saddle-point condition.
1173 PrepareModelGCE();
1174
1175 // Store saddle-point chemical potentials mu*_a/T
1176 m_SaddlePointMu.resize(4);
1177 m_SaddlePointMu[0] = m_Parameters.muB / m_Parameters.T;
1178 m_SaddlePointMu[1] = m_Parameters.muQ / m_Parameters.T;
1179 m_SaddlePointMu[2] = m_Parameters.muS / m_Parameters.T;
1180 m_SaddlePointMu[3] = m_Parameters.muC / m_Parameters.T;
1181
1182 // Store per-particle chemical potentials at the saddle point
1183 int N = m_TPS->ComponentsNumber();
1184 m_SaddlePointMuStar.resize(N);
1185 for (int j = 0; j < N; ++j) {
1186 m_SaddlePointMuStar[j] = m_modelgce->ChemicalPotential(j);
1187 }
1188
1189 // NOTE: m_modelgce is intentionally NOT cleaned up here.
1190 // The caller manages cleanup so that ComputeAnalyticCumulants can use
1191 // m_modelgce to compute GCE two-particle correlations (cross-susceptibilities
1192 // for non-ideal models).
1193
1194 // Zero the canonical chemical potentials
1195 if (m_BCE)
1196 m_Parameters.muB = 0.0;
1197 if (m_QCE)
1198 m_Parameters.muQ = 0.0;
1199 if (m_SCE)
1200 m_Parameters.muS = 0.0;
1201 if (m_CCE)
1202 m_Parameters.muC = 0.0;
1203
1205 }
1206
1207 void ThermalModelCanonical::SetupSaddlePointChargeIndices()
1208 {
1210 if (m_BCE && m_BMAX_list > 0) m_SaddlePointChargeIndices.push_back(0); // B
1211 if (m_QCE && m_QMAX_list > 0) m_SaddlePointChargeIndices.push_back(1); // Q
1212 if (m_SCE && m_SMAX_list > 0) m_SaddlePointChargeIndices.push_back(2); // S
1213 if (m_CCE && m_CMAX_list > 0) m_SaddlePointChargeIndices.push_back(3); // C
1214 m_SaddlePointDim = static_cast<int>(m_SaddlePointChargeIndices.size());
1215 }
1216
1217 double ThermalModelCanonical::ComputeSigmaLogDetFromModel()
1218 {
1219 int d = m_SaddlePointDim;
1220 if (d == 0)
1221 return 0.0;
1222
1223 int Nparts = m_TPS->ComponentsNumber();
1224 double Vc = m_Parameters.SVc;
1225 double T = m_Parameters.T;
1226 const auto& pc = m_modelgce->PrimCorrel();
1227
1228 // Build Sigma_{ab} = sum_{j,k} q_{a,j} q_{b,k} chi^GCE_{jk}
1229 // where chi^GCE_{jk} = PrimCorrel[j][k] * Vc * T
1230 Eigen::MatrixXd Sigma = Eigen::MatrixXd::Zero(d, d);
1231 for (int j = 0; j < Nparts; ++j) {
1232 for (int k = 0; k < Nparts; ++k) {
1233 if (pc[j][k] == 0.0) continue;
1234 double chi_jk = pc[j][k] * Vc * T;
1235 for (int ia = 0; ia < d; ++ia) {
1236 int a = m_SaddlePointChargeIndices[ia];
1237 double q_aj = m_TPS->Particle(j).GetCharge(a);
1238 if (q_aj == 0.0) continue;
1239 for (int ib = 0; ib < d; ++ib) {
1240 int b = m_SaddlePointChargeIndices[ib];
1241 Sigma(ia, ib) += q_aj * m_TPS->Particle(k).GetCharge(b) * chi_jk;
1242 }
1243 }
1244 }
1245 }
1246
1247 // log(det(Sigma)) via Cholesky
1248 double logDet;
1249 Eigen::LLT<Eigen::MatrixXd> llt(Sigma);
1250 if (llt.info() == Eigen::Success) {
1251 logDet = 0.0;
1252 for (int i = 0; i < d; ++i)
1253 logDet += 2.0 * log(llt.matrixL()(i, i));
1254 } else {
1255 double det = Sigma.determinant();
1256 logDet = (det > 0.0) ? log(det) : -1.e100;
1257 }
1258
1259 return logDet;
1260 }
1261
1262 void ThermalModelCanonical::ComputeSaddlePointPartitionFunctions()
1263 {
1264 // Step 1: Find saddle-point chemical potentials.
1265 // m_modelgce is kept alive (no CleanModelGCE) so we can use it
1266 // for re-solving at shifted charges, pressure, and Sigma.
1267 SolveSaddlePointEquations();
1268
1269 // Step 2: Build susceptibility matrix from m_modelgce
1270 SetupSaddlePointChargeIndices();
1271 m_modelgce->CalculateTwoParticleCorrelations();
1272 m_SaddlePointLogDetSigma = ComputeSigmaLogDetFromModel();
1273
1274 int d = m_SaddlePointDim;
1275 m_MultExp = 0.0;
1276 m_MultExpBanalyt = 0.0;
1277
1278 // Reference charges
1279 double Nref[4] = {
1280 static_cast<double>(m_Parameters.B),
1281 static_cast<double>(m_Parameters.Q),
1282 static_cast<double>(m_Parameters.S),
1283 static_cast<double>(m_Parameters.C)
1284 };
1285
1286 int Nparts = m_TPS->ComponentsNumber();
1287 double Vc = m_Parameters.SVc;
1288 double T = m_Parameters.T;
1289
1290 // Reference pressure from the GCE model at the saddle point
1291 double p0 = m_modelgce->CalculatePressure();
1292
1293 // Compute ln Z_SP(N) = Vc*p0/T - sum_a mu*_a N_a/T - d/2 ln(2pi) - 1/2 ln(det Sigma)
1294 {
1295 double term1 = p0 * Vc / T;
1296 double term2 = 0.0;
1297 for (int ia = 0; ia < d; ++ia) {
1298 int a = m_SaddlePointChargeIndices[ia];
1299 term2 -= m_SaddlePointMu[a] * Nref[a];
1300 }
1301 double term3 = -0.5 * d * log(2.0 * xMath::Pi()) - 0.5 * m_SaddlePointLogDetSigma;
1302 m_MultExp = term1 + term2 + term3;
1303 }
1304
1305 double muBguess = m_SaddlePointMu[0] * T;
1306 double muQguess = m_SaddlePointMu[1] * T;
1307 double muSguess = m_SaddlePointMu[2] * T;
1308 double muCguess = m_SaddlePointMu[3] * T;
1309
1310 // Compute reference open charm density at the saddle point (for analytical muC initial guess).
1311 // n_charm_ref = (1/2) * sum_{|C_j|>0} |C_j| * n_j(mu*) ≈ half the absolute charm density
1312 double nCharmRef = 0.0;
1313 {
1314 const auto& dens = m_modelgce->Densities();
1315 for (int j = 0; j < Nparts; ++j) {
1316 int absC = abs(m_TPS->Particle(j).Charm());
1317 if (absC > 0)
1318 nCharmRef += absC * dens[j];
1319 }
1320 }
1321 nCharmRef *= 0.5; // half = positive-charm density ≈ negative-charm density at mu*
1322
1323 // Reference mu . N term
1324 double muDotN_0 = 0.0;
1325 for (int ia = 0; ia < d; ++ia) {
1326 int a = m_SaddlePointChargeIndices[ia];
1327 muDotN_0 += m_SaddlePointMu[a] * Nref[a];
1328 }
1329
1330 double logDetSigma_0 = m_SaddlePointLogDetSigma;
1331
1332 int zeroIdx = m_QNMap[QuantumNumbers(0, 0, 0, 0)];
1333
1334 for (size_t iN = 0; iN < m_PartialZ.size(); ++iN) {
1335 if (static_cast<int>(iN) == zeroIdx) {
1336 m_PartialZ[iN] = 1.0;
1337 continue;
1338 }
1339
1340 int charges[4] = { m_QNvec[iN].B, m_QNvec[iN].Q, m_QNvec[iN].S, m_QNvec[iN].C };
1341
1342 // Shifted target charges
1343 double Nshifted[4] = { Nref[0], Nref[1], Nref[2], Nref[3] };
1344 for (int ia = 0; ia < d; ++ia) {
1345 int a = m_SaddlePointChargeIndices[ia];
1346 Nshifted[a] -= charges[a];
1347 }
1348
1349 // Analytical muC estimate for the shifted charm target.
1350 // Avoids overshooting in the first Newton step when charm is rare.
1351 // <C> ≈ 2 * nCharmRef * V * sinh(muC/T) → muC = T * arcsinh(Cshifted / (2 * nCharmRef * V))
1352 double muCguess_shifted = muCguess;
1353 if (m_CCE && nCharmRef > 0.0 && Nshifted[3] != 0.0) {
1354 double arg = Nshifted[3] / (2.0 * nCharmRef * m_Parameters.V);
1355 muCguess_shifted = m_Parameters.T * asinh(arg);
1356 }
1357
1358 // For zero-shifted charges (charges[a]==0), use the saddle-point mu as the initial guess
1359 // rather than the previous sector's solved mu. This prevents garbage propagation from
1360 // sectors where the solver converged to an unphysical fixed point due to ill-conditioning.
1361 // Shifted charges use the running muGuess from nearby sectors for faster convergence.
1362 double muBinit = (charges[0] != 0) ? muBguess : m_SaddlePointMu[0] * m_Parameters.T;
1363 double muQinit = (charges[1] != 0) ? muQguess : m_SaddlePointMu[1] * m_Parameters.T;
1364 double muSinit = (charges[2] != 0) ? muSguess : m_SaddlePointMu[2] * m_Parameters.T;
1365 double muCinit = (charges[3] != 0) ? muCguess_shifted : m_SaddlePointMu[3] * m_Parameters.T;
1366
1367 // Solve GCE constraint equations for shifted charges using m_modelgce.
1368 // This uses the correct equation of state (ideal or interacting).
1369 // All canonical charges are constrained to capture cross-correlations
1370 // (e.g. mu_C shifts when mu_B shifts, through charmed baryons like Lambda_c).
1371 bool converged = m_modelgce->SolveChemicalPotentials(
1372 Nshifted[0], Nshifted[1], Nshifted[2], Nshifted[3],
1373 muBinit, muQinit, muSinit, muCinit,
1374 static_cast<bool>(m_BCE), static_cast<bool>(m_QCE),
1375 static_cast<bool>(m_SCE), static_cast<bool>(m_CCE));
1376
1377 if (!converged) {
1378 // Solver did not converge for this QN sector: treat as inaccessible
1379 printf("**WARNING** ThermalModelCanonical::ComputeSaddlePointPartitionFunctions: "
1380 "Solver did not converge for QN sector (B,Q,S,C) = (%.0f,%.0f,%.0f,%.0f). Skipping.\n",
1381 Nshifted[0], Nshifted[1], Nshifted[2], Nshifted[3]);
1382 m_PartialZ[iN] = 0.0;
1383 continue;
1384 }
1385
1386 double muOverT_c[4];
1387 muOverT_c[0] = m_modelgce->Parameters().muB / T;
1388 muOverT_c[1] = m_modelgce->Parameters().muQ / T;
1389 muOverT_c[2] = m_modelgce->Parameters().muS / T;
1390 muOverT_c[3] = m_modelgce->Parameters().muC / T;
1391
1392 // Check for NaN/inf in solved chemical potentials
1393 bool muValid = true;
1394 for (int ia = 0; ia < 4; ++ia) {
1395 if (!std::isfinite(muOverT_c[ia])) {
1396 muValid = false;
1397 break;
1398 }
1399 }
1400 if (!muValid) {
1401 printf("**WARNING** ThermalModelCanonical::ComputeSaddlePointPartitionFunctions: "
1402 "NaN/inf chemical potentials for QN sector (B,Q,S,C) = (%.0f,%.0f,%.0f,%.0f). Skipping.\n",
1403 Nshifted[0], Nshifted[1], Nshifted[2], Nshifted[3]);
1404 m_PartialZ[iN] = 0.0;
1405 continue;
1406 }
1407
1408 // Update muGuess only for charges that are shifted in this sector.
1409 // For zero-shifted charges, the solved mu captures cross-correlations but
1410 // is not a useful initial guess for other sectors with different shift patterns.
1411 if (charges[0] != 0) muBguess = m_modelgce->Parameters().muB;
1412 if (charges[1] != 0) muQguess = m_modelgce->Parameters().muQ;
1413 if (charges[2] != 0) muSguess = m_modelgce->Parameters().muS;
1414 if (charges[3] != 0) muCguess = m_modelgce->Parameters().muC;
1415
1416 std::vector<double> muStar_c(Nparts);
1417 for (int j = 0; j < Nparts; ++j) {
1418 muStar_c[j] = m_modelgce->ChemicalPotential(j);
1419 }
1420
1421 // Term 1: Vc/T * [p_c - p_0]
1422 double p_c = m_modelgce->CalculatePressure();
1423 double deltaP = Vc * (p_c - p0) / T;
1424
1425 // Term 2: -[mu_c . N' - mu_0 . N]/T
1426 double muDotN_c = 0.0;
1427 for (int ia = 0; ia < d; ++ia) {
1428 int a = m_SaddlePointChargeIndices[ia];
1429 muDotN_c += muOverT_c[a] * Nshifted[a];
1430 }
1431 double deltaMuN = muDotN_c - muDotN_0;
1432
1433 // Term 3: -1/2 * [ln det Sigma_c - ln det Sigma_0]
1434 // Use the interacting model's PrimCorrel at the shifted mu
1435 m_modelgce->CalculateTwoParticleCorrelations();
1436 double logDetSigma_c = ComputeSigmaLogDetFromModel();
1437 double deltaLogDet = logDetSigma_c - logDetSigma_0;
1438
1439 // ln R(c) = deltaP - deltaMuN - 1/2 * deltaLogDet
1440 double lnR = deltaP - deltaMuN - 0.5 * deltaLogDet;
1441
1442 // Guard against NaN propagation
1443 if (!std::isfinite(lnR)) {
1444 m_PartialZ[iN] = 0.0;
1445 continue;
1446 }
1447
1448 m_PartialZ[iN] = exp(lnR);
1449 }
1450
1451 CleanModelGCE();
1452 }
1453
1454 void ThermalModelCanonical::ComputeAnalyticCumulants(bool computeCovariance)
1455 {
1456 // Analytic LO (or LO+NLO) from the CGF expansion.
1457 // Single saddle-point solve, then direct computation of means and (optionally) covariances.
1458 // For SaddlePointLO, only the leading-order terms are kept.
1459 //
1460 // All GCE quantities (densities, thermodynamics, susceptibilities) are read
1461 // from m_modelgce, which is always valid (either the external interacting model
1462 // or the default ThermalModelIdeal). This ensures correct results for both
1463 // ideal and non-ideal equations of state.
1464
1465 bool isNLO = (m_Method == SaddlePointNLO);
1466
1467 // Step 1: Solve saddle-point equations for mu*/T,
1468 // then compute GCE densities and two-particle correlations at the saddle point.
1469 SolveSaddlePointEquations();
1470
1471 m_modelgce->CalculateTwoParticleCorrelations();
1472
1473 // Set up charge indices (B, Q, S, C) that are treated canonically.
1474 // We do NOT call BuildSusceptibilityMatrix() here because that builds
1475 // Sigma from ideal-gas chi2. Instead, we build Sigma directly from
1476 // m_modelgce->PrimCorrel() below, which is correct for both ideal and
1477 // non-ideal equations of state.
1478 SetupSaddlePointChargeIndices();
1479
1480 int Nparts = m_TPS->ComponentsNumber();
1481 double Vc = m_Parameters.SVc;
1482 double T = m_Parameters.T;
1483
1484 // Step 2: Read GCE densities, thermodynamic quantities,
1485 // and susceptibilities from the GCE model.
1486 const auto& modelDensities = m_modelgce->Densities();
1487 const auto& pc = m_modelgce->PrimCorrel();
1488
1489 double epsGCE = m_modelgce->CalculateEnergyDensity();
1490 double pGCE = m_modelgce->CalculatePressure();
1491
1492 // W1[j] = <N_j>_GCE = density_j * Vc (canonical species only)
1493 // W2[j] = var_GCE(N_j) = chi^GCE_{jj} = PrimCorrel[j][j] * Vc * T
1494 std::vector<double> W1(Nparts, 0.0);
1495 std::vector<double> W2(Nparts, 0.0);
1496 for (int j = 0; j < Nparts; ++j) {
1497 if (!IsParticleCanonical(m_TPS->Particle(j)))
1498 continue;
1499 W1[j] = modelDensities[j] * Vc;
1500 W2[j] = pc[j][j] * Vc * T;
1501 }
1502
1503 // Build the charge susceptibility matrix over a given set of charge axes
1504 // (indices into {B=0,Q=1,S=2,C=3}):
1505 // Sigma_{ab} = sum_{j,k} q_{a,j} q_{b,k} chi^GCE_{jk}
1506 // For ideal gas (diagonal PrimCorrel), this reduces to sum_j q_aj q_bj W2[j].
1507 // For interacting models, off-diagonal chi^GCE_{jk} are included.
1508 auto buildSigma = [&](const std::vector<int>& idx) -> Eigen::MatrixXd {
1509 int dd = static_cast<int>(idx.size());
1510 Eigen::MatrixXd S = Eigen::MatrixXd::Zero(dd, dd);
1511 for (int j = 0; j < Nparts; ++j) {
1512 for (int k = 0; k < Nparts; ++k) {
1513 if (pc[j][k] == 0.0) continue;
1514 double chi_jk = pc[j][k] * Vc * T;
1515 for (int ia = 0; ia < dd; ++ia) {
1516 double q_aj = m_TPS->Particle(j).GetCharge(idx[ia]);
1517 if (q_aj == 0.0) continue;
1518 for (int ib = 0; ib < dd; ++ib)
1519 S(ia, ib) += q_aj * m_TPS->Particle(k).GetCharge(idx[ib]) * chi_jk;
1520 }
1521 }
1522 }
1523 return S;
1524 };
1525
1526 // Drop linearly dependent (redundant) charge axes so that Sigma is full
1527 // rank before it is inverted. Example: a proton-only gas with canonical B
1528 // and Q has q_B = q_Q for every species, so Sigma is rank 1 and the second
1529 // axis must be removed. Greedily keep an axis only if it strictly raises
1530 // the rank (keeps Sigma positive definite). The surviving count is then
1531 // d = rank(Sigma), the effective dimensionality used in the NLO pressure.
1532 {
1533 std::vector<int> cand = m_SaddlePointChargeIndices;
1534 std::vector<int> kept;
1535 for (size_t c = 0; c < cand.size(); ++c) {
1536 std::vector<int> trial = kept;
1537 trial.push_back(cand[c]);
1538 Eigen::MatrixXd St = buildSigma(trial);
1539 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(St, Eigen::EigenvaluesOnly);
1540 double maxEv = es.eigenvalues()(trial.size() - 1);
1541 double minEv = es.eigenvalues()(0);
1542 if (maxEv > 0.0 && minEv > 1.e-10 * maxEv)
1543 kept = trial;
1544 else
1545 std::cerr << "**WARNING** " << m_TAG
1546 << ": dropping linearly dependent conserved-charge axis "
1547 << cand[c] << " (B=0,Q=1,S=2,C=3) from the saddle-point basis." << std::endl;
1548 }
1550 m_SaddlePointDim = static_cast<int>(kept.size());
1551 }
1552
1553 int d = m_SaddlePointDim;
1554 Eigen::MatrixXd Sigma = buildSigma(m_SaddlePointChargeIndices);
1555
1556 // Compute log(det(Sigma)) for entropy
1557 double logDetSigma = 0.0;
1558 if (d > 0) {
1559 Eigen::LLT<Eigen::MatrixXd> llt(Sigma);
1560 if (llt.info() == Eigen::Success) {
1561 for (int i = 0; i < d; ++i)
1562 logDetSigma += 2.0 * log(llt.matrixL()(i, i));
1563 } else {
1564 double det = Sigma.determinant();
1565 logDetSigma = (det > 0.0) ? log(det) : -1.e100;
1566 }
1567 }
1568 m_SaddlePointLogDetSigma = logDetSigma;
1569
1570 // Compute m_MultExp = ln Z_SP for entropy calculation.
1571 // ln Z_SP = Vc * p_GCE / T - sum_a mu*_a N_a / T - d/2 ln(2pi) - 1/2 ln(det Sigma)
1572 {
1573 double term1 = pGCE * Vc / T;
1574 double term2 = 0.0;
1575 for (int ia = 0; ia < d; ++ia) {
1576 int a = m_SaddlePointChargeIndices[ia];
1577 double Na = (a == 0) ? m_Parameters.B : (a == 1) ? m_Parameters.Q : (a == 2) ? m_Parameters.S : m_Parameters.C;
1578 term2 -= m_SaddlePointMu[a] * Na;
1579 }
1580 double term3 = -0.5 * d * log(2.0 * xMath::Pi()) - 0.5 * logDetSigma;
1581 m_MultExp = term1 + term2 + term3;
1582 }
1583 m_MultExpBanalyt = 0.0;
1584
1585 // Set reference partition function to 1 so that log(m_PartialZ[0]) = 0
1586 // in CalculateEntropyDensity(), which uses m_MultExp + log(m_PartialZ[0]).
1587 m_PartialZ[m_QNMap[QuantumNumbers(0, 0, 0, 0)]] = 1.0;
1588
1589 // Step 2b (NLO only): Compute higher cluster moments W3, W4 and
1590 // energy-weighted cluster sums Ej from cluster integrals.
1591 // These are specific to the ideal-gas NLO expansion and cannot be
1592 // read from m_modelgce.
1593 std::vector<double> W3(Nparts, 0.0);
1594 std::vector<double> W4(Nparts, 0.0);
1595 std::vector<double> Ej(Nparts, 0.0);
1596
1597 if (isNLO) {
1598 for (int j = 0; j < Nparts; ++j) {
1599 ThermalParticle& tpart = m_TPS->Particle(j);
1600
1601 if (!IsParticleCanonical(tpart))
1602 continue;
1603
1604 if (tpart.Statistics() == 0
1605 || tpart.CalculationType() != IdealGasFunctions::ClusterExpansion)
1606 {
1607 // Boltzmann: all W^(k) equal
1608 double omega = W2[j];
1609 W3[j] = omega;
1610 W4[j] = omega;
1611 Ej[j] = tpart.DensityCluster(1, m_Parameters, IdealGasFunctions::EnergyDensity, m_UseWidth, m_SaddlePointMuStar[j]);
1612 }
1613 else {
1614 // Quantum statistics with cluster expansion
1615 for (int n = 1; n <= tpart.ClusterExpansionOrder(); ++n) {
1616 double dens_n = tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::ParticleDensity, m_UseWidth, m_SaddlePointMuStar[j]);
1617 double omega_n = dens_n * Vc / static_cast<double>(n);
1618 W3[j] += n * n * n * omega_n;
1619 W4[j] += n * n * n * n * omega_n;
1620
1621 double edens_n = tpart.DensityCluster(n, m_Parameters, IdealGasFunctions::EnergyDensity, m_UseWidth, m_SaddlePointMuStar[j]);
1622 Ej[j] += n * n * edens_n;
1623 }
1624 }
1625 }
1626 }
1627
1628 // Step 3: Precompute Sigma^{-1}, P, Gjj, computeG.
1629 //
1630 // P_{j,a} = sum_b Sigma^{-1}_{ab} q_{b,j}
1631 // G_{jk} = q_j^T Sigma^{-1} q_k = sum_a q_{a,j} P_{k,a}
1632 //
1633 // These are used by:
1634 // - Ideal-gas fast-path covariance: cov = delta_lm W2[l] - W2[l] W2[m] G_lm
1635 // - NLO corrections to thermodynamics, means, and covariances
1636 // For non-ideal LO, they are not needed (the general covariance path
1637 // uses the full chi^GCE matrix and Sigma^{-1} directly).
1638 bool isIdealGas = (m_InteractionModel == Ideal);
1639
1640 Eigen::MatrixXd SigmaInv = Sigma.inverse();
1641
1642 std::vector<double> P(Nparts * d, 0.0);
1643 std::vector<double> Gjj(Nparts, 0.0);
1644
1645 if (isIdealGas) {
1646 for (int j = 0; j < Nparts; ++j) {
1647 for (int ia = 0; ia < d; ++ia) {
1648 double val = 0.0;
1649 for (int ib = 0; ib < d; ++ib) {
1650 int b = m_SaddlePointChargeIndices[ib];
1651 val += SigmaInv(ia, ib) * m_TPS->Particle(j).GetCharge(b);
1652 }
1653 P[j * d + ia] = val;
1654 }
1655 }
1656
1657 for (int j = 0; j < Nparts; ++j) {
1658 double g = 0.0;
1659 for (int ia = 0; ia < d; ++ia) {
1660 int a = m_SaddlePointChargeIndices[ia];
1661 g += m_TPS->Particle(j).GetCharge(a) * P[j * d + ia];
1662 }
1663 Gjj[j] = g;
1664 }
1665 }
1666
1667 // Helper lambda: compute G_jk from precomputed P
1668 auto computeG = [&](int j, int k) -> double {
1669 double g = 0.0;
1670 for (int ia = 0; ia < d; ++ia) {
1671 int a = m_SaddlePointChargeIndices[ia];
1672 g += m_TPS->Particle(j).GetCharge(a) * P[k * d + ia];
1673 }
1674 return g;
1675 };
1676
1677 // Step 3b: Compute thermodynamic quantities.
1678 //
1679 // NLO:
1680 // Energy: ε = ε^GCE(μ*) - (1/2) Σ_j E_j G_jj
1681 // Pressure: P = p^GCE(μ*) - d·T/(2Vc)
1682 // Entropy: s = ln Z_SP / Vc + ε/T (computed in CalculateEntropyDensity)
1683 // LO:
1684 // Energy: ε = ε^GCE(μ*)
1685 // Pressure: P = p^GCE(μ*)
1686 // Entropy: s = s^GCE(μ*) (pure GCE, consistent with the LO P and ε;
1687 // the ln det Sigma Gaussian term is an NLO effect and is dropped)
1688 if (isNLO) {
1689 double deltaEps = 0.0;
1690 for (int j = 0; j < Nparts; ++j) {
1691 if (Ej[j] == 0.0 || Gjj[j] == 0.0)
1692 continue;
1693 deltaEps += Ej[j] * Gjj[j];
1694 }
1695 m_NLOEnergyDensity = epsGCE - 0.5 * deltaEps;
1696 m_NLOPressure = pGCE - d * T / (2.0 * Vc);
1697 m_NLOEntropyDensity = 0.; // unused for NLO; entropy comes from ln Z_SP path
1698 }
1699 else {
1700 m_NLOEnergyDensity = epsGCE;
1701 m_NLOPressure = pGCE;
1702 m_NLOEntropyDensity = m_modelgce->CalculateEntropyDensity(); // s^GCE(μ*)
1703 }
1704
1705 // Step 4: Compute means (LO, or LO + NLO if isNLO)
1706 for (int l = 0; l < Nparts; ++l) {
1707 if (!IsParticleCanonical(m_TPS->Particle(l))) {
1708 m_densities[l] = modelDensities[l];
1709 continue;
1710 }
1711
1712 // LO mean
1713 double mean = W1[l];
1714
1715 if (isNLO) {
1716 // NLO correction: -1/2 * sum_j G_jj * W_j^(3) * D_jl
1717 double nloTerm1 = -0.5 * W3[l] * Gjj[l];
1718
1719 double sumGjjW3Gjl = 0.0;
1720 for (int j = 0; j < Nparts; ++j) {
1721 if (W3[j] == 0.0 || Gjj[j] == 0.0)
1722 continue;
1723 double G_jl = computeG(j, l);
1724 sumGjjW3Gjl += W3[j] * Gjj[j] * G_jl;
1725 }
1726 double nloTerm2 = 0.5 * W2[l] * sumGjjW3Gjl;
1727
1728 mean += nloTerm1 + nloTerm2;
1729 }
1730
1731 m_densities[l] = mean / Vc;
1732 }
1733
1734 if (!computeCovariance) {
1735 CleanModelGCE();
1736 return;
1737 }
1738
1739 // Step 5: Compute LO + NLO covariance matrix
1740 int NN = Nparts;
1741
1742 m_PrimCorrel.resize(NN);
1743 for (int i = 0; i < NN; ++i)
1744 m_PrimCorrel[i].resize(NN, 0.0);
1745 m_TotalCorrel = m_PrimCorrel;
1746
1747 // For ideal gas, non-canonical species have no canonical correction
1748 // (chi^GCE is diagonal, so v_l = 0 when particle l carries no canonical charges).
1749 // Their GCE susceptibility is the final answer.
1750 // For non-ideal models, the full SAM formula in the general covariance path
1751 // below handles all species (including non-canonical) correctly.
1752 if (isIdealGas) {
1753 for (int l = 0; l < NN; ++l) {
1754 if (!IsParticleCanonical(m_TPS->Particle(l)))
1755 m_PrimCorrel[l][l] = pc[l][l];
1756 }
1757 }
1758
1759 // =====================================================================
1760 // Step 5a: LO covariance
1761 //
1762 // General formula (SAM / Schur complement of Eq. (2.86)):
1763 // cov(N_l, N_m) = chi^GCE_{lm} - v_l^T Sigma_chi^{-1} v_m
1764 //
1765 // For the ideal gas, chi_{jk} = delta_{jk} W2[j], which simplifies to:
1766 // cov = delta_lm W2[l] - W2[l] W2[m] G_lm
1767 // where G_lm = q_l^T Sigma^{-1} q_m.
1768 // This avoids the O(N^2) chiGCE allocation and double sums.
1769 // =====================================================================
1770
1771 if (isIdealGas) {
1772 // ---- Ideal gas fast path ----
1773 // cov(l,m) = delta_lm * W2[l] - W2[l] * W2[m] * G_lm
1774 // NLO terms also only use W2, computeG, P -- no chiGCE needed.
1775 for (int l = 0; l < NN; ++l) {
1776 if (!IsParticleCanonical(m_TPS->Particle(l)))
1777 continue;
1778
1779 for (int m = l; m < NN; ++m) {
1780 if (!IsParticleCanonical(m_TPS->Particle(m)))
1781 continue;
1782
1783 double cov = -W2[l] * W2[m] * computeG(l, m);
1784 if (l == m)
1785 cov += W2[l];
1786
1787 if (isNLO) {
1788 // NLO: -1/2 B_lm + 1/2 C_lm (identical to general formula)
1789
1790 // --- C_lm ---
1791 std::vector<double> Ml(d * d, 0.0);
1792 std::vector<double> Mm(d * d, 0.0);
1793
1794 for (int j = 0; j < Nparts; ++j) {
1795 if (W3[j] == 0.0)
1796 continue;
1797
1798 double D_jl = (j == l ? 1.0 : 0.0) - W2[l] * computeG(j, l);
1799 double D_jm = (j == m ? 1.0 : 0.0) - W2[m] * computeG(j, m);
1800
1801 double w3Djl = W3[j] * D_jl;
1802 double w3Djm = W3[j] * D_jm;
1803
1804 for (int ic = 0; ic < d; ++ic) {
1805 double Pjc = P[j * d + ic];
1806 for (int ia = 0; ia < d; ++ia) {
1807 int a = m_SaddlePointChargeIndices[ia];
1808 double q_aj = m_TPS->Particle(j).GetCharge(a);
1809 Ml[ic * d + ia] += w3Djl * Pjc * q_aj;
1810 Mm[ic * d + ia] += w3Djm * Pjc * q_aj;
1811 }
1812 }
1813 }
1814
1815 double C_lm = 0.0;
1816 for (int ic = 0; ic < d; ++ic)
1817 for (int ia = 0; ia < d; ++ia)
1818 C_lm += Ml[ic * d + ia] * Mm[ia * d + ic];
1819
1820 // --- B_lm ---
1821 std::vector<double> S_blm(d, 0.0);
1822 for (int k = 0; k < Nparts; ++k) {
1823 if (W3[k] == 0.0)
1824 continue;
1825 double D_kl = (k == l ? 1.0 : 0.0) - W2[l] * computeG(k, l);
1826 double D_km = (k == m ? 1.0 : 0.0) - W2[m] * computeG(k, m);
1827 double w3DklDkm = W3[k] * D_kl * D_km;
1828 for (int ib = 0; ib < d; ++ib) {
1829 int b = m_SaddlePointChargeIndices[ib];
1830 S_blm[ib] += m_TPS->Particle(k).GetCharge(b) * w3DklDkm;
1831 }
1832 }
1833
1834 std::vector<double> SinvS(d, 0.0);
1835 for (int ia = 0; ia < d; ++ia) {
1836 for (int ib = 0; ib < d; ++ib)
1837 SinvS[ia] += SigmaInv(ia, ib) * S_blm[ib];
1838 }
1839
1840 double B_lm = 0.0;
1841 for (int j = 0; j < Nparts; ++j) {
1842 if (Gjj[j] == 0.0)
1843 continue;
1844
1845 double D_jl = (j == l ? 1.0 : 0.0) - W2[l] * computeG(j, l);
1846 double D_jm = (j == m ? 1.0 : 0.0) - W2[m] * computeG(j, m);
1847
1848 double term1 = W4[j] * D_jl * D_jm;
1849
1850 double d2alpha_j = 0.0;
1851 for (int ia = 0; ia < d; ++ia) {
1852 int a = m_SaddlePointChargeIndices[ia];
1853 d2alpha_j -= m_TPS->Particle(j).GetCharge(a) * SinvS[ia];
1854 }
1855 double term2 = W3[j] * d2alpha_j;
1856
1857 B_lm += Gjj[j] * (term1 + term2);
1858 }
1859
1860 cov += -0.5 * B_lm + 0.5 * C_lm;
1861 }
1862
1863 double susc = cov / (Vc * T);
1864 m_PrimCorrel[l][m] = susc;
1865 if (l != m)
1866 m_PrimCorrel[m][l] = susc;
1867 }
1868 }
1869 } else {
1870 // ---- Non-ideal (general) path ----
1871 // Use the full N x N GCE susceptibility matrix from m_modelgce.
1872 // cov(l,m) = chi^GCE_{lm} - v_l^T Sigma^{-1} v_m
1873
1874 // Precompute v[l][a] = sum_j q_{a,j} * chi^GCE_{jl}
1875 // and u[l][a] = sum_b SigmaInv(a,b) * v[l][b]
1876 //
1877 // For non-canonical species l (all canonical charges zero), v_l is NOT
1878 // necessarily zero in interacting models: chi^GCE_{jl} can have off-diagonal
1879 // terms from interactions, so sum_j q_{a,j} chi^GCE_{jl} != 0 in general.
1880 // Therefore we compute v_l and u_l for ALL species, not just canonical ones.
1881 std::vector<std::vector<double>> vl(Nparts, std::vector<double>(d, 0.0));
1882 std::vector<std::vector<double>> ul(Nparts, std::vector<double>(d, 0.0));
1883 for (int l = 0; l < Nparts; ++l) {
1884 for (int j = 0; j < Nparts; ++j) {
1885 if (pc[j][l] == 0.0)
1886 continue;
1887 double chi_jl = pc[j][l] * Vc * T;
1888 for (int ia = 0; ia < d; ++ia) {
1889 int a = m_SaddlePointChargeIndices[ia];
1890 vl[l][ia] += m_TPS->Particle(j).GetCharge(a) * chi_jl;
1891 }
1892 }
1893 for (int ia = 0; ia < d; ++ia) {
1894 for (int ib = 0; ib < d; ++ib)
1895 ul[l][ia] += SigmaInv(ia, ib) * vl[l][ib];
1896 }
1897 }
1898
1899 // LO covariance for ALL species pairs (canonical and non-canonical).
1900 // The SAM formula cov(l,m) = chi^GCE_{lm} - v_l^T Sigma^{-1} v_m
1901 // applies to all species: interactions can induce canonical corrections
1902 // even for non-canonical species through off-diagonal chi^GCE_{jl}.
1903 for (int l = 0; l < NN; ++l) {
1904 for (int m = l; m < NN; ++m) {
1905 double cov = pc[l][m] * Vc * T;
1906 for (int ia = 0; ia < d; ++ia)
1907 cov -= vl[l][ia] * ul[m][ia];
1908
1909 double susc = cov / (Vc * T);
1910 m_PrimCorrel[l][m] = susc;
1911 if (l != m)
1912 m_PrimCorrel[m][l] = susc;
1913 }
1914 }
1915 }
1916
1917 // Set scaled variances
1918 for (int i = 0; i < NN; ++i) {
1919 m_wprim[i] = m_PrimCorrel[i][i];
1920 if (m_densities[i] > 0.)
1921 m_wprim[i] *= T / m_densities[i];
1922 else
1923 m_wprim[i] = 1.;
1924 if (m_wprim[i] != m_wprim[i])
1925 m_wprim[i] = 1.;
1926 }
1927
1928 m_TwoParticleCorrelationsCalculated = true;
1929
1930 CleanModelGCE();
1931 }
1932
1933} // namespace thermalfist
map< string, double > params
virtual void CalculateSusceptibilityMatrix()
Calculates the conserved charges susceptibility matrix.
virtual void FixParametersNoReset()
Method which actually implements ConstrainChemicalPotentialsNoReset() (for backward compatibility).
void SetSoverB(double SB)
The entropy per baryon ratio to be used to constrain the baryon chemical potential.
virtual void ChangeTPS(ThermalParticleSystem *TPS)
Change the particle list.
virtual void CalculateParticleChargeCorrelationMatrix()
Calculates the matrix of correlators between primordial (and also final) particle numbers and conserv...
bool UsePartialChemicalEquilibrium()
Whether partial chemical equilibrium with additional chemical potentials is used.
void SetQoverB(double QB)
The electric-to-baryon charge ratio to be used to constrain the electric chemical potential.
virtual bool SolveChemicalPotentials(double totB=0., double totQ=0., double totS=0., double totC=0., double muBinit=0., double muQinit=0., double muSinit=0., double muCinit=0., bool ConstrMuB=true, bool ConstrMuQ=true, bool ConstrMuS=true, bool ConstrMuC=true)
The procedure which calculates the chemical potentials which reproduce the specified total baryon,...
virtual void FillChemicalPotentials()
Sets the chemical potentials of all particles.
virtual void FixParameters()
Method which actually implements ConstrainChemicalPotentials() (for backward compatibility).
void SetUseWidth(bool useWidth)
Sets whether finite resonance widths are used. Deprecated.
virtual void SetParameters(const ThermalModelParameters &params)
The thermal parameters.
virtual void SetElectricChemicalPotential(double muQ)
Set the electric chemical potential.
virtual void ValidateCalculation()
Checks whether issues have occured during the calculation of particle densities in the CalculateDensi...
virtual void CalculateProxySusceptibilityMatrix()
Calculates the susceptibility matrix of conserved charges proxies.
ThermalModelBase(ThermalParticleSystem *TPS, const ThermalModelParameters &params=ThermalModelParameters())
Construct a new ThermalModelBase object.
virtual void SetStrangenessChemicalPotential(double muS)
Set the strangeness chemical potential.
virtual void CalculateDensities()
Calculates the primordial and total (after decays) densities of all species.
const ThermalModelParameters & Parameters() const
virtual void CalculateTwoParticleFluctuationsDecays()
Computes particle number correlations and fluctuations for all final-state particles which are marked...
std::map< QuantumNumbers, int > m_QNMap
Maps QuantumNumbers combinations to a 1-dimensional index.
double m_MultExpBanalyt
Exponential multiplier for analytical baryon fugacity calculations.
int m_IntegrationIterationsMultiplier
A multiplier to increase the number of iterations during the numerical integration used to calculate ...
int m_SaddlePointDim
Saddle-point approximation data (for SaddlePoint quadrature method)
std::vector< double > m_PartialZ
The computed canonical partition function.
int m_QCE
Flag indicating if electric charge is conserved canonically.
ThermalModelBase * m_modelgce_ext
External GCE model (not owned), set via SetModelGCE()
void SolveChemicalPotentialsGCE(bool resetInitialValues)
void ChangeTPS(ThermalParticleSystem *TPS)
Change the particle list.
virtual void CalculateTwoParticleCorrelations()
Computes the fluctuations and correlations of the primordial particle numbers.
double m_NLOEnergyDensity
ε = ε^GCE(μ*) - (1/2) Σ_j E_j G_jj (LO: ε^GCE(μ*))
int m_CCE
Flag indicating if charm is conserved canonically.
virtual void CalculateFluctuations()
Computes the fluctuation observables.
double m_NLOPressure
P = p^GCE(μ*) - d·T/(2·Vc) (LO: p^GCE(μ*))
double m_MultExp
Exponential multiplier for canonical partition function calculations.
virtual double ParticleScaledVariance(int part)
void SetModelGCE(ThermalModelBase *model)
Sets an external GCE model to use in the saddle-point approach.
std::vector< QuantumNumbers > m_QNvec
A set of QuantumNumbers combinations for which it is necessary to compute the canonical partition fun...
std::vector< double > m_Corr
A vector of chemical factors.
virtual double GetGCEDensity(int i) const
Density of particle species i in the grand-canonical ensemble.
virtual bool IsParticleCanonical(const ThermalParticle &part)
Determines whether the specified ThermalParticle is treat canonically or grand-canonically in the pre...
virtual void CalculatePartitionFunctions(double Vc=-1.)
Calculates all necessary canonical partition functions.
double m_NLOEntropyDensity
LO entropy: s^GCE(μ*) (used only by SaddlePointLO; NLO uses lnZ_SP)
virtual ~ThermalModelCanonical(void)
Destroy the ThermalModelCanonical object.
virtual void CalculateQuantumNumbersRange(bool computeFluctuations=false)
Calculates the range of quantum numbers values for which it is necessary to compute the canonical par...
virtual void FixParametersNoReset()
Method which actually implements ConstrainChemicalPotentialsNoReset() (for backward compatibility).
std::vector< int > m_SaddlePointChargeIndices
Which of {B=0,Q=1,S=2,C=3} are canonical.
int m_BCE
Flag indicating if baryon charge is conserved canonically.
virtual void ValidateCalculation()
Checks whether issues have occured during the calculation of particle densities in the CalculateDensi...
virtual bool IsConservedChargeCanonical(ConservedCharge::Name charge) const
bool m_Banalyt
Flag indicating whether the analytical calculation of baryon fugacity is used.
ThermalModelBase * m_modelgce_default
Default ideal-gas GCE model (owned, created once)
int m_SCE
Flag indicating if strangeness is conserved canonically.
std::vector< double > m_SaddlePointMu
Saddle-point mu*_a/T for each conserved charge (size 4)
CanonicalMethod m_Method
Method for computing the canonical partition functions.
virtual void FixParameters()
Method which actually implements ConstrainChemicalPotentials() (for backward compatibility).
virtual void CalculatePrimordialDensities()
Calculates the primordial densities of all species.
ThermalModelCanonical(ThermalParticleSystem *TPS, const ThermalModelParameters &params=ThermalModelParameters())
Construct a new ThermalModelCanonical object.
std::vector< double > m_SaddlePointMuStar
Per-particle full chemical potential at saddle point.
Class implementing the Ideal HRG model.
Class containing all information about a particle specie.
int BaryonCharge() const
Particle's baryon number.
int Statistics() const
Particle's statistics.
int Strangeness() const
Particle's strangeness.
void SetCalculationType(IdealGasFunctions::QStatsCalculationType type)
Sets the CalculationType() method to evaluate quantum statistics.
double Density(const ThermalModelParameters &params, IdealGasFunctions::Quantity type=IdealGasFunctions::ParticleDensity, bool useWidth=0, double mu=0.) const
Computes a specified ideal gas thermodynamic function.
IdealGasFunctions::QStatsCalculationType CalculationType() const
Method to evaluate quantum statistics.
int ElectricCharge() const
Particle's electric charge.
int Charm() const
Particle's charm.
int ClusterExpansionOrder() const
Number of terms in the cluster expansion method.
double DensityCluster(int n, const ThermalModelParameters &params, IdealGasFunctions::Quantity type=IdealGasFunctions::ParticleDensity, bool useWidth=0, double mu=0.) const
double ScaledVariance(const ThermalModelParameters &params, bool useWidth=0, double mu=0.) const
Computes the scaled variance of particle number fluctuations in the ideal gas. Computes the scaled va...
Class containing the particle list.
void GetCoefsIntegrateLegendre10(double a, double b, std::vector< double > *x, std::vector< double > *w)
constexpr double Pi()
Pi constant.
Definition xMath.h:23
double BesselIexp(int n, double x)
Modified Bessel function I_n(x), divided by exponential factor.
Definition xMath.cpp:791
The main namespace where all classes and functions of the Thermal-FIST library reside.
Definition CosmicEoS.h:9
@ SaddlePointLO
Saddle-point LO only (GCE densities/thermo, canonical LO fluctuations)
@ SaddlePoint
Saddle-point approximation (per-QN solve)
@ SaddlePointNLO
Saddle-point with systematic NLO expansion (preserves exact conservation laws)
@ GaussLegendre
Composite 10-point Gauss-Legendre on subintervals (default)
Name
Set of all conserved charges considered.
@ ElectricCharge
Electric charge.
Struct containing a set of quantum numbers: Baryon number, electric charge, strangeness,...
Structure containing all thermal parameters of the model.
Contains some extra mathematical functions used in the code.