Skip to content

Level set tools

level_set_tools

This module provides a set of classes and functions enabling multi-material capabilities. Users initialise materials by instantiating the Material class and define the physical properties of material interfaces using field_interface. They instantiate the LevelSetSolver class by providing relevant parameters and call the solve method to request a solver update. Finally, they may call the entrainment function to calculate material entrainment in the simulation.

Material(*, density=None, B=None, RaB=None) dataclass

A material with physical properties for the level-set approach.

Expects material buoyancy to be defined using a value for either the reference density, buoyancy number, or compositional Rayleigh number.

Contains static methods to calculate the physical properties of a material. Methods implemented here describe properties in the simplest non-dimensional simulation setup and must be overriden for more complex scenarios.

Attributes:

Name Type Description
density Optional[Number]

An integer or a float representing the reference density.

B Optional[Number]

An integer or a float representing the buoyancy number.

RaB Optional[Number]

An integer or a float representing the compositional Rayleigh number.

density_B_RaB Optional[Number]

A string to notify how the buoyancy term is calculated.

__post_init__()

Checks instance field values.

Raises:

Type Description
ValueError

Incorrect field types.

Source code in g-adopt/gadopt/level_set_tools.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __post_init__(self):
    """Checks instance field values.

    Raises:
        ValueError:
          Incorrect field types.
    """
    count_None = 0
    for field_var in fields(self):
        field_var_value = getattr(self, field_var.name)
        if isinstance(field_var_value, Number):
            self.density_B_RaB = field_var.name
        elif field_var_value is None:
            count_None += 1
        else:
            raise ValueError(
                "When provided, density, B, and RaB must have type int or float."
            )
    if count_None != 2:
        raise ValueError(
            "One, and only one, of density, B, and RaB must be provided, and it "
            "must be an integer or a float."
        )

viscosity(*args, **kwargs) staticmethod

Calculates dynamic viscosity (Pa s).

Source code in g-adopt/gadopt/level_set_tools.py
 98
 99
100
101
@staticmethod
def viscosity(*args, **kwargs):
    """Calculates dynamic viscosity (Pa s)."""
    return 1.0

thermal_expansion(*args, **kwargs) staticmethod

Calculates volumetric thermal expansion coefficient (K^-1).

Source code in g-adopt/gadopt/level_set_tools.py
103
104
105
106
@staticmethod
def thermal_expansion(*args, **kwargs):
    """Calculates volumetric thermal expansion coefficient (K^-1)."""
    return 1.0

thermal_conductivity(*args, **kwargs) staticmethod

Calculates thermal conductivity (W m^-1 K^-1).

Source code in g-adopt/gadopt/level_set_tools.py
108
109
110
111
@staticmethod
def thermal_conductivity(*args, **kwargs):
    """Calculates thermal conductivity (W m^-1 K^-1)."""
    return 1.0

specific_heat_capacity(*args, **kwargs) staticmethod

Calculates specific heat capacity at constant pressure (J kg^-1 K^-1).

Source code in g-adopt/gadopt/level_set_tools.py
113
114
115
116
@staticmethod
def specific_heat_capacity(*args, **kwargs):
    """Calculates specific heat capacity at constant pressure (J kg^-1 K^-1)."""
    return 1.0

internal_heating_rate(*args, **kwargs) staticmethod

Calculates internal heating rate per unit mass (W kg^-1).

Source code in g-adopt/gadopt/level_set_tools.py
118
119
120
121
@staticmethod
def internal_heating_rate(*args, **kwargs):
    """Calculates internal heating rate per unit mass (W kg^-1)."""
    return 0.0

thermal_diffusivity(*args, **kwargs) classmethod

Calculates thermal diffusivity (m^2 s^-1).

Source code in g-adopt/gadopt/level_set_tools.py
123
124
125
126
@classmethod
def thermal_diffusivity(cls, *args, **kwargs):
    """Calculates thermal diffusivity (m^2 s^-1)."""
    return cls.thermal_conductivity() / cls.density() / cls.specific_heat_capacity()

LevelSetSolver(level_set, velocity, tstep, tstep_alg, subcycles, epsilon, reini_params=None, solver_params=None)

Solver for the conservative level-set approach.

Solves the advection and reinitialisation equations for a level set function.

Attributes:

Name Type Description
mesh

The UFL mesh where values of the level set function exist.

level_set

The Firedrake function for the level set.

func_space_lsgp

The UFL function space where values of the projected level-set gradient are calculated.

level_set_grad_proj

The Firedrake function for the projected level-set gradient.

proj_solver

An integer or a float representing the reference density.

reini_params

A dictionary containing parameters used in the reinitialisation approach.

ls_solver

The G-ADOPT timestepper object for the advection equation.

reini_ts

The G-ADOPT timestepper object for the reinitialisation equation.

subcycles

An integer specifying the number of advection solves to perform.

Initialises the solver instance.

Parameters:

Name Type Description Default
level_set Function

The Firedrake function for the level set.

required
velocity ListTensor

The UFL expression for the velocity.

required
tstep Constant

The Firedrake function over the Real space for the simulation time step.

required
tstep_alg ABCMeta

The class for the timestepping algorithm used in the advection solver.

required
subcycles int

An integer specifying the number of advection solves to perform.

required
epsilon Constant

A UFL constant denoting the thickness of the hyperbolic tangent profile.

required
reini_params Optional[dict]

A dictionary containing parameters used in the reinitialisation approach.

None
solver_params Optional[dict]

A dictionary containing solver parameters used in the advection and reinitialisation approaches.

None
Source code in g-adopt/gadopt/level_set_tools.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def __init__(
    self,
    level_set: fd.Function,
    velocity: fd.ufl.tensors.ListTensor,
    tstep: fd.Constant,
    tstep_alg: abc.ABCMeta,
    subcycles: int,
    epsilon: fd.Constant,
    reini_params: Optional[dict] = None,
    solver_params: Optional[dict] = None,
):
    """Initialises the solver instance.

    Args:
        level_set:
          The Firedrake function for the level set.
        velocity:
          The UFL expression for the velocity.
        tstep:
          The Firedrake function over the Real space for the simulation time step.
        tstep_alg:
          The class for the timestepping algorithm used in the advection solver.
        subcycles:
          An integer specifying the number of advection solves to perform.
        epsilon:
          A UFL constant denoting the thickness of the hyperbolic tangent profile.
        reini_params:
          A dictionary containing parameters used in the reinitialisation approach.
        solver_params:
          A dictionary containing solver parameters used in the advection and
          reinitialisation approaches.
    """
    self.level_set = level_set
    self.u = velocity
    self.tstep = tstep
    self.tstep_alg = tstep_alg
    self.subcycles = subcycles

    self.reini_params = reini_params or reini_params_default
    self.solver_params = solver_params or solver_params_default

    self.mesh = extract_unique_domain(level_set)
    self.func_space = level_set.function_space()

    self.func_space_lsgp = fd.VectorFunctionSpace(
        self.mesh, "CG", self.func_space.finat_element.degree
    )
    self.level_set_grad_proj = fd.Function(
        self.func_space_lsgp,
        name=f"Level-set gradient (projection) #{level_set.name()[-1]}",
    )

    self.proj_solver = self.gradient_L2_proj()

    self.ls_eq_attrs = {"u": velocity}
    self.reini_eq_attrs = {
        "level_set_grad": self.level_set_grad_proj,
        "epsilon": epsilon,
    }

    self.solvers_ready = False

gradient_L2_proj()

Constructs a projection solver.

Projects the level-set gradient from a discontinuous function space to the equivalent continuous one.

Returns:

Type Description
LinearVariationalSolver

A Firedrake solver capable of projecting a discontinuous gradient field on

LinearVariationalSolver

a continuous function space.

Source code in g-adopt/gadopt/level_set_tools.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def gradient_L2_proj(self) -> fd.variational_solver.LinearVariationalSolver:
    """Constructs a projection solver.

    Projects the level-set gradient from a discontinuous function space to the
    equivalent continuous one.

    Returns:
        A Firedrake solver capable of projecting a discontinuous gradient field on
        a continuous function space.
    """
    test_function = fd.TestFunction(self.func_space_lsgp)
    trial_function = fd.TrialFunction(self.func_space_lsgp)

    mass_term = fd.inner(test_function, trial_function) * fd.dx(domain=self.mesh)
    residual_element = (
        self.level_set * fd.div(test_function) * fd.dx(domain=self.mesh)
    )
    residual_boundary = (
        self.level_set
        * fd.dot(test_function, fd.FacetNormal(self.mesh))
        * fd.ds(domain=self.mesh)
    )
    residual = -residual_element + residual_boundary
    problem = fd.LinearVariationalProblem(
        mass_term, residual, self.level_set_grad_proj
    )

    solver_params = {
        "mat_type": "aij",
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver_type": "mumps",
    }

    return fd.LinearVariationalSolver(problem, solver_parameters=solver_params)

update_level_set_gradient(*args, **kwargs)

Calls the gradient projection solver.

Can be used as a callback.

Source code in g-adopt/gadopt/level_set_tools.py
277
278
279
280
281
282
def update_level_set_gradient(self, *args, **kwargs):
    """Calls the gradient projection solver.

    Can be used as a callback.
    """
    self.proj_solver.solve()

set_up_solvers()

Sets up the time steppers for advection and reinitialisation.

Source code in g-adopt/gadopt/level_set_tools.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def set_up_solvers(self):
    """Sets up the time steppers for advection and reinitialisation."""
    test = fd.TestFunction(self.func_space)

    self.ls_solver = GenericTransportSolver(
        "advection",
        self.level_set,
        self.tstep / self.subcycles,
        self.tstep_alg,
        eq_attrs={"u": self.u},
        solver_parameters=self.solver_params,
    )

    reinitialisation_equation = Equation(
        test,
        self.func_space,
        reinitialisation_term,
        mass_term=scalar_eq.mass_term,
        eq_attrs=self.reini_eq_attrs,
    )
    self.reini_ts = self.reini_params["tstep_alg"](
        reinitialisation_equation,
        self.level_set,
        self.reini_params["tstep"],
        solver_parameters=self.solver_params,
    )

solve(step)

Updates the level-set function.

Calls advection and reinitialisation solvers within a subcycling loop. The reinitialisation solver can be iterated and might not be called at each simulation time step.

Parameters:

Name Type Description Default
step int

An integer representing the current simulation step.

required
Source code in g-adopt/gadopt/level_set_tools.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def solve(self, step: int):
    """Updates the level-set function.

    Calls advection and reinitialisation solvers within a subcycling loop.
    The reinitialisation solver can be iterated and might not be called at each
    simulation time step.

    Args:
        step:
          An integer representing the current simulation step.
    """
    if not self.solvers_ready:
        self.set_up_solvers()
        self.solvers_ready = True

    for subcycle in range(self.subcycles):
        self.ls_solver.solve()

        if step >= self.reini_params["frequency"]:
            self.reini_ts.solution_old.assign(self.level_set)

        if step % self.reini_params["frequency"] == 0:
            for reini_step in range(self.reini_params["iterations"]):
                self.reini_ts.advance(
                    0, update_forcings=self.update_level_set_gradient
                )

            self.ls_solver.solution_old.assign(self.level_set)

reinitialisation_term(eq, trial)

Term for the conservative level set reinitialisation equation.

Implements terms on the right-hand side of Equation 17 from Parameswaran, S., & Mandal, J. C. (2023). A stable interface-preserving reinitialization equation for conservative level set method. European Journal of Mechanics-B/Fluids, 98, 40-63.

Source code in g-adopt/gadopt/level_set_tools.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def reinitialisation_term(
    eq: Equation, trial: fd.Argument | fd.ufl.indexed.Indexed | fd.Function
) -> fd.Form:
    """Term for the conservative level set reinitialisation equation.

    Implements terms on the right-hand side of Equation 17 from
    Parameswaran, S., & Mandal, J. C. (2023).
    A stable interface-preserving reinitialization equation for conservative level set
    method.
    European Journal of Mechanics-B/Fluids, 98, 40-63.
    """
    sharpen_term = -trial * (1 - trial) * (1 - 2 * trial) * eq.test * eq.dx
    balance_term = (
        eq.epsilon
        * (1 - 2 * trial)
        * fd.sqrt(fd.inner(eq.level_set_grad, eq.level_set_grad))
        * eq.test
        * eq.dx
    )

    return sharpen_term + balance_term

field_interface_recursive(level_set, material_value, method)

Sets physical property expressions for each material.

Ensures that the correct expression is assigned to each material based on the level-set functions. Property transition across material interfaces are expressed according to the provided method.

Parameters:

Name Type Description Default
level_set list

A list of level-set UFL functions.

required
material_value list

A list of physical property values applicable to each material.

required
method str

A string specifying the nature of property transitions between materials.

required

Returns:

Type Description
Expr

A UFL expression to calculate physical property values throughout the domain.

Raises:

Type Description
ValueError

Incorrect method name supplied.

Source code in g-adopt/gadopt/level_set_tools.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
def field_interface_recursive(
    level_set: list, material_value: list, method: str
) -> fd.ufl.core.expr.Expr:
    """Sets physical property expressions for each material.

    Ensures that the correct expression is assigned to each material based on the
    level-set functions.
    Property transition across material interfaces are expressed according to the
    provided method.

    Args:
        level_set:
          A list of level-set UFL functions.
        material_value:
          A list of physical property values applicable to each material.
        method:
          A string specifying the nature of property transitions between materials.

    Returns:
        A UFL expression to calculate physical property values throughout the domain.

    Raises:
      ValueError: Incorrect method name supplied.

    """
    ls = fd.max_value(fd.min_value(level_set.pop(), 1), 0)

    if level_set:  # Directly specify material value on only one side of the interface
        match method:
            case "sharp":
                return fd.conditional(
                    ls > 0.5,
                    material_value.pop(),
                    field_interface_recursive(level_set, material_value, method),
                )
            case "arithmetic":
                return material_value.pop() * ls + field_interface_recursive(
                    level_set, material_value, method
                ) * (1 - ls)
            case "geometric":
                return material_value.pop() ** ls * field_interface_recursive(
                    level_set, material_value, method
                ) ** (1 - ls)
            case "harmonic":
                return 1 / (
                    ls / material_value.pop()
                    + (1 - ls)
                    / field_interface_recursive(level_set, material_value, method)
                )
            case _:
                raise ValueError(
                    "Method must be sharp, arithmetic, geometric, or harmonic."
                )
    else:  # Final level set; specify values for both sides of the interface
        match method:
            case "sharp":
                return fd.conditional(ls < 0.5, *material_value)
            case "arithmetic":
                return material_value[0] * (1 - ls) + material_value[1] * ls
            case "geometric":
                return material_value[0] ** (1 - ls) * material_value[1] ** ls
            case "harmonic":
                return 1 / ((1 - ls) / material_value[0] + ls / material_value[1])
            case _:
                raise ValueError(
                    "Method must be sharp, arithmetic, geometric, or harmonic."
                )

field_interface(level_set, material_value, method)

Executes field_interface_recursive with a modified argument.

Calls field_interface_recursive using a copy of the level-set list to ensure the original one is not consumed by the function call.

Parameters:

Name Type Description Default
level_set list

A list of level-set UFL functions.

required
material_value list

A list of physical property values applicable to each material.

required
method str

A string specifying the nature of property transitions between materials.

required

Returns:

Type Description
Expr

A UFL expression to calculate physical property values throughout the domain.

Source code in g-adopt/gadopt/level_set_tools.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def field_interface(
    level_set: list, material_value: list, method: str
) -> fd.ufl.core.expr.Expr:
    """Executes field_interface_recursive with a modified argument.

    Calls field_interface_recursive using a copy of the level-set list to ensure the
    original one is not consumed by the function call.

    Args:
        level_set:
          A list of level-set UFL functions.
        material_value:
          A list of physical property values applicable to each material.
        method:
          A string specifying the nature of property transitions between materials.

    Returns:
        A UFL expression to calculate physical property values throughout the domain.
    """
    return field_interface_recursive(level_set.copy(), material_value, method)

density_RaB(Simulation, level_set, func_space_interp, method='sharp')

Sets up buoyancy-related fields.

Assigns UFL expressions to buoyancy-related fields based on the way the Material class was initialised.

Parameters:

Name Type Description Default
Simulation

A class representing the current simulation.

required
level_set list

A list of level-set UFL functions.

required
func_space_interp WithGeometry

A continuous UFL function space where material fields are calculated.

required
method Optional[str]

An optional string specifying the nature of property transitions between materials.

'sharp'

Returns:

Type Description
Constant

A tuple containing the reference density field, the density difference field,

Constant | Expr

the density field, the UFL expression for the compositional Rayleigh number,

Function

the compositional Rayleigh number field, and a boolean indicating if the

Constant | Expr

simulation is expressed in dimensionless form.

Raises:

Type Description
ValueError

Inconsistent buoyancy-related field across materials.

Source code in g-adopt/gadopt/level_set_tools.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def density_RaB(
    Simulation,
    level_set: list,
    func_space_interp: fd.functionspaceimpl.WithGeometry,
    method: Optional[str] = "sharp",
) -> tuple[
    fd.Constant,
    fd.Constant | fd.ufl.core.expr.Expr,
    fd.Function,
    fd.Constant | fd.ufl.core.expr.Expr,
    fd.Function,
    bool,
]:
    """Sets up buoyancy-related fields.

    Assigns UFL expressions to buoyancy-related fields based on the way the Material
    class was initialised.

    Args:
        Simulation:
          A class representing the current simulation.
        level_set:
          A list of level-set UFL functions.
        func_space_interp:
          A continuous UFL function space where material fields are calculated.
        method:
          An optional string specifying the nature of property transitions between
          materials.

    Returns:
        A tuple containing the reference density field, the density difference field,
        the density field, the UFL expression for the compositional Rayleigh number,
        the compositional Rayleigh number field, and a boolean indicating if the
        simulation is expressed in dimensionless form.

    Raises:
        ValueError: Inconsistent buoyancy-related field across materials.
    """
    density = fd.Function(func_space_interp, name="Density")
    RaB = fd.Function(func_space_interp, name="RaB")
    # Identify if the governing equations are written in dimensional form or not and
    # define accordingly relevant variables for the buoyancy term
    if all(material.density_B_RaB == "density" for material in Simulation.materials):
        dimensionless = False
        RaB_ufl = fd.Constant(1)
        ref_dens = fd.Constant(Simulation.reference_material.density)
        dens_diff = field_interface(
            level_set,
            [material.density - ref_dens for material in Simulation.materials],
            method=method,
        )
        density.interpolate(dens_diff + ref_dens)
    else:
        dimensionless = True
        ref_dens = fd.Constant(1)
        dens_diff = fd.Constant(1)
        if all(material.density_B_RaB == "B" for material in Simulation.materials):
            RaB_ufl = field_interface(
                level_set,
                [Simulation.Ra * material.B for material in Simulation.materials],
                method=method,
            )
        elif all(material.density_B_RaB == "RaB" for material in Simulation.materials):
            RaB_ufl = field_interface(
                level_set,
                [material.RaB for material in Simulation.materials],
                method=method,
            )
        else:
            raise ValueError(
                "All materials must share a common buoyancy-defining parameter."
            )
        RaB.interpolate(RaB_ufl)

    return ref_dens, dens_diff, density, RaB_ufl, RaB, dimensionless

entrainment(level_set, material_area, entrainment_height)

Calculates entrainment diagnostic.

Determines the proportion of a material that is located above a given height.

Parameters:

Name Type Description Default
level_set Function

A level-set Firedrake function.

required
material_area Number

An integer or a float representing the total area occupied by a material.

required
entrainment_height Number

An integer or a float representing the height above which the entrainment diagnostic is determined.

required

Returns:

Type Description

A float corresponding to the calculated entrainment diagnostic.

Source code in g-adopt/gadopt/level_set_tools.py
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
def entrainment(
    level_set: fd.Function, material_area: Number, entrainment_height: Number
):
    """Calculates entrainment diagnostic.

    Determines the proportion of a material that is located above a given height.

    Args:
        level_set:
          A level-set Firedrake function.
        material_area:
          An integer or a float representing the total area occupied by a material.
        entrainment_height:
          An integer or a float representing the height above which the entrainment
          diagnostic is determined.

    Returns:
        A float corresponding to the calculated entrainment diagnostic.
    """
    mesh_coords = fd.SpatialCoordinate(level_set.function_space().mesh())
    target_region = mesh_coords[1] >= entrainment_height
    material_entrained = fd.conditional(level_set < 0.5, 1, 0)

    return (
        fd.assemble(fd.conditional(target_region, material_entrained, 0) * fd.dx)
        / material_area
    )