Skip to content

aragog (package)

aragog

Package level variables and initialises the package logger

CFG_DATA = importlib.resources.files(f'{__package__}.cfg') module-attribute

__version__ = '26.01.06' module-attribute

logger = logging.getLogger(__name__) module-attribute

Output(solver)

Stores inputs and outputs of the models.

Source code in aragog/output.py
44
45
46
47
48
49
def __init__(self, solver: Solver):
    self.solver: Solver = solver
    self.parameters: Parameters = solver.parameters
    self.solution: OptimizeResult = self.solver.solution
    self.evaluator: Evaluator = self.solver.evaluator
    self.state: State = self.solver.state

conductive_heat_flux_basic property

Conductive heat flux

convective_heat_flux_basic property

Convective heat flux

core_mass property

Core mass computed with constant density

dTdr property

dTdr

dTdrs property

dTdrs

density_basic property

Density

gravitational_separation_heat_flux_basic property

Gravitational separation heat flux

heat_capacity_basic property

Heat capacity

heating property

Internal heat generation at staggered nodes

heating_dilatation property

Internal heat generation from dilatation/compression at staggered nodes

heating_radio property

Internal heat generation from radioactive decay at staggered nodes

heating_tidal property

Internal heat generation from tidal heat dissipation at staggered nodes

liquidus_K_staggered property

Liquidus

log10_viscosity_basic property

Viscosity of the basic mesh

log10_viscosity_staggered property

Viscosity of the staggered mesh

mantle_mass property

Mantle mass computed from the AdamsWilliamsonEOS

mass_radii_km_basic property

Mass radii of the basic mesh in km

mass_radii_km_staggered property

Mass radii of the staggered mesh in km

mass_staggered property

Mass of each layer on staggered mesh

melt_fraction_basic property

Melt fraction on the basic mesh

melt_fraction_global property

Volume-averaged melt fraction

melt_fraction_staggered property

Melt fraction on the staggered mesh

mixing_heat_flux_basic property

Convective mixing heat flux

pressure_GPa_basic property

Pressure of the basic mesh in GPa

pressure_GPa_staggered property

Pressure of the staggered mesh in GPa

radii_km_basic property

Radii of the basic mesh in km

radii_km_staggered property

Radii of the staggered mesh in km

rheological_front property

Rheological front at the last solve iteration given user defined threshold. It is defined as a dimensionless distance with respect to the outer radius.

shape_basic property

Shape of the basic data

shape_staggered property

Shape of the staggered data

solidus_K_staggered property

Solidus

solution_top_temperature property

Solution (last iteration) temperature at the top of the domain (planet surface)

super_adiabatic_temperature_gradient_basic property

Super adiabatic temperature gradient

temperature_K_basic property

Temperature of the basic mesh in K

temperature_K_staggered property

Temperature of the staggered mesh in K

thermal_expansivity_basic property

Thermal expansivity

times property

Times in years

total_heat_flux_basic property

Conductive heat flux

plot(num_lines=11, figsize=(25, 10))

Plots the solution with labelled lines according to time.

Args: num_lines: Number of lines to plot. Defaults to 11. figsize: Size of the figure. Defaults to (25, 10).

Source code in aragog/output.py
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
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
507
508
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 plot(self, num_lines: int = 11, figsize: tuple = (25, 10)) -> None:
    """Plots the solution with labelled lines according to time.

    Args:
        num_lines: Number of lines to plot. Defaults to 11.
        figsize: Size of the figure. Defaults to (25, 10).
    """
    assert self.solution is not None

    self.state.update(self.solution.y, self.solution.t)

    _, axs = plt.subplots(1, 10, sharey=True, figsize=figsize)

    # Ensure there are at least 2 lines to plot (first and last).
    num_lines = max(2, num_lines)

    # Calculate the time step based on the total number of lines.
    time_step: float = self.time_range / (num_lines - 1)

    # plot temperature
    try:
        axs[0].scatter(self.liquidus_K_staggered, self.pressure_GPa_staggered)
        axs[0].scatter(self.solidus_K_staggered, self.pressure_GPa_staggered)
    except AttributeError:
        pass

    # Plot the first line.
    def plot_times(ax, x: npt.NDArray, y: npt.NDArray) -> None:
        # If `x` is a float, create an array of the same length as `y` filled with `x`
        if np.isscalar(x):
            x = np.full((len(y), len(self.times)), x, dtype=np.float64)
        elif x.ndim == 1:
            # If `x` is 1D, reshape it or repeat it across the time dimension
            x = np.tile(x[:, np.newaxis], (1, len(self.times)))

        label_first: str = f"{self.times[0]:.2f}"
        ax.plot(x[:, 0], y, label=label_first)

        # Loop through the selected lines and plot each with a label.
        for i in range(1, num_lines - 1):
            desired_time: float = self.times[0] + i * time_step
            # Find the closest available time step.
            closest_time_index: np.intp = np.argmin(np.abs(self.times - desired_time))
            time: float = self.times[closest_time_index]
            label: str = f"{time:.2f}"  # Create a label based on the time.
            ax.plot(
                x[:, closest_time_index],
                y,
                label=label,
            )

        # Plot the last line.
        times_end: float = self.times[-1]
        label_last: str = f"{times_end:.2f}"
        ax.plot(x[:, -1], y, label=label_last)

    plot_times(axs[0], self.temperature_K_basic, self.pressure_GPa_basic)
    axs[0].set_ylabel("Pressure (GPa)")
    axs[0].set_xlabel("Temperature (K)")
    axs[0].set_title("Temperature")

    plot_times(axs[1], self.melt_fraction_staggered, self.pressure_GPa_staggered)
    axs[1].set_xlabel("Melt fraction")
    axs[1].set_title("Melt fraction")

    plot_times(axs[2], self.log10_viscosity_basic, self.pressure_GPa_basic)
    axs[2].set_xlabel("Log10(viscosity)")
    axs[2].set_title("Log10(viscosity)")

    plot_times(axs[3], self.convective_heat_flux_basic, self.pressure_GPa_basic)
    axs[3].set_xlabel("Convective heat flux")
    axs[3].set_title("Convective heat flux")

    plot_times(
        axs[4],
        self.super_adiabatic_temperature_gradient_basic,
        self.pressure_GPa_basic,
    )
    axs[4].set_xlabel("Super adiabatic temperature gradient")
    axs[4].set_title("Super adiabatic temperature gradient")

    plot_times(
        axs[5],
        self.dTdr,
        self.pressure_GPa_basic,
    )
    axs[5].set_xlabel("dTdr")
    axs[5].set_title("dTdr")

    plot_times(
        axs[6],
        self.dTdrs,
        self.pressure_GPa_basic,
    )
    axs[6].set_xlabel("dTdrs")
    axs[6].set_title("dTdrs")

    plot_times(
        axs[7],
        self.density_basic,
        self.pressure_GPa_basic,
    )
    axs[7].set_xlabel("Density")
    axs[7].set_title("Density")

    plot_times(
        axs[8],
        self.heat_capacity_basic,
        self.pressure_GPa_basic,
    )
    axs[8].set_xlabel("Heat capacity")
    axs[8].set_title("Heat capacity")

    plot_times(
        axs[9],
        self.thermal_expansivity_basic,
        self.pressure_GPa_basic,
    )
    axs[9].set_xlabel("Thermal expansivity")
    axs[9].set_title("Thermal expansivity")

    # Shrink current axis by 20% to allow space for the legend.
    # box = ax.get_position()
    # ax.set_position((box.x0, box.y0, box.width * 0.8, box.height))

    # axs[0].grid(True)

    legend = axs[2].legend()  # (loc="center left", bbox_to_anchor=(1, 0.5))
    legend.set_title("Time (yr)")
    plt.gca().invert_yaxis()

    plt.show()

write_at_time(file_path, tidx=-1, compress=False)

Write the state of the model at a particular time to a NetCDF4 file on the disk.

Args: file_path: Path to the output file tidx: Index on the time axis at which to access the data compress: Whether to compress the data

Source code in aragog/output.py
324
325
326
327
328
329
330
331
332
333
334
335
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
def write_at_time(self, file_path: str, tidx: int = -1, compress: bool = False) -> None:
    """Write the state of the model at a particular time to a NetCDF4 file on the disk.

    Args:
        file_path: Path to the output file
        tidx: Index on the time axis at which to access the data
        compress: Whether to compress the data
    """

    logger.debug("Writing i=%d NetCDF file to %s", tidx, file_path)

    # Update the state
    assert self.solution is not None
    self.state.update(self.solution.y, self.solution.t)

    # Open the dataset
    ds: nc.Dataset = nc.Dataset(file_path, mode="w")

    # Metadata
    ds.description = "Aragog output data"
    ds.argog_version = __version__

    # Function to save scalar quantities
    def _add_scalar_variable(key: str, value: float, units: str):
        ds.createVariable(key, np.float64)
        ds[key][0] = value.item() if hasattr(value, 'item') else value
        ds[key].units = units

    # Save scalar quantities
    _add_scalar_variable("time", self.times[tidx], "yr")
    _add_scalar_variable("phi_global", self.melt_fraction_global, "")
    _add_scalar_variable("mantle_mass", self.mantle_mass, "kg")
    _add_scalar_variable("rheo_front", self.rheological_front, "")

    # Create dimensions for mesh quantities
    ds.createDimension("basic", self.shape_basic[0])
    ds.createDimension("staggered", self.shape_staggered[0])

    # Function to save mesh quantities
    def _add_mesh_variable(key: str, some_property: Any, units: str):
        if key[-2:] == "_b":
            mesh = "basic"
        elif key[-2:] == "_s":
            mesh = "staggered"
        else:
            raise KeyError(f"NetCDF variable name must end in _b or _s: {key}")
        ds.createVariable(
            key,
            np.float64,
            (mesh,),
            compression="zlib" if compress else None,
            shuffle = True,
            complevel = 4
        )
        ds[key][:] = some_property[:, tidx]
        ds[key].units = units

    # Save mesh quantities
    _add_mesh_variable("radius_b", self.radii_km_basic, "km")
    _add_mesh_variable("pres_b", self.pressure_GPa_basic, "GPa")
    _add_mesh_variable("temp_b", self.temperature_K_basic, "K")
    _add_mesh_variable("phi_b", self.melt_fraction_basic, "")
    _add_mesh_variable("Fcond_b", self.conductive_heat_flux_basic, "W m-2")
    _add_mesh_variable("Fconv_b", self.convective_heat_flux_basic, "W m-2")
    _add_mesh_variable("Fgrav_b", self.gravitational_separation_heat_flux_basic, "W m-2")
    _add_mesh_variable("Fmix_b", self.mixing_heat_flux_basic, "W m-2")
    _add_mesh_variable("Ftotal_b", self.total_heat_flux_basic, "W m-2")
    _add_mesh_variable("log10visc_b", self.log10_viscosity_basic, "Pa s")
    _add_mesh_variable("log10visc_s", self.log10_viscosity_staggered, "Pa s")
    _add_mesh_variable("density_b", self.density_basic, "kg m-3")
    _add_mesh_variable("heatcap_b", self.heat_capacity_basic, "J kg-1 K-1")
    _add_mesh_variable("mass_s", self.mass_staggered, "kg")
    _add_mesh_variable("Hradio_s", self.heating_radio, "W kg-1")
    _add_mesh_variable("Hvol_s", self.heating_dilatation, "W kg-1")
    _add_mesh_variable("Htidal_s", self.heating_tidal, "W kg-1")
    _add_mesh_variable("Htotal_s", self.heating, "W kg-1")

    # Close the dataset
    ds.close()

Solver(param)

Solves the interior dynamics

Args: filename: Filename of a file with configuration settings root: Root path to the flename

Attributes: filename: Filename of a file with configuration settings root: Root path to the filename. Defaults to empty parameters: Parameters evaluator: Evaluator state: State

Source code in aragog/solver.py
486
487
488
489
490
491
def __init__(self, param: Parameters):
    logger.info("Creating an Aragog model")
    self.parameters: Parameters = param
    self.evaluator: Evaluator
    self.state: State
    self._solution: OptimizeResult

solution property

The solution.

temperature_basic property

Temperature of the basic mesh in K

temperature_staggered property

Temperature of the staggered mesh in K

dTdt(time, temperature)

dT/dt at the staggered nodes

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

Returns: dT/dt at the staggered nodes

Source code in aragog/solver.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
def dTdt(
    self,
    time: npt.NDArray | float,
    temperature: npt.NDArray,
) -> npt.NDArray:
    """dT/dt at the staggered nodes

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

    Returns:
        dT/dt at the staggered nodes
    """
    logger.debug("temperature passed into dTdt = %s", temperature)
    # logger.debug("temperature.shape = %s", temperature.shape)
    self.state.update(temperature, time)
    heat_flux: npt.NDArray = self.state.heat_flux
    # logger.debug("heat_flux = %s", heat_flux)
    self.evaluator.boundary_conditions.apply_flux_boundary_conditions(self.state)
    # logger.debug("heat_flux = %s", heat_flux)
    # logger.debug("mesh.basic.area.shape = %s", self.data.mesh.basic.area.shape)

    energy_flux: npt.NDArray = heat_flux * self.evaluator.mesh.basic.area
    # logger.debug("energy_flux size = %s", energy_flux.shape)

    delta_energy_flux: npt.NDArray = np.diff(energy_flux, axis=0)
    # logger.debug("delta_energy_flux size = %s", delta_energy_flux.shape)
    # logger.debug("capacitance = %s", self.state.phase_staggered.capacitance.shape)
    # FIXME: Update capacitance for mixed phase (enthalpy of fusion contribution)
    capacitance: npt.NDArray = (
        self.state.capacitance_staggered() * self.evaluator.mesh.basic.volume
    )

    # Heating rate (dT/dt) from flux divergence (power per unit area)
    dTdt: npt.NDArray = -delta_energy_flux / capacitance
    logger.debug("dTdt (fluxes only) = %s", dTdt)

    # Additional heating rate (dT/dt) from internal heating (power per unit mass)
    dTdt += self.state.heating * (
        self.state.phase_staggered.density() / self.state.capacitance_staggered()
    )

    logger.debug("dTdt (with internal heating) = %s", dTdt)

    return dTdt

from_file(filename, root=Path()) classmethod

Parses a configuration file

Args: filename: Filename root: Root of the filename

Returns: Parameters

Source code in aragog/solver.py
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
@classmethod
def from_file(cls, filename: str | Path, root: str | Path = Path()) -> Self:
    """Parses a configuration file

    Args:
        filename: Filename
        root: Root of the filename

    Returns:
        Parameters
    """
    configuration_file: Path = Path(root) / Path(filename)
    logger.info("Parsing configuration file = %s", configuration_file)
    parameters: Parameters = Parameters.from_file(configuration_file)

    return cls(parameters)

initialize()

Initializes the model.

Source code in aragog/solver.py
510
511
512
513
514
def initialize(self) -> None:
    """Initializes the model."""
    logger.info("Initializing %s", self.__class__.__name__)
    self.evaluator = Evaluator(self.parameters)
    self.state = State(self.parameters, self.evaluator)

make_tsurf_event()

Creates a temperature event function for use with an ODE solver to monitor changes in the surface temperature.The event triggers when the change exceeds the threshold, allowing the solver to stop integration.

Returns: The event has the attributes: - terminal = True: Integration stops when the event is triggered. - direction = -1: Only triggers when the function is decreasing through zero.

Source code in aragog/solver.py
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
def make_tsurf_event(self):
    """
    Creates a temperature event function for use with an ODE solver to monitor changes 
    in the surface temperature.The event triggers when the change exceeds the 
    threshold, allowing the solver to stop integration.

    Returns:
        The event has the attributes:
            - terminal = True: Integration stops when the event is triggered.
            - direction = -1: Only triggers when the function is decreasing through zero.
    """
    tsurf_initial = [None]

    def tsurf_event(time: float, temperature: npt.NDArray) -> float:
        """
        Event function to detect when surface temperature changes beyond a specified threshold.

        Args:
            time (float): Current time.
            temperature (np.ndarray): Current temperature profile.

        Returns:
            float: The difference between the threshold and the actual change in surface 
                temperature. When this value crosses zero from above, the event is triggered.
        """
        tsurf_current = temperature[-1] * self.parameters.scalings.temperature
        tsurf_threshold = self.parameters.solver.tsurf_poststep_change * self.parameters.scalings.temperature

        if tsurf_initial[0] is None:
            tsurf_initial[0] = tsurf_current
            return 1.0  

        delta = abs(tsurf_current - tsurf_initial[0])

        return tsurf_threshold - delta  

    tsurf_event.terminal = self.parameters.solver.event_triggering
    tsurf_event.direction = -1

    return tsurf_event

reset()

This function initializes the model, while keeping the previous state of the PhaseEvaluatorCollection object. This avoids multiple loads of lookup table data when running Aragog multiple times.

Source code in aragog/solver.py
516
517
518
519
520
521
522
523
524
525
526
527
def reset(self) -> None:
    """This function initializes the model, while keeping the previous state of the
    PhaseEvaluatorCollection object. This avoids multiple loads of lookup table data
    when running Aragog multiple times.
    """
    logger.info("Resetting %s", self.__class__.__name__)
    # Update the Evaluator object except the phase properties
    self.evaluator.mesh = Mesh(self.parameters)
    self.evaluator.boundary_conditions = BoundaryConditions(self.parameters, self.evaluator.mesh)
    self.evaluator.initial_condition = InitialCondition(self.parameters, self.evaluator.mesh, self.evaluator.phases)
    # Reinstantiate the solver state
    self.state = State(self.parameters, self.evaluator)

DownloadLookupTableData(fname='')

Download lookup table data

Inputs : - fname (optional) : folder name, i.e. "1TPa-dK09-elec-free/MgSiO3_Wolf_Bower_2018" if not provided download all the folder list

Source code in aragog/data.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
152
153
154
155
156
157
158
159
160
161
def DownloadLookupTableData(fname: str = ""):
    """
    Download lookup table data

    Inputs :
        - fname (optional) :    folder name, i.e. "1TPa-dK09-elec-free/MgSiO3_Wolf_Bower_2018"
                                if not provided download all the folder list
    """

    osf = OSF()
    project = osf.project(project_id)
    storage = project.storage("osfstorage")

    data_dir = GetFWLData() / "interior_lookup_tables"
    data_dir.mkdir(parents=True, exist_ok=True)

    # If no folder specified download all basic list
    if not fname:
        folder_list = basic_list
    elif fname in full_list:
        folder_list = [fname]
    else:
        raise ValueError(f"Unrecognised folder name: {fname}")

    for folder in folder_list:
        folder_dir = data_dir / folder
        max_tries = 2 # Maximum download attempts, could be a function argument

        if not folder_dir.exists():
            logger.info(f"Downloading interior lookup table data to {data_dir}")
            for i in range(max_tries):
                logger.info(f"Attempt {i + 1} of {max_tries}")
                success = False

                try:
                    download_zenodo_folder(folder = folder, data_dir=data_dir)
                    success = True
                except RuntimeError as e:
                    logger.error(f"Zenodo download failed: {e}")
                    folder_dir.rmdir()

                if not success:
                    try:
                        download_OSF_folder(storage=storage, folders=folder, data_dir=data_dir)
                        success = True
                    except RuntimeError as e:
                        logger.error(f"OSF download failed: {e}")

                if success:
                    break

                if i < max_tries - 1:
                    logger.info("Retrying download...")
                    sleep(5) # Wait 5 seconds before retrying
                else:
                    logger.error("Max retries reached. Download failed.")

    return

aragog_file_logger(console_level=logging.INFO, file_level=logging.DEBUG, log_dir=os.getcwd())

Sets up console logging and file logging according to arguments.

Returns: A logger

Source code in aragog/__init__.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def aragog_file_logger(
    console_level=logging.INFO, file_level=logging.DEBUG, log_dir=os.getcwd()
) -> logging.Logger:
    """Sets up console logging and file logging according to arguments.

    Returns:
        A logger
    """
    # Console logger
    package_logger: logging.Logger = logging.getLogger(__name__)
    package_logger.setLevel(file_level)
    package_logger.handlers = []
    console_handler: logging.Handler = logging.StreamHandler()
    console_formatter: logging.Formatter = simple_formatter()
    console_handler.setFormatter(console_formatter)
    console_handler.setLevel(console_level)
    logger.addHandler(console_handler)
    # File logger
    log_file = os.path.join(log_dir, f"{__package__}.log")
    file_handler: logging.Handler = logging.FileHandler(log_file)
    file_formatter: logging.Formatter = complex_formatter()
    file_handler.setFormatter(file_formatter)
    file_handler.setLevel(logging.DEBUG)
    package_logger.addHandler(file_handler)

    return package_logger

complex_formatter()

Complex formatter for logging

Returns: Formatter for logging

Source code in aragog/__init__.py
36
37
38
39
40
41
42
43
44
45
46
47
def complex_formatter() -> logging.Formatter:
    """Complex formatter for logging

    Returns:
        Formatter for logging
    """
    fmt: str = "[%(asctime)s - %(name)-30s - %(lineno)03d - %(levelname)-9s - %(funcName)s()]"
    fmt += " - %(message)s"
    datefmt: str = "%Y-%m-%d %H:%M:%S"
    formatter: logging.Formatter = logging.Formatter(fmt, datefmt=datefmt)

    return formatter

debug_logger()

Sets up debug logging to the console.

Returns: A logger

Source code in aragog/__init__.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def debug_logger() -> logging.Logger:
    """Sets up debug logging to the console.

    Returns:
        A logger
    """
    package_logger: logging.Logger = logging.getLogger(__name__)
    package_logger.setLevel(logging.DEBUG)
    logger.handlers = []
    console_handler: logging.Handler = logging.StreamHandler()
    console_formatter: logging.Formatter = simple_formatter()
    console_handler.setFormatter(console_formatter)
    logger.addHandler(console_handler)

    return package_logger

simple_formatter()

Simple formatter for logging

Returns: Formatter for logging

Source code in aragog/__init__.py
50
51
52
53
54
55
56
57
58
59
60
def simple_formatter() -> logging.Formatter:
    """Simple formatter for logging

    Returns:
        Formatter for logging
    """
    fmt: str = "[%(asctime)s - %(name)-30s - %(levelname)-9s] - %(message)s"
    datefmt: str = "%H:%M:%S"
    formatter: logging.Formatter = logging.Formatter(fmt, datefmt=datefmt)

    return formatter