First example
Defining a model in WallGo requires a few different ingredients: a scalar potential, a list of the particles in the model together with their properties, and the matrix elements for interactions between these particles. The matrix elements are used to compute the collision integrals in the C++ part of WallGo. The collision integrals are then loaded into the Python part of WallGo.
Concretely, let’s consider a simple model of a real scalar field coupled to a Dirac fermion via a Yukawa coupling,
In this case the scalar field may undergo a phase transition, with the fermion field contributing to the friction for the bubble wall growth. This model has been used as a toy model for the study of bubble nucleation in cosmological phase transitions. [1]
The definition of the Model starts by inheriting from the WallGo.GenericModel class. This class holds the features of a model which enter directly in the Python side of WallGo. This includes the list of particles (WallGo.Particle objects) and a reference to a definition of the effective potential.
import pathlib
import numpy as np
# WallGo imports
import WallGo
from WallGo import Fields, GenericModel, Particle
class YukawaModel(GenericModel):
"""
The Yukawa model, inheriting from WallGo.GenericModel.
"""
def __init__(self) -> None:
"""
Initialize the Yukawa model.
"""
self.modelParameters: dict[str, float] = {}
# Initialize internal effective potential
self.effectivePotential = EffectivePotentialYukawa(self)
# Create a list of particles relevant for the Boltzmann equations
self.defineParticles()
# ~ GenericModel interface
@property
def fieldCount(self) -> int:
"""How many classical background fields"""
return 1
def getEffectivePotential(self) -> "EffectivePotentialYukawa":
return self.effectivePotential
# ~
def defineParticles(self) -> None:
"""
Define the out-of-equilibrium particles for the model.
"""
self.clearParticles()
# === left fermion ===
# Vacuum mass squared
def psiMsqVacuum(fields: Fields) -> Fields:
return (
self.modelParameters["mf"]
+ self.modelParameters["y"] * fields.getField(0)
) ** 2
# Field-derivative of the vacuum mass squared
def psiMsqDerivative(fields: Fields) -> Fields:
return (
2
* self.modelParameters["y"]
* (
self.modelParameters["mf"]
+ self.modelParameters["y"] * fields.getField(0)
)
)
psiL = Particle(
"psiL",
index=1,
msqVacuum=psiMsqVacuum,
msqDerivative=psiMsqDerivative,
statistics="Fermion",
totalDOFs=2,
)
psiR = Particle(
"psiR",
index=2,
msqVacuum=psiMsqVacuum,
msqDerivative=psiMsqDerivative,
statistics="Fermion",
totalDOFs=2,
)
self.addParticle(psiL)
self.addParticle(psiR)
The scalar potential is used both for determining the free energy of homogeneous phases and for the shape and width of the bubble wall. In principle the potentials determining these two phenomena are different, as the former is coarse grained all the way to infinite length scales, while the latter can only consistenly be coarse grained on length scales shorter than the bubble wall width. [2] Nervertheless, at high temperatures and to leading order in powers of the coupling, these two potentials agree.
At high temperatures, the leading order effective potential of our simple model is
where we have defined the effective tadpole coefficient and effective mass as
The implementation in WallGo is as follows: one defines a class, here called WallGo.EffectivePotentialYukawa which inherits from the base class WallGo.EffectivePotential. This definition must contain a member function called evaluate which evaluates the potential as a function of the scalar fields and temperature.
"""
Effective potential for the Yukawa model.
"""
def __init__(self, owningModel: YukawaModel) -> None:
"""
Initialize the EffectivePotentialYukawa.
"""
super().__init__()
assert owningModel is not None, "Invalid model passed to Veff"
self.owner = owningModel
self.modelParameters = self.owner.modelParameters
# ~ EffectivePotential interface
fieldCount = 1
"""How many classical background fields"""
effectivePotentialError = 1e-15
"""
Relative accuracy at which the potential can be computed. Here the potential is
polynomial so we can set it to the machine precision.
"""
# ~
def evaluate(self, fields: Fields, temperature: float) -> float | np.ndarray:
"""
Evaluate the effective potential.
"""
# getting the field from the list of fields (here just of length 1)
fields = WallGo.Fields(fields)
phi = fields.getField(0)
# the constant term
f0 = -np.pi**2 / 90 * (1 + 4 * 7 / 8) * temperature**4
# coefficients of the temperature and field dependent terms
y = self.modelParameters["y"]
mf = self.modelParameters["mf"]
sigmaEff = (
self.modelParameters["sigma"]
+ 1 / 24 * (self.modelParameters["gamma"] + 4 * y * mf) * temperature**2
)
msqEff = (
self.modelParameters["msq"]
+ 1 / 24 * (self.modelParameters["lam"] + 4 * y**2) * temperature**2
)
potentialTotal = (
f0
+ sigmaEff * phi
+ 1 / 2 * msqEff * phi**2
+ 1 / 6 * self.modelParameters["gamma"] * phi**3
+ 1 / 24 * self.modelParameters["lam"] * phi**4
)
return np.array(potentialTotal)
The initialisation of an WallGo.EffectivePotential object takes the model parameters and the number of background scalar fields as arguments and stores them for use in evaluating the potential. It is possible to override other member functions when defining WallGo.EffectivePotentialYukawa, such as the initialisation function, or to add additional member functions and variables, though we haven’t done so in this simple example.
Once these two classes have been defined, we can now run WallGo to compute the bubble wall speed.
def main() -> None:
manager = WallGo.WallGoManager()
# Change the amount of grid points in the spatial coordinates
# for faster computations
manager.config.configGrid.spatialGridSize = 20
# Increase the number of iterations in the wall solving to
# ensure convergence
manager.config.configEOM.maxIterations = 25
# Decrease error tolerance for phase tracing to ensure stability
manager.config.configThermodynamics.phaseTracerTol = 1e-8
pathtoCollisions = pathlib.Path(__file__).resolve().parent / pathlib.Path(
f"CollisionOutput_N11"
)
manager.setPathToCollisionData(pathtoCollisions)
model = YukawaModel()
manager.registerModel(model)
inputParameters = {
"sigma": 0.0,
"msq": 1.0,
"gamma": -1.2,
"lam": 0.10,
"y": 0.55,
"mf": 0.30,
}
model.modelParameters.update(inputParameters)
manager.setupThermodynamicsHydrodynamics(
WallGo.PhaseInfo(
temperature=8.0, # nucleation temperature
phaseLocation1=WallGo.Fields([0.4]),
phaseLocation2=WallGo.Fields([27.0]),
),
WallGo.VeffDerivativeSettings(
temperatureVariationScale=1.0,
fieldValueVariationScale=[100.0],
),
)
# ---- Solve wall speed in Local Thermal Equilibrium (LTE) approximation
vwLTE = manager.wallSpeedLTE()
print(f"LTE wall speed: {vwLTE:.6f}")
solverSettings = WallGo.WallSolverSettings(
bIncludeOffEquilibrium=False,
# meanFreePathScale is determined here by the annihilation channels,
# and scales inversely with y^4 or lam^2. This is why
# meanFreePathScale has to be so large.
meanFreePathScale=5000.0, # In units of 1/Tnucl
wallThicknessGuess=10.0, # In units of 1/Tnucl
)
results = manager.solveWall(solverSettings)
print(
f"Wall velocity without out-of-equilibrium contributions {results.wallVelocity:.6f}"
)