Generate Synthetic Data for the Parameter-Profiles Example#

Produces the data.csv / energy.csv / time.csv / aux_axis.csv used by ../example.ipynb.
The data mimics time-resolved XPS of a semiconductor core level: a single GLP peak on a flat background, measured as a depth average. XPS is surface sensitive, so each depth layer contributes with an exponential IMFP weight, and band bending shifts the peak position linearly with depth. After photoexcitation at t = 0, a surface photovoltage collapses the band bending to flat-band condition; it recovers exponentially over ~100 ps.

Ground truth model

  • Offset + GLP peak — see models_energy_truth.yaml

  • Depth profiles (models_profile_truth.yaml): IMFP weighting on GLP_01_A (pExpDecay), band bending shift on GLP_01_x0 (pLinear)

  • Time dependence on the band bending gradient GLP_01_x0_pLinear_01_m: BandBendingRecovery — see models_time_truth.yaml

Re-run this notebook to regenerate the data in place.

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import trspecfit

1. Define the Ground Truth Model#

Build the truth model on the same axes the example fits over. The *_truth.yaml files hold the ground truth parameters. The two profiles attach to the GLP parameters; the dynamics attaches to the profile’s gradient parameter, so the depth profile re-shapes at every time step (spectral diffusion).

project = trspecfit.Project(path=Path.cwd(), config_file=None)

# Save the axes first and reload them, so the simulation runs on bit-identical
# axes to what ../example.ipynb later loads (CSV rounding would otherwise
# introduce tiny axis differences between generation and fitting).
data_fmt = '%.6e'
np.savetxt('energy.csv', np.arange(95, 102, 0.025), fmt=data_fmt)  # 280 points (eV)
np.savetxt('time.csv', np.arange(-50, 300, 2), fmt=data_fmt)       # 175 points (ps)
np.savetxt('aux_axis.csv', np.arange(0, 10, 0.2), fmt=data_fmt)    # 50 points (nm)
energy = np.loadtxt('energy.csv')
time = np.loadtxt('time.csv')
depth = np.loadtxt('aux_axis.csv')

file = trspecfit.File(
    parent_project=project,
    energy=energy,
    time=time,
    aux_axis=depth,
)

# Load the ground truth energy model, attach the two depth profiles, and make
# the band bending gradient time-dependent.
file.load_model(model_yaml='models_energy_truth.yaml', model_info='truth')
file.add_par_profile(
    target_model='truth',
    target_parameter='GLP_01_A',
    profile_yaml='models_profile_truth.yaml',
    profile_model='IMFP',
)
file.add_par_profile(
    target_model='truth',
    target_parameter='GLP_01_x0',
    profile_yaml='models_profile_truth.yaml',
    profile_model='BandBending',
)
file.add_time_dependence(
    target_model='truth',
    target_parameter='GLP_01_x0_pLinear_01_m',
    dynamics_yaml='models_time_truth.yaml',
    dynamics_model='BandBendingRecovery',
)

file.describe_model()
Loading model to describe energy- (and time-)dependent data: truth
Loading profile model for parameter: GLP_01_A of IMFP model
Loading profile model for parameter: GLP_01_x0 of BandBending model
Loading model to describe time-dependence of a model parameter: GLP_01_x0_pLinear_01_m of BandBendingRecovery model
model name: truth
function: Offset from trspecfit.functions.energy
function: GLP from trspecfit.functions.energy
all lmfit.Parameters() [flattened and sorted alphabetically]:
Name                                     Value      Min      Max   Stderr     Vary     Expr Brute_Step
GLP_01_A                                     0     -inf      inf     None    False     None     None
GLP_01_A_pExpDecay_01_A                    100     -inf      inf     None     True     None     None
GLP_01_A_pExpDecay_01_tau                    2     -inf      inf     None     True     None     None
GLP_01_F                                   1.2     -inf      inf     None     True     None     None
GLP_01_m                                   0.3     -inf      inf     None     True     None     None
GLP_01_x0                                 99.5     -inf      inf     None     True     None     None
GLP_01_x0_pLinear_01_b                       0     -inf      inf     None    False     None     None
GLP_01_x0_pLinear_01_m                    -0.5     -inf      inf     None     True     None     None
GLP_01_x0_pLinear_01_m_expFun_01_A         0.5     -inf      inf     None     True     None     None
GLP_01_x0_pLinear_01_m_expFun_01_t0          0     -inf      inf     None     True     None     None
GLP_01_x0_pLinear_01_m_expFun_01_tau       100     -inf      inf     None     True     None     None
GLP_01_x0_pLinear_01_m_gaussCONV_SD         10    1e-06      100     None     True     None     None
Offset_y0                                  0.5     -inf      inf     None     True     None     None

2. Simulate and Save#

Photon-counting detection draws each pixel from a Poisson distribution, so the noise is signal-dependent (shot noise) — the realistic regime for counting detectors. Lower counts_per_delay ⇒ noisier data.

sim = trspecfit.Simulator(
    model=file.model_active,
    detection='photon_counting',
    counts_per_delay=20000,
    seed=42,
)
clean, noisy, noise = sim.simulate_2d()

# Save noisy data (data is [time, energy], matching File's expectation).
# The axes were already saved (and reloaded) above.
np.savetxt('data.csv', noisy, delimiter=',', fmt=data_fmt)

snr = clean.max() / np.std(noise)
print(f'data.csv  |  shape {noisy.shape}  |  peak SNR ~ {snr:.1f}')
data.csv  |  shape (175, 280)  |  peak SNR ~ 40.4

3. Quick Visual Check#

In the 2D map, look at t = 0: the peak jumps toward higher binding energy, grows, and sharpens — at flat-band condition all depth layers line up at the surface binding energy — then relaxes back over ~100 ps. The 1D slices compare the equilibrium line shape (broadened and skewed by band bending) with the near-flat-band one.

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

im = axes[0].pcolormesh(energy, time, noisy, shading='auto')
axes[0].set_xlabel('Binding energy (eV)')
axes[0].set_ylabel('Time (ps)')
axes[0].set_title('Simulated 2D data')
axes[0].invert_xaxis()  # XPS convention: high binding energy on the left
plt.colorbar(im, ax=axes[0])

t_ind = np.abs(time - 10).argmin()
axes[1].plot(energy, noisy[t_ind], lw=0.8, label='noisy (t = 10 ps)')
axes[1].plot(energy, clean[t_ind], lw=2.0, label='clean (t = 10 ps)')
axes[1].plot(energy, clean[:10].mean(axis=0), lw=2.0, ls='--',
             label='clean (equilibrium)')
axes[1].set_xlabel('Binding energy (eV)')
axes[1].set_ylabel('Intensity')
axes[1].set_title('Near-flat-band vs equilibrium line shape')
axes[1].invert_xaxis()
axes[1].legend()

plt.tight_layout()
plt.show()
../../../../_images/5b189fd4c7fa15884e6053c638bdd4bdfcaa65afc5f6b1f16db98b1af40d54d4.png

Ground Truth Parameters#

Parameter

Value

Description

Offset y0

0.5

Flat background

GLP A

0 (fixed)

Amplitude comes entirely from the IMFP profile

GLP x0

99.5 eV

Surface binding energy

GLP F

1.2 eV

Peak FWHM

GLP m

0.3

Gaussian/Lorentzian mixing

pExpDecay A

100

Surface spectral weight (IMFP profile)

pExpDecay tau

2.0 nm

IMFP — fixed in the fits, known from tables

pLinear m

−0.5 eV/nm

Equilibrium band bending gradient

pLinear b

0

Profile is a pure shift relative to x0

gaussCONV SD

10 ps

IRF width

expFun A

+0.5 eV/nm

Photovoltage collapse (flat-band at t = 0)

expFun tau

100 ps

Band bending recovery time constant

Model files#

The YAML files next to this notebook, as shipped with the example.

models_energy_truth.yaml#

# ground truth energy model for data generation
# (same structure as the profile-aware fit models in ../models_energy.yaml)

# Single core-level peak on a flat background. A is 0 because the IMFP depth
# profile (data/models_profile_truth.yaml) provides the full amplitude;
# x0 is the *surface* binding energy — the band bending profile adds the
# depth-dependent shift.
truth:
    Offset:
        y0: [0.5, True]
    GLP:
        A: [0, False]
        x0: [99.5, True]
        F: [1.2, True]
        m: [0.3, True]

models_profile_truth.yaml#

# ground truth profile models for data generation
# Both profiles are evaluated over the depth axis (data/aux_axis.csv, nm)
# and the spectra are averaged uniformly over that axis.

# IMFP weighting, attached to GLP_01_A:
# signal from depth z contributes with weight A * exp(-z / tau),
# where tau is the inelastic mean free path (IMFP).
IMFP:
    pExpDecay:
        A: [100, True]
        tau: [2.0, True]

# Band bending, attached to GLP_01_x0:
# the core level shifts linearly with depth, x0(z) = x0_surface + m * z.
# m < 0: higher binding energy at the surface (upward band bending).
BandBending:
    pLinear:
        m: [-0.5, True]
        b: [0, False]

models_time_truth.yaml#

# ground truth time model for data generation
# Attached to the band bending gradient GLP_01_x0_pLinear_01_m — a parameter
# of the *profile* model, not of the energy model (spectral diffusion).

# Photoexcitation creates a surface photovoltage that collapses the
# equilibrium band bending (m jumps by +A, i.e. less negative), then recovers
# exponentially with time constant tau. The step is blurred by a Gaussian IRF.
# A = 0.5 exactly cancels the equilibrium gradient m = -0.5 at t = 0:
# the bands are driven all the way to flat-band condition (SPV saturation).
BandBendingRecovery:
    gaussCONV:
        SD: [10, True, 1.0E-6, 100]
    expFun:
        A: [0.5, True]
        tau: [100, True]
        t0: [0, True]