Tutorial

Unraveling the Secrets of the Earth’s Magnetic Field with sedprep

Welcome to the sedprep tutorial! In this notebook, we’ll journey through the process of preprocessing palaeomagnetic sediment records, focusing on an example from Sweden. sedprep offers tools to estimate and correct for various distortions in sedimentary magnetic records. Our path will guide you from preparing your data over estimating necessary parameters to applying the parameters in a final preprocessing step.

Setting the Stage

Before we dive into the parameter estimation, let’s ensure we have all the right tools in our kit. The first step in any Python adventure involves importing the necessary packages. For this tutorial, apart from the Python package sedprep, we’ll need a couple of standard libraries: numpy and pandas for handling our data and matplotlib for bringing our results to life visually. Here’s how to get started:

[1]:
import numpy as np
import pandas as pd
import requests

from sedprep.data_handling import read_arch_data
from sedprep.utils import kalmag
from sedprep.optimizer import Optimizer
cpu

The Significance of Archaeomagnetic Data and Lava Flow Samples

Before we delve into the sediment records, it’s essential to understand the role of archaeomagnetic data. sedprep utilizes archaeomagnetic data and lava flow samples as globally distributed independent magnetic information. This invaluable information, coupled with prior knowledge of the geomagnetic field, forms the backbone of our Bayesian modeling technique. By comparing sediment records to a signal reference signal influenced by the prior knowledge and the independent measurements, sedprep meticulously estimates parameters that bring clarity to the otherwise distorted magnetic signals in sediment records. Here we use the dataset from Schanner et al (2022). It is publically available using the links in the next cell.

[2]:
import io
pre = "https://nextcloud.gfz-potsdam.de/s/"
rej_response = requests.get(f"{pre}WLxDTddq663zFLP/download")
rej_response.raise_for_status()
with np.load(io.BytesIO(rej_response.content), allow_pickle=True) as fh:
    to_reject = fh["to_reject"]
data_response = requests.get(f"{pre}r6YxrrABRJjideS/download")
arch_data = read_arch_data(io.BytesIO(data_response.content), to_reject)
arch_data.head(4)
Rejected 347 outliers.
/opt/conda/envs/virtualenv/lib/python3.12/site-packages/sedprep/data_handling.py:553: UserWarning: Records with indices [   72    73    74    75    76    77    78    79    80    81   108   109
   357   358  1065  1066  1153  2402  2575  2671  2753  2756  2757  3036
  3215  3217  3218  3219  3220  3221  3222  3223  3224  3225  3503  3772
  3988  4047  4235  4415  4504  4530  4531  4655  5172  5326  5434  5496
  5551  5707  5763  5802  5805  5959  6053  6105  6647  6706  6781  6809
  7149  7580  7585  7593  7692  7702  7735  7776  7806  7826  7881  7979
  8067  8296  8327  8427  8464  8510  8670  8708  8752  9224  9653 10002
 10132 10331 10509 10604 10647 11527 11758] contain declination, but not inclination! The errors need special treatment!
To be able to use the provided data, these records have been dropped from the output.
[2]:
t dt rad colat lat lon D dD I dI F dF type UID FID
0 -12000 619.0 6371.2 70.3294 19.6706 258.5713 -10.9 1.667730 48.5 1.105071 38.6 2.0 arch_data 13128 b'\xf0\xbf\xe2\x01\xce\x1bJ,\x8ax\xc9=\x11\x82...
1 -12000 100.0 6371.2 70.5300 19.4700 205.1000 NaN 6.238796 23.3 5.730000 37.2 4.9 arch_data 3413 b'\xf0\xbf\xe2\x01\xce\x1bJ,\x8ax\xc9=\x11\x82...
2 -12000 1000.0 6371.2 47.2010 42.7990 141.3220 20.3 6.552033 71.8 2.046429 37.6 1.9 arch_data 3467 b'\xf0\xbf\xe2\x01\xce\x1bJ,\x8ax\xc9=\x11\x82...
3 -11938 163.0 6371.2 61.6830 28.3170 343.3920 1.6 1.708651 46.0 1.186929 35.6 3.7 arch_data 10712 b'\xf0\xbf\xe2\x01\xce\x1bJ,\x8ax\xc9=\x11\x82...

Preparing Sediment Data for sedprep Analysis

In this section, we focus on importing and formatting sediment data for compatibility with sedprep. We utilize a dataset from GEOMAGIA Braun et al (2015), specifically with the Location Code “FUR” and CoreID “P2”. The raw dataset is available in the FUR_P2_raw_geomagia.csv file. It’s crucial that the data is structured according to sedprep’s specifications for efficient analysis.

The sedprep package requires data to be structured with specific columns, as detailed below:

  • t: Age in calendar years

  • dt: Age uncertainties

  • depth: Depth in centimeter

  • lat: Latitude of core location

  • lon: Longitude of core location

  • D: Declination

  • I: Inclination

  • F: Relative palaeointensity

  • dD: Measurement uncertainties for Declination

  • dI: Measurement uncertainties for Inclination

  • dF: Measurement uncertainties for Relative palaeointensity

  • subs: Name of subsections

  • type: Set to “sediment” for differentiation

  • UID: Unique identifier

  • FID: SHA-256 hash

These requirements ensure that the data is appropriately prepared for the sedprep processing pipeline. For data not sourced from GEOMAGIA, custom preparation scripts may be necessary. The data preparation steps for the GEOMAGIA dataset are encapsulated within the data_preparation.py file.

The Python snippet below outlines the process for preparing the GEOMAGIA “FUR” dataset for sedprep. This includes reading the raw data, adjusting declinations, converting MAD values to measurement uncertainties using the method described in Khokhlov and Hulot, evaluating the age-depth model at given depths, and outlier removal based on unique identifiers.

[3]:
import arviz as az
from src.data_preparation import read_geomagia_data, remove_outliers, evaluate_adm
from sedprep.data_handling import adjust_declination, mad_to_alpha95, alpha95_to_dD_dI
from sedprep.constants import default_alpha95
import tempfile

remove_uids_DI = [0, 1] + [x for x in range(120, 128)]
remove_uids_F = [0, 1] + [x for x in range(120, 128)]

df = read_geomagia_data("FUR_P2")

# Adjust declination to be within -180 to 180 degrees
df["D"] = adjust_declination(df["D"])

# Convert MAD values to alpha95, then to measurement uncertainties
df["alpha_95"] = mad_to_alpha95(df["MAD"])
df["alpha_95"].where(df["alpha_95"].notna(), other=default_alpha95, inplace=True)
df["dD"], df["dI"] = alpha95_to_dD_dI(df["alpha_95"], df["I"], df["lat"])
df["type"] = "sediment"

# Evaluate age-depth model at depths
r = requests.get(f"{pre}jYd7ka4ncWeiF6e/download")
with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as temp_file:
    temp_file.write(r.content)
idata = az.from_netcdf(temp_file.name)
# idata = az.InferenceData.from_netcdf(f"./age_depth_models/FUR_P2_adm.nc")
t, dt = evaluate_adm(idata, df["depth"].values)
df["t"] = t
df["dt"] = dt

# Remove outliers
df, removed_rows_DI, removed_rows_F = remove_outliers(
    df, remove_uids_DI, remove_uids_F
)

# Assign default relative paleointensity measurement uncertainty
df["dF"] = 0.1

# Finalize data preparation by selecting and exporting the relevant columns
df.dropna(subset=["D", "I", "F"], how="all", inplace=True)
df.reset_index(drop=True, inplace=True)
sed_data = df[
    ["t", "dt", "depth", "lat", "lon", "D", "I", "F", "dD", "dI", "dF", "subs", "type", "UID", "FID"]
]
# Uncomment to save the prepared data
# sed_data.to_csv(f"dat/FUR_P2_prepared.csv", index=False)

# Load the prepared data (if previously saved)
sed_data = pd.read_csv(f"dat/FUR_P2_prepared.csv")
sed_data.head(4)
/tmp/ipykernel_1823/2632750983.py:17: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.


/opt/conda/envs/virtualenv/lib/python3.12/site-packages/arviz/data/inference_data.py:157: UserWarning: adm_pars group is not defined in the InferenceData scheme
[3]:
t dt depth lat lon D I F dD dI dF subs type UID FID
0 1760.944483 104.765082 10.6 59.3833 12.08 -12.3 74.9 1.1374 10.997890 2.865 0.1 P2 sediment 2 b'x\x0bs\xacQ\x01\x85\x87vYz\xe2Z\n\x96*U\x11g...
1 1647.214755 112.495987 16.7 59.3833 12.08 -16.3 76.6 1.1158 12.362571 2.865 0.1 P2 sediment 3 b'x\x0bs\xacQ\x01\x85\x87vYz\xe2Z\n\x96*U\x11g...
2 1585.688837 129.002795 20.0 59.3833 12.08 0.7 72.7 1.1884 9.634304 2.865 0.1 P2 sediment 4 b'x\x0bs\xacQ\x01\x85\x87vYz\xe2Z\n\x96*U\x11g...
3 1533.913598 122.801067 22.7 59.3833 12.08 -7.6 74.7 1.1576 10.857494 2.865 0.1 P2 sediment 5 b'x\x0bs\xacQ\x01\x85\x87vYz\xe2Z\n\x96*U\x11g...

The quality of RPI measurements often varies compared to directional components and their uncertainties are often unknown Roberts et al, (2013). Therefore, we estimate parameters associated to directional components and intensity separately. When estimating parameters associated to directional components we recommend removing the intensity values by replacing them with np.nan values. The same for the directional data when estimating parameters for intensities. This step is not necessary but recommend because of the different data quality. In this tutorial we will from now on only focus on the directional components. Estimating components for intensities or even estimating all parameters together can be done in an analogous way.

[4]:
sed_data["F"] = np.nan
sed_data["dF"] = np.nan

Initializing the Optimizer for Parameter Estimation

With our sediment data meticulously prepared and formatted, we progress to the core of our sedprep analysis — parameter optimization. This crucial step involves the Optimizer class, a powerful component of sedprep designed to fine-tune the parameters.

At its instantiation, the Optimizer requires the local sediment record and global archaeomagnetic data as foundational inputs. It allows for customization through optional parameters such as the choice between two or four lock-in function parameters, prior mean and covariance matrices for Bayesian inference, and the temporal bounds and resolution of the analysis.

[5]:
mean_path = "https://nextcloud.gfz-potsdam.de/s/exaT4iPjnbq2xzo/download"
cov_path = "https://nextcloud.gfz-potsdam.de/s/NcLAi6yM2mp9WDA/download"
prior_mean, prior_cov = kalmag(mean_path, cov_path)

optimizer = Optimizer(
    sed_data,                       # a single local sediment record
    arch_data,                      # global archaeomagnetic data and lava flows
    lif_params=2,                   # either 2 or 4 depending on what class of lock-in functions to optimize (default: 4)
    prior_mean=prior_mean,          # prior mean (optional)
    prior_cov=prior_cov,            # prior covariance (optional)
    delta_t=40,                     # step size for kalman filter (default: 40)
    start=2000,                     # year where the kalman filter starts (default: 2000)
    end=min(min(sed_data.t), -6000),# year where the kalman filter ends (default: -6000)
)

The Optimizer class methodically segments the provided sediment and archaeomagnetic data into analyzable chunks, accounting for the specified temporal range and resolution. This segmentation enhances the efficiency and precision of the subsequent optimization process.

Utilizing Bayesian techniques and a sophisticated Kalman filter implementation, the Optimizer estimates a set of parameters including lock-in function parameters, declination offsets, inclination shallowing factor, and relative palaeointensity calibration factor. This multi-parameter estimation is critical for correcting the various distortions that can affect sedimentary magnetic records.

The class offers two primary methods for parameter optimization: optimize_with_dlib and optimize_with_scipy. The optimize_with_dlib method employs dlib’s LIPO-TR function optimization algorithm King, D. E. which is adept at finding global optima without requiring initial guess. The second approach utilizes a wrapper for scipy’s optimization algorithms Virtanen et al which necessitate an initial guess. For both methods users must specify which parameters to optimize. Non-optimized parameters can be set to fixed values, allowing for focused optimization of specific parameters. For declination offset, inclination shallowing and calibration factor for intensity an initial guess can be calculated based on an axial dipole assumption. These values are provided by the Optimizer class using the following code.

[6]:
axial_dip_offset = optimizer.prior_mean_offsets
axial_dip_f_shallow = optimizer.prior_mean_f_shallow
print(axial_dip_offset, axial_dip_f_shallow)
{'P2': -0.6} 0.9828208988652258

Optimization with dlib

Determining an initial guess for the lock-in function parameters is less straightforward. Following the recommendations in Bohsung et al (2023), it’s advisable to use optimize_with_dlib for lock-in function parameters, holding the declination offset and inclination shallowing factor constant. Then use this global optimum together with the fixed values as initial values for a scipy optimization algorithm.

dlib_args = {
    "max_feval": 3500,
    "rtol": 1e-8,
    "max_opt": 70,
    "n_rand": 300,
}
global_bs_opt = optimizer.optimize_with_dlib(
    optimize_bs_DI=True,            # Optimize lock-in parameters for directions
    optimize_bs_F=False,            # Do not optimize lock-in parameters for intensity
    optimize_offsets=False,         # Do not optimize offsets
    optimize_f_shallow=False,       # Do not optimize shallowing factor
    optimize_cal_fac=False,         # Do not optimize calibration factor
    fixed_bs_DI=None,
    fixed_bs_F=None,
    fixed_offsets=axial_dip_offset,     # Use fixed offsets
    fixed_f_shallow=axial_dip_f_shallow, # Use fixed shallowing factor
    fixed_cal_fac=None,
    max_lid=100,                    # Set upper bound for lock-in parameters
    **dlib_args,                    # Optimization arguments for dlib
)

This code block demonstrates how to configure and execute the optimize_with_dlib method to focus on optimizing the DI lock-in function parameters, utilizing fixed values for other parameters. Optimizing parameters, is a resource-intensive task, requiring significant memory and computational power. For optimal performance, executing this process on a GPU is highly recommended due to its ability to handle parallel computations efficiently, thus speeding up the optimization.

While we abstain from executing the optimize_with_dlib optimization in real-time within this notebook, it’s valuable to present an example of what the output might look like upon successful completion of the process when run in an adequately equipped computing environment:

success: True
    fun: 94424.7547073246
        x: [ 0.000e+00  0.000e+00  0.000e+00  5.087e+01]
    nfev: 481
nfev_lipo: 481

This output snapshot provides insight into the outcome of the optimization process:

  • success: Indicates the optimization process concluded successfully.

  • fun: Represents the value of the objective function (log marginal likelihood) at the optimum.

  • x: The optimal parameter values found by the optimization process. In this context, it suggests that the optimal parameters for the lock-in functions are zeros for \(b_1\) to \(b_3\) and ~50.87 for \(b_4\).

  • nfev & nfev_lipo: Denote the number of function evaluations performed during the optimization process, providing a measure of the computational effort involved.

Optimization with scipy

The optimize_with_scipy method serves as a sophisticated wrapper for SciPy’s optimization algorithms, enabling the simultaneous optimization of all relevant parameters. This method is particularly valuable after obtaining a global optimum for lock-in function parameters using optimize_with_dlib, as it allows for further refinement of all parameters in simultaneously. The scipy optimization algorithm require initial values x0. We set x0 to the lock-in function parameters (global_bs_opt.x) obtained from the optimize_with_dlib method in the last step and the fixed values for declination offset and shallowing factor (axial_dip_offset, axial_dip_f_shallow) obtained from an axial dipole approximation.

With the initial guess in hand, the optimize_with_scipy method is poised to refine all parameters, leveraging the computational power and flexibility of SciPy’s optimization algorithms. Several optimization algorithms (e.g. “SLSQP”) offer the utilization of gradient information which can speed up the optimization. However, higher memory is needed to execute with gradient information. Therefore, it makes sense to sometimes chose an optimization algorithm that works without storing gradients (e.g. “Nelder-Mead”).

x0 = (
    list(global_bs_opt.x)
    + list(axial_dip_offset.values())
    + [axial_dip_f_shallow]
)
scipy_args = {
    "options": {"maxiter": 3500, "maxfun": 3500},
    "grad": True,       # Utilizing gradient information, if available
    "method": "SLSQP",  # Sequential Least Squares Programming (SLSQP) method
}
polished_opt = optimizer.optimize_with_scipy(
    x0=x0,
    optimize_bs_DI=True,
    optimize_bs_F=False,
    optimize_offsets=True,
    optimize_f_shallow=True,
    optimize_cal_fac=False,
    max_lid=100,
    **scipy_args,
)

Similar to the optimize_with_dlib process, the optimize_with_scipy method is computationally demanding, necessitating substantial memory and processing power. This underscores the importance of conducting these optimizations on high-performance computing platforms, ideally with GPU support, to ensure efficient execution and optimal results.

Documenting Optimization Results

Once optimization is complete, the Optimizer returns a consolidated list of optimized parameters alongside additional relevant information. To dissect this list into distinct, usable parameter values, you can employ the split_input method. This function systematically divides the aggregated parameter list into individual parameters, enabling further analysis or application of these parameters in subsequent processes.

bs_DI, bs_F, offsets, f_shallow, cal_fac = optimizer.split_input(
    polished_opt.x,# The list of optimized parameters from scipy optimization
    optimize_bs_DI=True,        # Indicating lock-in parameters for directions were optimized
    optimize_bs_F=False,        # Indicating lock-in parameters for intensity were optimized
    optimize_offsets=True,      # Indicating Offsets were optimized
    optimize_f_shallow=True,    # Indicating shallowing factor was optimized
    optimize_cal_fac=False,     # Indicating claibration factor was optimized
)

Once all parameters are estimated it’s advised to document the estimated parameters alongside the specific conditions and hyperparameters under which optimization was performed. The Optimizer class facilitates this through the write_results method, which meticulously records the optimization outcomes and parameters in a CSV format. This approach not only safeguards the results against potential loss but also establishes a clear record that can be easily shared or revisited for future reference.

optimizer.write_results(
    "results/estimated_parameters/",
    bs_DI=[b for b in bs_DI],
    bs_F=bs_F,
    offsets=offsets,
    f_shallow=f_shallow,
    cal_fac=cal_fac,
    optimizer_output=polished_opt,
    optimizer_args=scipy_args,
    max_lid=100,
    delta_t=optimizer.delta_t,
    optimizer="scipy",
)

Applying the Estimated Parameters to Preprocess the Sediment Data

The clean_data function in sedprep plays a crucial role in the final stage of preprocessing sediment data by applying the estimated parameters to calibrate and correct the records. This step is vital for ensuring the accuracy and reliability of paleomagnetic analyses, allowing researchers to draw more precise conclusions about historical geomagnetic field behavior.

This function integrates several optional preprocessing steps:

  • Applying declination offsets: Corrects declination values by applying estimated offsets, either uniformly across the entire dataset or selectively to specified subsections.

  • Unshallowing inclination: Adjusts inclination data based on an estimated shallowing factor, compensating for the common issue of inclination shallowing in sedimentary records.

  • Calibrating paleointensity: Applies the estimated calibration factor to relative paleointensity data, scaling the data to intensity values.

  • Deconvolving the signal: Utilizes lock-in function parameters to deconvolve declination and inclination and intensity signals, aiming to remove the effects of post-depositional detrital remanent magnetization (pDRM).

The next code cell contains the following steps: Reading the prepared sediment data and the results of parameter estimation from CSV files. Extracting and parsing the estimated parameters for both the four-parameter (4p) and two-parameter (2p) lock-in function cases from the results. Applying these parameters to the sediment data using the clean_data function, generating cleaned and preprocessed datasets for both cases.

[7]:
import ast
import json
from sedprep.data_handling import clean_data

preprocess_data = False     # set to True to apply estimated parameters to clean data

# Load the prepared sediment data
sed = pd.read_csv(f"dat/FUR_P2_prepared.csv")
# Load the estimated parameters from optimization results
res = pd.read_csv(f"results/estimated_parameters/FUR_P2.csv")
res = res[res.optimizer == "scipy"].reset_index(drop=True)
# Extract and parse estimated parameters for both 4-parameter and 2-parameter cases
# Use ast.literal_eval and json.loads for parsing the stored string representations
est_bs_DI_4p = ast.literal_eval(res[~res.bs_DI.isna()].bs_DI.values[0])
est_bs_F_4p = ast.literal_eval(res[~res.bs_F.isna()].bs_F.values[0])
est_offsets_4p = json.loads(res[~res.offsets.isna()].offsets.values[0].replace("'", '"'))
est_f_shallow_4p = res[~res.f_shallow.isna()].f_shallow.values[0]
est_cal_fac_4p = res[~res.cal_fac.isna()].cal_fac.values[0]
est_bs_DI_2p = ast.literal_eval(res[~res.bs_DI.isna()].bs_DI.values[1])
est_bs_F_2p = ast.literal_eval(res[~res.bs_F.isna()].bs_F.values[1])
est_offsets_2p = json.loads(res[~res.offsets.isna()].offsets.values[1].replace("'", '"'))
est_f_shallow_2p = res[~res.f_shallow.isna()].f_shallow.values[1]
est_cal_fac_2p = res[~res.cal_fac.isna()].cal_fac.values[1]
if preprocess_data:
    # Apply estimated parameters to clean the data for the 4-parameter case
    prep_df_4p = clean_data(
        sed,
        est_bs_DI_4p,
        est_bs_F_4p,
        est_offsets_4p,
        est_f_shallow_4p,
        est_cal_fac_4p,
    )
    prep_df_4p.to_csv(
        f"results/preprocessed_data/FUR_P2_preprocessed_4p.csv", index=False
    )
    # Apply estimated parameters to clean the data for the 2-parameter case
    prep_df_2p = clean_data(
        sed,
        est_bs_DI_2p,
        est_bs_F_2p,
        est_offsets_2p,
        est_f_shallow_2p,
        est_cal_fac_2p,
    )
    prep_df_2p.to_csv(
        f"results/preprocessed_data/FUR_P2_preprocessed_2p.csv", index=False
    )
else:
    prep_df_4p = pd.read_csv(f"results/preprocessed_data/FUR_P2_preprocessed_4p.csv")
    prep_df_2p = pd.read_csv(f"results/preprocessed_data/FUR_P2_preprocessed_2p.csv")

Last but not least we plot the results. The figure shows the results for the FUR_P2 record. The sediment data points, shown with uncertainties, are marked in red. The corrected sediment data are indicated in blue for the four-parameter lock-in function estimation and in purple for the two-parameter estimation. The gray lines are mean and one hundred samples from the ArchKalmag14k.r model. The two estimated lock-in functions are shown on the right: in blue for the four-parameter case and purple for the two-parameter case. The boxes on the left side detail the estimated parameters for both lock-in function scenarios.

[8]:
import matplotlib.pyplot as plt
from sedprep.utils import lif
from sedprep.plot_functions import plot_DIF
r = requests.get("https://nextcloud.gfz-potsdam.de/s/drmP3eSSg8Pq4r4/download", stream=True)
akm_file = np.load(io.BytesIO(r.raw.read()), allow_pickle=True)
# akm_file = np.load("dat/akm14k.r_samples.npz")

lif_knots = np.linspace(0, 70, 5000)
props = dict(boxstyle='round,pad=0.6', facecolor='none', alpha=0.8)

fig, axs = plt.subplot_mosaic(
    2 * [3 * ["D"] + ["."]]
    + 2 * [3 * ["D"] + ["LIF_DI"]]
    + 2 * [3 * ["I"] + ["LIF_DI"]]
    + 2 * [3 * ["I"] + ["."]]
    + 4 * [3 * ["F"] + ["LIF_F"]], figsize=(12, 6), dpi=300)
fig.align_ylabels()
plt.subplots_adjust(wspace=0.5, hspace=0.2)
axF_twin = axs["F"].twinx()
[ax.set_xticklabels([]) for ax in [axs["D"], axs["I"]]]
plot_DIF(sed, axs["D"], axs["I"], axs["F"], axF_twin, time_or_depth="depth", xerr=True, yerr=True, legend=False, model_file=akm_file)
axs["D"].legend(bbox_to_anchor=(0, 0.02), loc="lower left", ncol=3)
plot_DIF(prep_df_4p, axs["D"], axs["I"], axF_twin, time_or_depth="depth", xerr=True, yerr=True, legend=False, distinguish_subs=False)
plot_DIF(prep_df_2p, axs["D"], axs["I"], axF_twin, time_or_depth="depth", xerr=True, yerr=True, legend=False, distinguish_subs=False, color="C4")
axF_twin.set_ylabel("Intensity [uT]")
for ax, bs_4p, bs_2p in zip([axs["LIF_DI"], axs["LIF_F"]], [est_bs_DI_4p, est_bs_F_4p], [est_bs_DI_2p, est_bs_F_2p]):
    ax.plot(lif_knots, lif(bs_4p)(lif_knots), c="C0", lw=2, zorder=3, label="4p")
    ax.plot(lif_knots, lif(bs_2p)(lif_knots), c="C4", lw=2, zorder=3, label="2p")
    ax.set_yticks([], [])
axs["LIF_DI"].legend(bbox_to_anchor=(0.5, 1.35), title="Lock-in functions", loc="center", ncol=3)
offsets_text = r"$\bf{Declination\ offsets}$" + "\n"
offsets_text += f"4p: {est_offsets_4p[0]:.2f}\n"
offsets_text += f"2p: {est_offsets_2p[0]:.2f}"
f_shallow_text = r"$\bf{Shallowing\ factor}$" + "\n"
f_shallow_text += f"4p: {est_f_shallow_4p:.3f}\n"
f_shallow_text += f"2p: {est_f_shallow_2p:.3f}"
cal_fac_text = r"$\bf{Calibration\ factor}$" + "\n"
cal_fac_text += f"4p: {est_cal_fac_4p:.2f}\n"
cal_fac_text += f"2p: {est_cal_fac_2p:.2f}"
axs["D"].text(-0.25, 0.5, offsets_text, transform=axs["D"].transAxes, va="center", ha="center", bbox=props)
axs["I"].text(-0.25, 0.5, f_shallow_text, transform=axs["I"].transAxes, va="center", ha="center", bbox=props)
axs["F"].text(-0.25, 0.5, cal_fac_text, transform=axs["F"].transAxes, va="center", ha="center", bbox=props)
# fig.suptitle(f"Results for {name}", x=0, fontweight="bold")
plt.show()
../_images/tutorial_tutorial_23_0.png

Finally, we also present the results for a second Examples called BIR. For details see Bohsung

[9]:
sed = pd.read_csv(f"dat/BIR_prepared.csv")

res = pd.read_csv(f"results/estimated_parameters/BIR.csv")
res = res[res.optimizer == "scipy"].reset_index(drop=True)
est_bs_DI_4p = ast.literal_eval(res[~res.bs_DI.isna()].bs_DI.values[0])
est_bs_F_4p = ast.literal_eval(res[~res.bs_F.isna()].bs_F.values[0])
est_offsets_4p = json.loads(res[~res.offsets.isna()].offsets.values[0].replace("'", '"'))
est_f_shallow_4p = res[~res.f_shallow.isna()].f_shallow.values[0]
est_cal_fac_4p = res[~res.cal_fac.isna()].cal_fac.values[0]
est_bs_DI_2p = ast.literal_eval(res[~res.bs_DI.isna()].bs_DI.values[1])
est_bs_F_2p = ast.literal_eval(res[~res.bs_F.isna()].bs_F.values[1])
est_offsets_2p = json.loads(res[~res.offsets.isna()].offsets.values[1].replace("'", '"'))
est_f_shallow_2p = res[~res.f_shallow.isna()].f_shallow.values[1]
est_cal_fac_2p = res[~res.cal_fac.isna()].cal_fac.values[1]

prep_df_4p = pd.read_csv(f"results/preprocessed_data/BIR_preprocessed_4p.csv")
prep_df_2p = pd.read_csv(f"results/preprocessed_data/BIR_preprocessed_2p.csv")

fig, axs = plt.subplot_mosaic(
    2 * [3 * ["D"] + ["."]]
    + 2 * [3 * ["D"] + ["LIF_DI"]]
    + 2 * [3 * ["I"] + ["LIF_DI"]]
    + 2 * [3 * ["I"] + ["."]], figsize=(12, 4), dpi=300)
fig.align_ylabels()
plt.subplots_adjust(wspace=0.5, hspace=0.2)
axs["D"].set_xticklabels([])
plot_DIF(sed, axs["D"], axs["I"], time_or_depth="depth", xerr=True, yerr=True, legend=False, model_file=akm_file)
axs["D"].legend(bbox_to_anchor=(0, 0.98), loc="upper left", ncol=2)
plot_DIF(prep_df_4p, axs["D"], axs["I"], time_or_depth="depth", xerr=True, yerr=True, legend=False, distinguish_subs=False)
plot_DIF(prep_df_2p, axs["D"], axs["I"], time_or_depth="depth", xerr=True, yerr=True, legend=False, distinguish_subs=False, color="C4")
axs["LIF_DI"].plot(lif_knots, lif(est_bs_DI_4p)(lif_knots), c="C0", lw=2, zorder=3, label="4p")
axs["LIF_DI"].plot(lif_knots, lif(est_bs_DI_2p)(lif_knots), c="C4", lw=2, zorder=3, label="2p")
axs["LIF_DI"].set_yticks([], [])
axs["LIF_DI"].legend(bbox_to_anchor=(0.5, 1.35), title="Lock-in functions", loc="center", ncol=3)
offsets_text = r"$\bf{Declination\ offsets}$" + "\n"
offsets_text += f"4p: 1: {est_offsets_4p['BIR2-1']:.2f}; 2: {est_offsets_4p['BIR2-2']:.2f}; 3: {est_offsets_4p['BIR2-3']:.2f}\n"
offsets_text += f"2p: 1: {est_offsets_2p['BIR2-1']:.2f}; 2: {est_offsets_2p['BIR2-2']:.2f}; 3: {est_offsets_2p['BIR2-3']:.2f}"
f_shallow_text = r"$\bf{Shallowing\ factor}$" + "\n"
f_shallow_text += f"4p: {est_f_shallow_4p:.3f}\n"
f_shallow_text += f"2p: {est_f_shallow_2p:.3f}"
axs["D"].text(-0.25, 0.5, offsets_text, transform=axs["D"].transAxes, va="center", ha="center", bbox=props)
axs["I"].text(-0.25, 0.5, f_shallow_text, transform=axs["I"].transAxes, va="center", ha="center", bbox=props)
# fig.suptitle(f"Results for {name}", x=0, fontweight="bold")
plt.show()
../_images/tutorial_tutorial_25_0.png

The file that was used to get the results presented here can be found in “src/optimize.py”.

The link to download the age-depth model of BIR is “https://nextcloud.gfz-potsdam.de/s/naGec8zgSmNbE2P/download”.