Skip to content

aragog.core

core

Core classes and functions

logger = logging.getLogger(__name__) module-attribute

BoundaryConditions(_parameters, _mesh) dataclass

Boundary conditions

Args: parameters: Parameters mesh: Mesh

apply_flux_boundary_conditions(state)

Applies the boundary conditions to the state.

Args: state: The state to apply the boundary conditions to

Source code in aragog/core.py
111
112
113
114
115
116
117
118
119
120
def apply_flux_boundary_conditions(self, state: State) -> None:
    """Applies the boundary conditions to the state.

    Args:
        state: The state to apply the boundary conditions to
    """
    self.apply_flux_inner_boundary_condition(state)
    self.apply_flux_outer_boundary_condition(state)
    logger.debug("temperature = %s", state.temperature_basic)
    logger.debug("heat_flux = %s", state.heat_flux)

apply_flux_inner_boundary_condition(state)

Applies the flux boundary condition to the state at the inner boundary.

Args: state: The state to apply the boundary conditions to

Equivalent to CORE_BC in C code. 1: Simple core cooling 2: Prescribed heat flux 3: Prescribed temperature

Source code in aragog/core.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def apply_flux_inner_boundary_condition(self, state: State) -> None:
    """Applies the flux boundary condition to the state at the inner boundary.

    Args:
        state: The state to apply the boundary conditions to

    Equivalent to CORE_BC in C code.
        1: Simple core cooling
        2: Prescribed heat flux
        3: Prescribed temperature
    """
    if self._settings.inner_boundary_condition == 1:
        self.core_cooling(state)
    elif self._settings.inner_boundary_condition == 2:
        state.heat_flux[0, :] = self._settings.inner_boundary_value
    elif self._settings.inner_boundary_condition == 3:
        pass
        # raise NotImplementedError
    else:
        msg: str = (
            f"inner_boundary_condition = {self._settings.inner_boundary_condition} is unknown"
        )
        raise ValueError(msg)

apply_flux_outer_boundary_condition(state)

Applies the flux boundary condition to the state at the outer boundary.

Args: state: The state to apply the boundary conditions to

Equivalent to SURFACE_BC in C code. 1: Grey-body atmosphere 2: Zahnle steam atmosphere 3: Couple to atmodeller 4: Prescribed heat flux 5: Prescribed temperature

Source code in aragog/core.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def apply_flux_outer_boundary_condition(self, state: State) -> None:
    """Applies the flux boundary condition to the state at the outer boundary.

    Args:
        state: The state to apply the boundary conditions to

    Equivalent to SURFACE_BC in C code.
        1: Grey-body atmosphere
        2: Zahnle steam atmosphere
        3: Couple to atmodeller
        4: Prescribed heat flux
        5: Prescribed temperature
    """
    if self._settings.outer_boundary_condition == 1:
        self.grey_body(state)
    elif self._settings.outer_boundary_condition == 2:
        raise NotImplementedError
    elif self._settings.outer_boundary_condition == 3:
        msg: str = "Requires coupling to atmodeller"
        logger.error(msg)
        raise NotImplementedError(msg)
    elif self._settings.outer_boundary_condition == 4:
        state.heat_flux[-1, :] = self._settings.outer_boundary_value
    elif self._settings.outer_boundary_condition == 5:
        pass
    else:
        msg: str = (
            f"outer_boundary_condition = {self._settings.outer_boundary_condition} is unknown"
        )
        raise ValueError(msg)

apply_temperature_boundary_conditions(temperature, temperature_basic, dTdr)

Conforms the temperature and dTdr at the basic nodes to temperature boundary conditions.

Args: temperature: Temperature at the staggered nodes temperature_basic: Temperature at the basic nodes dTdr: Temperature gradient at the basic nodes

Source code in aragog/core.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def apply_temperature_boundary_conditions(
    self, temperature: npt.NDArray, temperature_basic: npt.NDArray, dTdr: npt.NDArray
) -> None:
    """Conforms the temperature and dTdr at the basic nodes to temperature boundary conditions.

    Args:
        temperature: Temperature at the staggered nodes
        temperature_basic: Temperature at the basic nodes
        dTdr: Temperature gradient at the basic nodes
    """
    # Core-mantle boundary
    if self._settings.inner_boundary_condition == 3:
        temperature_basic[0, :] = self._settings.inner_boundary_value
        dTdr[0, :] = (
            2 * (temperature[0, :] - temperature_basic[0, :])
            / self._mesh.basic.delta_mesh[0]
            * self._mesh.dxidr[0]
        )
    # Surface
    if self._settings.outer_boundary_condition == 5:
        temperature_basic[-1, :] = self._settings.outer_boundary_value
        dTdr[-1, :] = (
            2 * (temperature_basic[-1, :] - temperature[-1, :])
            / self._mesh.basic.delta_mesh[-1]
            * self._mesh.dxidr[-1]
        )

apply_temperature_boundary_conditions_melt(melt_fraction, melt_fraction_basic, dphidr)

Conforms the melt fraction gradient dphidr at the basic nodes to temperature boundary conditions.

Args: melt_fraction: Melt fraction at the staggered nodes melt_fraction_basic: Melt fraction at the basic nodes dphidr: Melt fraction gradient at the basic nodes

Source code in aragog/core.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def apply_temperature_boundary_conditions_melt(
    self, melt_fraction: npt.NDArray, melt_fraction_basic: npt.NDArray, dphidr: npt.NDArray
) -> None:
    """Conforms the melt fraction gradient dphidr at the basic nodes
       to temperature boundary conditions.

    Args:
        melt_fraction: Melt fraction at the staggered nodes
        melt_fraction_basic: Melt fraction at the basic nodes
        dphidr: Melt fraction gradient at the basic nodes
    """
    # Core-mantle boundary
    if self._settings.inner_boundary_condition == 3:
        dphidr[0, :] = (
            2 * (melt_fraction[0, :] - melt_fraction_basic[0, :])
            / self._mesh.basic.delta_mesh[0]
            * self._mesh.dxidr[0]
        )
    # Surface
    if self._settings.outer_boundary_condition == 5:
        dphidr[-1, :] = (
            2 * (melt_fraction_basic[-1, :] - melt_fraction[-1, :])
            / self._mesh.basic.delta_mesh[-1]
            * self._mesh.dxidr[-1]
        )

core_cooling(state)

Applies a core cooling heat flux according to Eq. (37) of Bower et al., 2018

Args: state: The state to apply the boundary condition to

Source code in aragog/core.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def core_cooling(self, state: State) -> None:
    """Applies a core cooling heat flux according to Eq. (37) of Bower et al., 2018

    Args:
        state: The state to apply the boundary condition to
    """
    core_capacity: float = (
        4
        / 3
        * np.pi
        * np.power(self._mesh.basic.radii[0], 3)
        * self._mesh.settings.core_density
        * self._settings.core_heat_capacity
    )
    cell_capacity = self._mesh.basic.volume[0] * state.capacitance_staggered()[0, :]
    radius_ratio: float = self._mesh.basic.radii[1] / self._mesh.basic.radii[0]
    alpha = np.power(radius_ratio, 2) / ((cell_capacity / (core_capacity * 1.147)) + 1)

    state.heat_flux[0, :] = alpha * state.heat_flux[1, :]

grey_body(state)

Applies a grey body flux at the surface.

Args: state: The state to apply the boundary conditions to

Source code in aragog/core.py
153
154
155
156
157
158
159
160
161
162
163
def grey_body(self, state: State) -> None:
    """Applies a grey body flux at the surface.

    Args:
        state: The state to apply the boundary conditions to
    """
    state.heat_flux[-1, :] = (
        self._settings.emissivity
        * self._settings.scalings_.stefan_boltzmann_constant
        * (np.power(state.top_temperature, 4) - self._settings.equilibrium_temperature**4)
    )

InitialCondition(_parameters, _mesh, _phases) dataclass

Initial condition

Args: parameters: Parameters mesh: Mesh phases: PhaseEvaluatorCollection

get_adiabat(pressure_basic)

Gets an adiabatic temperature profile by integrating the adiatiabatic temperature gradient dTdPs from the surface. Uses the set surface temperature.

Args: Pressure field on the basic nodes

Returns: Adiabatic temperature profile for the staggered nodes

Source code in aragog/core.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def get_adiabat(self, pressure_basic) -> npt.NDArray:
    """Gets an adiabatic temperature profile by integrating
       the adiatiabatic temperature gradient dTdPs from the surface.
       Uses the set surface temperature.

    Args:
        Pressure field on the basic nodes

    Returns:
        Adiabatic temperature profile for the staggered nodes
    """

    def adiabat_ode(P,T):
        self._phases.active.set_pressure(P)
        self._phases.active.set_temperature(T)
        self._phases.active.update()
        return self._phases.active.dTdPs()

    # flip the pressure field top to bottom
    pressure_basic = np.flip(pressure_basic)

    sol = solve_ivp(
         adiabat_ode, (pressure_basic[0], pressure_basic[-1]),
         [self._settings.surface_temperature], t_eval=pressure_basic,
         method='RK45', rtol=1e-6, atol=1e-9)

    # flip back the temperature field from bottom to top
    temperature_basic = np.flip(sol.y[0])

    # Return temperature field at staggered nodes
    return self._mesh.quantity_at_staggered_nodes(temperature_basic)

get_linear()

Gets a linear temperature profile

Returns: Linear temperature profile for the staggered nodes Only works for uniform spatial mesh.

Source code in aragog/core.py
253
254
255
256
257
258
259
260
261
262
263
264
265
def get_linear(self) -> npt.NDArray:
    """Gets a linear temperature profile

    Returns:
        Linear temperature profile for the staggered nodes
        Only works for uniform spatial mesh.
    """
    temperature_basic: npt.NDArray = np.linspace(
        self._settings.basal_temperature,
        self._settings.surface_temperature,
        self._mesh.basic.number_of_nodes,
    )
    return self._mesh.quantity_at_staggered_nodes(temperature_basic)

Mesh(parameters)

A staggered mesh.

The basic mesh is used for the flux calculations and the staggered mesh is used for the volume calculations.

Args: parameters: Parameters

Source code in aragog/mesh.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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
def __init__(self, parameters: Parameters):

    # STEP 1: Set up the basic mesh
    self.settings: _MeshParameters = parameters.mesh
    basic_coordinates: npt.NDArray = self.get_constant_spacing()
    if self.settings.eos_method == 1:
        self.eos = AdamsWilliamsonEOS(
            self.settings, basic_coordinates
        )
    elif self.settings.eos_method == 2:
        self.eos = UserDefinedEOS(
            self.settings, basic_coordinates
        )
    else:
        msg: str = (f"Unknown method to initialize Equation of State")
        raise ValueError(msg)
    if parameters.mesh.mass_coordinates:
        self._planet_density: float = self.get_planet_density(basic_coordinates)
        basic_mass_coordinates: npt.NDArray = (
            self.get_basic_mass_coordinates_from_spatial_coordinates(basic_coordinates))
        logger.debug("Basic mass coordinates = %s", basic_mass_coordinates)
    else:
        basic_mass_coordinates = basic_coordinates
    self.basic: FixedMesh = FixedMesh(
        self.settings,
        basic_coordinates,
        basic_mass_coordinates,
        np.max(basic_coordinates),
        np.min(basic_coordinates)
    )

    # STEP 2: Set up the staggered mesh
    staggered_mass_coordinates: npt.NDArray = (
        self.basic.mass_radii[:-1] + 0.5 * self.basic.delta_mesh)
    if parameters.mesh.mass_coordinates:
        staggered_coordinates: npt.NDArray = (
            self.get_staggered_spatial_coordinates_from_mass_coordinates(staggered_mass_coordinates))
    else:
        staggered_coordinates = staggered_mass_coordinates
    self.staggered: FixedMesh = FixedMesh(
        self.settings,
        staggered_coordinates,
        staggered_mass_coordinates,
        self.basic.outer_boundary,
        self.basic.inner_boundary,
    )
    self.eos.set_staggered_pressure(self.staggered.radii)

    # STEP 3: Set up the transform matrices
    if parameters.mesh.mass_coordinates:
        self._dxidr: npt.NDArray = self.get_dxidr_basic()
    else:
        self._dxidr: npt.NDArray = np.ones_like(self.basic.radii)
    self._d_dr_transform: npt.NDArray = self._get_d_dr_transform_matrix()
    self._quantity_transform: npt.NDArray = self._get_quantity_transform_matrix()

dxidr property

dxi/dr at basic nodes

d_dr_at_basic_nodes(staggered_quantity)

Determines d/dr at the basic nodes of a quantity defined at the staggered nodes.

Args: staggered_quantity: A quantity defined at the staggered nodes.

Returns: d/dr at the basic nodes

Source code in aragog/mesh.py
370
371
372
373
374
375
376
377
378
379
380
381
382
def d_dr_at_basic_nodes(self, staggered_quantity: npt.NDArray) -> npt.NDArray:
    """Determines d/dr at the basic nodes of a quantity defined at the staggered nodes.

    Args:
        staggered_quantity: A quantity defined at the staggered nodes.

    Returns:
        d/dr at the basic nodes
    """
    d_dr_at_basic_nodes: npt.NDArray = self._d_dr_transform.dot(staggered_quantity)
    logger.debug("d_dr_at_basic_nodes = %s", d_dr_at_basic_nodes)

    return d_dr_at_basic_nodes

get_basic_mass_coordinates_from_spatial_coordinates(basic_coordinates)

Computes the basic mass coordinates from basic spatial coordinates.

Args: Basic spatial coordinates

Returns: Basic mass coordinates

Source code in aragog/mesh.py
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
def get_basic_mass_coordinates_from_spatial_coordinates(self, basic_coordinates: npt.NDArray) -> npt.NDArray:
    """Computes the basic mass coordinates from basic spatial coordinates.

    Args:
        Basic spatial coordinates

    Returns:
        Basic mass coordinates
    """

    # Set the mass coordinates at the inner boundary from the core mass
    basic_mass_coordinates = np.zeros_like(basic_coordinates)
    basic_mass_coordinates[:,:] = (
        self.settings.core_density / self._planet_density
        * np.power(basic_coordinates[0,:], 3.0)
    )

    # Get mass coordinates by adding individual cell contributions to the mantle mass
    basic_volumes = (np.power(basic_coordinates[1:,:],3.0)
        - np.power(basic_coordinates[:-1,:],3.0))
    for i in range(1, self.settings.number_of_nodes):
        basic_mass_coordinates[i:,:] += (
            self.staggered_effective_density[i-1,:] * basic_volumes[i-1,:] / self._planet_density
        )

    return np.power(basic_mass_coordinates, 1.0/3.0)

get_constant_spacing()

Constant radius spacing across the mantle

Returns: Radii with constant spacing as a column vector

Source code in aragog/mesh.py
320
321
322
323
324
325
326
327
328
329
330
331
def get_constant_spacing(self) -> npt.NDArray:
    """Constant radius spacing across the mantle

    Returns:
        Radii with constant spacing as a column vector
    """
    radii: npt.NDArray = np.linspace(
        self.settings.inner_radius, self.settings.outer_radius, self.settings.number_of_nodes
    )
    radii = np.atleast_2d(radii).T

    return radii

get_dxidr_basic()

Computes dxidr at basic nodes.

Source code in aragog/mesh.py
308
309
310
311
312
313
314
315
316
317
318
def get_dxidr_basic(self) -> npt.NDArray:
    """Computes dxidr at basic nodes."""

    dxidr = (
        self.eos.basic_density
        / self._planet_density
        * np.power(self.basic.radii,2.0)
        / np.power(self.basic.mass_radii,2.0)
    )

    return dxidr

get_planet_density(basic_coordinates)

Computes the planet density.

Args: Basic spatial coordinates

Returns: Planet effective density

Source code in aragog/mesh.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_planet_density(self, basic_coordinates: npt.NDArray) -> float:
    """Computes the planet density.

    Args:
        Basic spatial coordinates

    Returns:
        Planet effective density
    """
    core_mass = self.settings.core_density *  np.power(basic_coordinates[0,0], 3.0)
    basic_volumes = (np.power(basic_coordinates[1:,0],3.0)
        - np.power(basic_coordinates[:-1,0],3.0))
    mantle_mass = np.sum(
        self.staggered_effective_density[:,0] * basic_volumes
    )
    planet_density = (
        (core_mass + mantle_mass) / (np.power(basic_coordinates[-1,0],3.0)))
    return planet_density.item()

get_staggered_spatial_coordinates_from_mass_coordinates(staggered_mass_coordinates)

Computes the staggered spatial coordinates from staggered mass coordinates.

Args: Staggered mass coordinates

Returns: Staggered spatial coordinates

Source code in aragog/mesh.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def get_staggered_spatial_coordinates_from_mass_coordinates(self, staggered_mass_coordinates: npt.NDArray) -> npt.NDArray:
    """Computes the staggered spatial coordinates from staggered mass coordinates.

    Args:
        Staggered mass coordinates

    Returns:
        Staggered spatial coordinates
    """

    # Initialise the staggered spatial coordinate to the inner boundary
    staggered_coordinates = (np.ones_like(staggered_mass_coordinates)
        * np.power(self.settings.inner_radius,3.0))

    # Add first half cell contribution
    staggered_coordinates += (
        self._planet_density
         * (np.power(staggered_mass_coordinates[0,:],3.0)
        - np.power(self.basic.mass_radii[0,:], 3.0))
        / self.staggered_effective_density[0,:]
    )

    # Get spatial coordinates by adding individual cell contributions to the mantle mass
    shell_effective_density = 0.5*(
        self.staggered_effective_density[1:,:] + self.staggered_effective_density[:-1,:])
    shell_mass_volumes = (np.power(staggered_mass_coordinates[1:,:],3.0)
        - np.power(staggered_mass_coordinates[:-1,:],3.0))
    for i in range(1,self.settings.number_of_nodes-1):
        staggered_coordinates[i:,:] += (
            self._planet_density
            * shell_mass_volumes[i-1,:]
            / shell_effective_density[i-1,:]
        )

    return np.power(staggered_coordinates, 1.0/3.0)

quantity_at_basic_nodes(staggered_quantity)

Determines a quantity at the basic nodes that is defined at the staggered nodes.

Uses backward and forward differences at the inner and outer radius, respectively, to obtain the quantity values of the basic nodes at the innermost and outermost nodes. When using temperature boundary conditions, values at outer boundaries will be overwritten. When using flux boundary conditions, values at outer boundaries will be used to provide estimate of individual components of heat fluxes though the total heat flux is imposed.

Args: staggered_quantity: A quantity defined at the staggered nodes

Returns: The quantity at the basic nodes

Source code in aragog/mesh.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def quantity_at_basic_nodes(self, staggered_quantity: npt.NDArray) -> npt.NDArray:
    """Determines a quantity at the basic nodes that is defined at the staggered nodes.

    Uses backward and forward differences at the inner and outer radius, respectively, to
    obtain the quantity values of the basic nodes at the innermost and outermost nodes.
    When using temperature boundary conditions, values at outer boundaries will be overwritten.
    When using flux boundary conditions, values at outer boundaries will be used to provide
    estimate of individual components of heat fluxes though the total heat flux is imposed.

    Args:
        staggered_quantity: A quantity defined at the staggered nodes

    Returns:
        The quantity at the basic nodes
    """
    quantity_at_basic_nodes: npt.NDArray = self._quantity_transform.dot(staggered_quantity)
    logger.debug("quantity_at_basic_nodes = %s", quantity_at_basic_nodes)

    return quantity_at_basic_nodes

quantity_at_staggered_nodes(basic_quantity)

Determines a quantity at the staggered nodes that is defined at the basic nodes.

Staggered nodes are always located at cell centers, whatever the mesh.

Args: basic_quantity: A quantity defined at the basic nodes

Returns: The quantity at the staggered nodes

Source code in aragog/mesh.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def quantity_at_staggered_nodes(self, basic_quantity: npt.NDArray) -> npt.NDArray:
    """Determines a quantity at the staggered nodes that is defined at the basic nodes.

    Staggered nodes are always located at cell centers, whatever the mesh.

    Args:
        basic_quantity: A quantity defined at the basic nodes

    Returns:
        The quantity at the staggered nodes
    """
    quantity_at_staggered_nodes: npt.NDArray = 0.5 * (
        basic_quantity[:-1, ...] + basic_quantity[1:, ...])
    logger.debug("quantity_at_staggered_nodes = %s", quantity_at_staggered_nodes)

    return quantity_at_staggered_nodes

Parameters(*, boundary_conditions, energy, initial_condition, mesh, phase_solid, phase_liquid, phase_mixed, radionuclides, scalings, solver) dataclass

Assembles all the parameters.

The parameters in each section are scaled here to ensure that all the parameters are scaled (non-dimensionalised) consistently with each other.

from_file(*filenames) classmethod

Parses the parameters in a configuration file(s)

Args: *filenames: Filenames of the configuration data

Source code in aragog/parser.py
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
@classmethod
def from_file(cls, *filenames) -> Self:
    """Parses the parameters in a configuration file(s)

    Args:
        *filenames: Filenames of the configuration data
    """
    parser: ConfigParser = ConfigParser()
    parser.read(*filenames)

    init_dict: dict[str, Any] = {}
    for section_name, dataclass_ in _get_dataclass_from_section_name().items():
        init_dict[section_name] = parser.parse_section(
            using_dataclass=dataclass_, section_name=section_name
        )
    radionuclides: list[_Radionuclide] = []
    for radionuclide_section in cls.radionuclide_sections(parser):
        radionuclide = parser.parse_section(
            using_dataclass=_Radionuclide, section_name=radionuclide_section
        )
        radionuclides.append(radionuclide)

    init_dict["radionuclides"] = radionuclides

    return cls(**init_dict)  # Unpacking gives required arguments so pylint: disable=E1125

radionuclide_sections(parser) staticmethod

Section names relating to radionuclides

Sections relating to radionuclides must have the prefix radionuclide_

Source code in aragog/parser.py
499
500
501
502
503
504
505
506
507
508
509
@staticmethod
def radionuclide_sections(parser: ConfigParser) -> list[str]:
    """Section names relating to radionuclides

    Sections relating to radionuclides must have the prefix radionuclide_
    """
    return [
        parser[section].name
        for section in parser.sections()
        if section.startswith("radionuclide_")
    ]

PhaseEvaluatorCollection(parameters) dataclass

A collection of phase evaluators

Creates the phase evaluators and selects the active phase based on configuration data.

Args: parameters: Parameters

Attributes: liquid: Liquid evaluator solid: Solid evaluator mixed: Mixed evaluator composite: Composite evaluator active: The active evaluator, which is defined by configuration data

State(parameters, _evaluator) dataclass

Stores and updates the state at temperature and pressure.

Args: parameters: Parameters evaluator: Evaluator

Attributes: evaluator: Evaluator critical_reynolds_number: Critical Reynolds number gravitational_separation_flux: Gravitational separation flux at the basic nodes heating: Total internal heat production at the staggered nodes (power per unit mass) heating_radio: Radiogenic heat production at the staggered nodes (power per unit mass) heating_tidal: Tidal heat production at the staggered nodes (power per unit mass) heat_flux: Heat flux at the basic nodes (power per unit area) inviscid_regime: True if the flow is inviscid and otherwise False, at the basic nodes inviscid_velocity: Inviscid velocity at the basic nodes is_convective: True if the flow is convecting and otherwise False, at the basic nodes reynolds_number: Reynolds number at the basic nodes super_adiabatic_temperature_gradient: Super adiabatic temperature gradient at the basic nod temperature_basic: Temperature at the basic nodes temperature_staggered: Temperature at the staggered nodes bottom_temperature: Temperature at the bottom basic node top_temperature: Temperature at the top basic node viscous_regime: True if the flow is viscous and otherwise False, at the basic nodes viscous_velocity: Viscous velocity at the basic nodes

critical_reynolds_number property

Critical Reynolds number from Abe (1993)

heat_flux property

The total heat flux according to the fluxes specified in the configuration.

heating property

The power generation according to the heat sources specified in the configuration.

heating_dilatation property

The heat source through dilation/compression.

heating_radio property

The radiogenic power generation.

heating_tidal property

The tidal power generation.

mass_flux property

The total melt mass flux according to the fluxes specified in the configuration.

conductive_heat_flux()

Conductive heat flux:

\[ q_{cond} = -k \frac{\partial T}{\partial r} \]

where \(k\) is thermal conductivity, \(T\) is temperature, and \(r\) is radius.

Source code in aragog/solver.py
114
115
116
117
118
119
120
121
122
123
124
125
def conductive_heat_flux(self) -> npt.NDArray:
    r"""Conductive heat flux:

    $$
    q_{cond} = -k \frac{\partial T}{\partial r}
    $$

    where $k$ is thermal conductivity, $T$ is temperature, and $r$ is radius.
    """
    conductive_heat_flux: npt.NDArray = self.phase_basic.thermal_conductivity() * -self.dTdr()

    return conductive_heat_flux

convective_heat_flux()

Convective heat flux:

\[ q_{conv} = -\rho c_p \kappa_h \left( \frac{\partial T}{\partial r} - \left( \frac{\partial T}{\partial r} \right)_S \right) \]

where \(\rho\) is density, \(c_p\) is heat capacity at constant pressure, \(\kappa_h\) is eddy diffusivity, \(T\) is temperature, \(r\) is radius, and \(S\) is entropy.

Source code in aragog/solver.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def convective_heat_flux(self) -> npt.NDArray:
    r"""Convective heat flux:

    $$
    q_{conv} = -\rho c_p \kappa_h \left( \frac{\partial T}{\partial r}
        - \left( \frac{\partial T}{\partial r} \right)_S \right)
    $$ 

    where $\rho$ is density, $c_p$ is heat capacity at constant pressure,
    $\kappa_h$ is eddy diffusivity, $T$ is temperature, $r$ is radius, and
    $S$ is entropy.
    """
    convective_heat_flux: npt.NDArray = (
        self.phase_basic.density()
        * self.phase_basic.heat_capacity()
        * self.eddy_diffusivity()
        * -self._super_adiabatic_temperature_gradient
    )

    return convective_heat_flux

dilatation_heating()

Dilatation/compression heating (power per unit mass)

Returns: Dilatation/compression heating (power per unit mass) at each layer of the staggered mesh, at a given point in time.

Source code in aragog/solver.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def dilatation_heating(self) -> npt.NDArray:
    """Dilatation/compression heating (power per unit mass)

    Returns:
        Dilatation/compression heating (power per unit mass) at each layer of the staggered
            mesh, at a given point in time.
    """

    mass_flux_staggered: npt.NDArray = self._evaluator.mesh.quantity_at_staggered_nodes(self._mass_flux)

    dilatation_volume_source: npt.NDArray = (
        self.phase_staggered.gravitational_acceleration()
        * self.phase_staggered.delta_specific_volume()
        * mass_flux_staggered
    )

    return dilatation_volume_source

gravitational_separation_mass_flux()

Gravitational separation mass flux:

\[ j_{grav} = \rho \phi (1 - \phi) v_{rel} \]

where \(\rho\) is density, \(\phi\) is melt fraction, and \(v_{rel}\) is relative velocity.

Source code in aragog/solver.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def gravitational_separation_mass_flux(self) -> npt.NDArray:
    r"""Gravitational separation mass flux:

    $$
    j_{grav} = \rho \phi (1 - \phi) v_{rel}
    $$ 

    where $\rho$ is density, $\phi$ is melt fraction, and
    $v_{rel}$ is relative velocity.
    """
    gravitational_separation_mass_flux: npt.NDArray = (
        self.phase_basic.density()
        * self.phase_basic.melt_fraction()
        * (1.0 - self.phase_basic.melt_fraction())
        * self.phase_basic.relative_velocity()
    )
    return gravitational_separation_mass_flux

mixing_mass_flux()

Mixing mass flux:

\[ j_{cm} = -\rho \kappa_h \frac{\partial \phi}{\partial r} \]

where \(\rho\) is density, \(\kappa_h\) is eddy diffusivity, \(\phi\) is melt mass fraction, and \(r\) is radius.

Source code in aragog/solver.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def mixing_mass_flux(self) -> npt.NDArray:
    r"""Mixing mass flux:

    $$
    j_{cm} = -\rho \kappa_h \frac{\partial \phi}{\partial r}
    $$

    where $\rho$ is density, $\kappa_h$ is eddy diffusivity,
    $\phi$ is melt mass fraction, and $r$ is radius.
    """
    mixing_mass_flux: npt.NDArray = (
        self.phase_basic.density()
        * self.eddy_diffusivity()
        * -self.dphidr()
    )
    return mixing_mass_flux

radiogenic_heating(time)

Radiogenic heating (constant with radius)

Args: time: Time

Returns: Radiogenic heating (power per unit mass) at each layer of the staggered mesh, at a given point in time.

Source code in aragog/solver.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def radiogenic_heating(self, time: float) -> npt.NDArray:
    """Radiogenic heating (constant with radius)

    Args:
        time: Time

    Returns:
        Radiogenic heating (power per unit mass) at each layer of the staggered
            mesh, at a given point in time.
    """

    # Total heat production at a given time (power per unit mass)
    radio_heating_float: float = 0
    for radionuclide in self._evaluator.radionuclides:
        radio_heating_float += radionuclide.get_heating(time)

    # Convert to 1D array (assuming abundances are constant)
    return radio_heating_float * np.ones_like(self.temperature_staggered)

tidal_heating()

Tidal heating at each layer of the mantle.

Args: time: Time

Returns: Tidal heating (power per unit mass) at each layer of the staggered mesh, at a given point in time.

Source code in aragog/solver.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def tidal_heating(self) -> npt.NDArray:
    """Tidal heating at each layer of the mantle.

    Args:
        time: Time

    Returns:
        Tidal heating (power per unit mass) at each layer of the staggered
            mesh, at a given point in time.
    """

    length = len(self._settings.tidal_array)

    # Must have correct shape
    if length == 1:
        # scalar
        out = np.ones_like(self.temperature_staggered) * self._settings.tidal_array[0]

    elif length == len(self.temperature_staggered):
        # vector with correct length
        out = np.array([self._settings.tidal_array]).T

    else:
        # vector with invalid length
        raise ValueError(f"Tidal heating array has invalid length {length}")

    return out

update(temperature, time)

Updates the state.

The evaluation order matters because we want to minimise the number of evaluations.

Args: temperature: Temperature at the staggered nodes pressure: Pressure at the staggered nodes time: Time

Source code in aragog/solver.py
336
337
338
339
340
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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def update(self, temperature: npt.NDArray, time: FloatOrArray) -> None:
    """Updates the state.

    The evaluation order matters because we want to minimise the number of evaluations.

    Args:
        temperature: Temperature at the staggered nodes
        pressure: Pressure at the staggered nodes
        time: Time
    """
    logger.debug("Updating the state")

    logger.debug("Setting the temperature profile")
    self._temperature_staggered = temperature
    self._temperature_basic = self._evaluator.mesh.quantity_at_basic_nodes(temperature)
    logger.debug("temperature_basic = %s", self.temperature_basic)
    self._dTdr = self._evaluator.mesh.d_dr_at_basic_nodes(temperature)
    logger.debug("dTdr = %s", self.dTdr())
    self._evaluator.boundary_conditions.apply_temperature_boundary_conditions(
        temperature, self._temperature_basic, self.dTdr()
    )

    logger.debug("Setting the melt fraction profile")
    self.phase_staggered.set_temperature(temperature)
    self.phase_staggered.update()
    self.phase_basic.set_temperature(self._temperature_basic)
    self.phase_basic.update()
    phase_to_use = self._evaluator._parameters.phase_mixed.phase
    if phase_to_use == "mixed" or phase_to_use == "composite":
        self._dphidr = self._evaluator.mesh.d_dr_at_basic_nodes(
            self.phase_staggered.melt_fraction()
        )
        logger.debug("dphidr = %s", self.dphidr())
        self._evaluator.boundary_conditions.apply_temperature_boundary_conditions_melt(
            self.phase_staggered.melt_fraction(), self.phase_basic.melt_fraction(), self._dphidr
        )
    else:
        self._dphidr = np.zeros_like(self._dTdr)

    self._super_adiabatic_temperature_gradient = self.dTdr() - self.phase_basic.dTdrs()
    self._is_convective = self._super_adiabatic_temperature_gradient < 0
    velocity_prefactor: npt.NDArray = (
        self.phase_basic.gravitational_acceleration()
        * self.phase_basic.thermal_expansivity()
        * -self._super_adiabatic_temperature_gradient
    )
    # Viscous velocity
    self._viscous_velocity = (
        velocity_prefactor * self._evaluator.mesh.basic.mixing_length_cubed
    ) / (18 * self.phase_basic.kinematic_viscosity())
    self._viscous_velocity[~self.is_convective] = 0  # Must be super-adiabatic
    # Inviscid velocity
    self._inviscid_velocity = (
        velocity_prefactor * self._evaluator.mesh.basic.mixing_length_squared
    ) / 16
    self._inviscid_velocity[~self.is_convective] = 0  # Must be super-adiabatic
    self._inviscid_velocity[self._is_convective] = np.sqrt(
        self._inviscid_velocity[self._is_convective]
    )
    # Reynolds number
    self._reynolds_number = (
        self._viscous_velocity
        * self._evaluator.mesh.basic.mixing_length
        / self.phase_basic.kinematic_viscosity()
    )
    # Eddy diffusivity
    self._eddy_diffusivity = np.where(
        self.viscous_regime, self._viscous_velocity, self._inviscid_velocity
    )
    self._eddy_diffusivity *= self._evaluator.mesh.basic.mixing_length

    # Heat flux (power per unit area)
    self._heat_flux = np.zeros_like(self.temperature_basic)
    self._mass_flux = np.zeros_like(self.temperature_basic)
    if self._settings.conduction:
        self._heat_flux += self.conductive_heat_flux()
    if self._settings.convection:
        self._heat_flux += self.convective_heat_flux()
    if self._settings.gravitational_separation:
        self._mass_flux += self.gravitational_separation_mass_flux()
    if self._settings.mixing:
        self._mass_flux += self.mixing_mass_flux()
    self._heat_flux += self._mass_flux * self.phase_basic.latent_heat()

    # Heating (power per unit mass)
    self._heating = np.zeros_like(self.temperature_staggered)
    self._heating_radio = np.zeros_like(self.temperature_staggered)
    self._heating_dilatation = np.zeros_like(self.temperature_staggered)
    self._heating_tidal = np.zeros_like(self.temperature_staggered)

    if self._settings.radionuclides:
        self._heating_radio = self.radiogenic_heating(time)
        self._heating += self._heating_radio

    if self._settings.dilatation:
        self._heating_dilatation = self.dilatation_heating()
        self._heating += self._heating_dilatation

    if self._settings.tidal:
        self._heating_tidal = self.tidal_heating()
        self._heating += self._heating_tidal

_BoundaryConditionsParameters(outer_boundary_condition, outer_boundary_value, inner_boundary_condition, inner_boundary_value, emissivity, equilibrium_temperature, core_heat_capacity) dataclass

Stores parameters in the boundary_conditions section in the configuration data.

scale_attributes(scalings)

Scales the attributes.

Args: scalings: scalings

Source code in aragog/parser.py
149
150
151
152
153
154
155
156
157
158
159
def scale_attributes(self, scalings: _ScalingsParameters) -> None:
    """Scales the attributes.

    Args:
        scalings: scalings
    """
    self.scalings_ = scalings
    self.equilibrium_temperature /= self.scalings_.temperature
    self.core_heat_capacity /= self.scalings_.heat_capacity
    self._scale_inner_boundary_condition()
    self._scale_outer_boundary_condition()

_InitialConditionParameters(initial_condition=1, surface_temperature=4000, basal_temperature=4000, init_file='') dataclass

Stores the settings in the initial_condition section in the configuration data.

scale_attributes(scalings)

Scales the attributes.

Initial condition method 1: Linear profile 2: User-defined temperature field (from file) 3: Adiabatic profile

Args: scalings: scalings

Source code in aragog/parser.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def scale_attributes(self, scalings: _ScalingsParameters) -> None:
    """Scales the attributes.

    Initial condition method
        1: Linear profile
        2: User-defined temperature field (from file)
        3: Adiabatic profile

    Args:
        scalings: scalings
    """
    self.scalings_ = scalings
    self.surface_temperature /= self.scalings_.temperature
    self.basal_temperature /= self.scalings_.temperature

    if self.initial_condition == 2:
        if self.init_file == "":
            msg: str = (f"you must provide an initial temperature file")
            raise ValueError(msg)
        self.init_temperature = np.loadtxt(self.init_file)
        self.init_temperature /= self.scalings_.temperature