Generate Synthetic Data for Shared-Parameter Fitting#

Generate multiple datasets that share the same spectral model but differ in the amplitude of the time-dependent peak shift. This simulates a common experimental scenario where identical samples are measured at different pump fluences or excitation intensities.

Ground truth model:

  • GLP peak + linear background (LinBack)

  • Time dependence: exponential shift of peak position (expFun + gaussCONV IRF)

  • 6 datasets with expFun A: [5, 2, 1, 0.5, 0.2, 0.1]

  • All other parameters identical across files

import os
import numpy as np
import trspecfit

1. Define Ground Truth Model#

Build a 2D model (energy + time) from YAML files. The models_energy_truth.yaml and models_time_truth.yaml files define the ground truth parameters.

project = trspecfit.Project(path=os.getcwd())

energy = np.arange(0, 20, 0.1)
time = np.concatenate([
    np.arange(-20, 5, 0.5),   # fine steps around t0
    np.arange(5, 100, 2.0),   # coarser for the decay tail
])

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

# Load energy model
file.load_model(
    model_yaml='models_energy_truth.yaml',
    model_info='single_peak',
)

# Add time dependence to peak position
file.add_time_dependence(
    target_model='single_peak',
    target_parameter='GLP_01_x0',
    dynamics_yaml='models_time_truth.yaml',
    dynamics_model=['MonoExpPosIRF'],
)

file.describe_model()
Config file <repo>/examples/fitting_workflows/21_multi_file_shared_fit/data/project.yaml not found, using defaults
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF model
model name: single_peak
function: LinBack 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                       10        5       15     None     True     None     None
GLP_01_F                        1     0.75      2.5     None     True     None     None
GLP_01_m                      0.3        0        1     None     True     None     None
GLP_01_x0                      10        5       15     None     True     None     None
GLP_01_x0_expFun_01_A           5        0       15     None     True     None     None
GLP_01_x0_expFun_01_t0          0     -inf      inf     None    False     None     None
GLP_01_x0_expFun_01_tau        50        1      100     None     True     None     None
GLP_01_x0_gaussCONV_SD          3    1e-06       10     None     True     None     None
LinBack_b                       0       -5        5     None     True     None     None
LinBack_m                     0.4       -1        1     None     True     None     None
LinBack_xStart                  0     -inf      inf     None    False     None     None
LinBack_xStop                  20     -inf      inf     None    False     None     None

2. Generate Datasets at Different Shift Amplitudes#

Each amplitude is its own dynamics entry in models_time_truth.yaml (the YAML is the source of truth; a variant is another entry). For each one, reload the energy model, attach that entry, simulate noisy data, and save to CSV. The axes are shared across all files.

amplitudes = [5, 2, 1, 0.5, 0.2, 0.1]

# Save shared axes (once)
np.savetxt('energy.csv', energy)
np.savetxt('time.csv', time)

for i, amp in enumerate(amplitudes, start=1):
    # The shift amplitude is a dynamics entry of its own: reload the
    # energy model and attach the entry for this amplitude
    file.load_model(
        model_yaml='models_energy_truth.yaml',
        model_info='single_peak',
    )
    file.add_time_dependence(
        target_model='single_peak',
        target_parameter='GLP_01_x0',
        dynamics_yaml='models_time_truth.yaml',
        dynamics_model=[f'MonoExpPosIRF_A{amp:g}'.replace('.', 'p')],
    )
    model = file.model_active

    # Simulate with photon counting noise
    sim = trspecfit.Simulator(
        model=model,
        detection='photon_counting',
        counts_per_delay=5000,
        seed=42 + i,
    )
    _clean, noisy, _noise = sim.simulate_2d()

    # Save data
    np.savetxt(f'data_{i}.csv', noisy, delimiter=',')
    print(f'data_{i}.csv  |  expFun A = {amp:6.3f}  |  shape {noisy.shape}')
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF_A5 model
data_1.csv  |  expFun A =  5.000  |  shape (98, 200)
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF_A2 model
data_2.csv  |  expFun A =  2.000  |  shape (98, 200)
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF_A1 model
data_3.csv  |  expFun A =  1.000  |  shape (98, 200)
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF_A0p5 model
data_4.csv  |  expFun A =  0.500  |  shape (98, 200)
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF_A0p2 model
data_5.csv  |  expFun A =  0.200  |  shape (98, 200)
Loading model to describe energy- (and time-)dependent data: single_peak
Loading model to describe time-dependence of a model parameter: GLP_01_x0 of MonoExpPosIRF_A0p1 model
data_6.csv  |  expFun A =  0.100  |  shape (98, 200)
<cell>:10: UserWarning: Model "single_peak" already exists on file 'test' — replacing the live model object. Completed fits are unaffected (they are kept in the fit history).
  file.load_model(

3. Quick Visual Check#

Plot the strongest and weakest datasets side by side.

import matplotlib.pyplot as plt

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

d1 = np.loadtxt('data_1.csv', delimiter=',')
d6 = np.loadtxt('data_6.csv', delimiter=',')

im0 = axes[0].pcolormesh(energy, time, d1, shading='auto')
axes[0].set_title(f'expFun A = {amplitudes[0]}')
axes[0].set_xlabel('Energy')
axes[0].set_ylabel('Time')
plt.colorbar(im0, ax=axes[0])

im1 = axes[1].pcolormesh(energy, time, d6, shading='auto')
axes[1].set_title(f'expFun A = {amplitudes[-1]}')
axes[1].set_xlabel('Energy')
axes[1].set_ylabel('Time')
plt.colorbar(im1, ax=axes[1])

plt.tight_layout()
plt.show()
../../../../_images/c5797d4c9facf566be9a0bd3d60973bd7b2b3d3407a9ce5e84c8fc57aaea5205.png

Ground Truth Parameters#

Parameter

Value

Description

LinBack m

0.4

Background slope

LinBack b

0.0

Background intercept

LinBack xStart

0

Background start

LinBack xStop

20

Background stop

GLP A

10

Peak amplitude

GLP x0

10

Peak center

GLP F

1.0

Peak width

GLP m

0.3

Mixing (Gauss/Lorentz)

gaussCONV SD

3

IRF width

expFun A

varies

Shift amplitude

expFun tau

50

Decay time constant

expFun t0

0

Time zero

Model files#

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

models_energy_truth.yaml#

# ground truth model for data generation
# parameters: [value, vary, min, max]

single_peak:
    LinBack:
      m: [4E-1, True, -1, 1]
      b: [0, True, -5, 5]
      xStart: [0, False]
      xStop: [20, False]
    GLP:
      A: [10, True, 5, 15]
      x0: [10, True, 5, 15]
      F: [1.0, True, 0.75, 2.5]
      m: [0.3, True, 0, 1]

models_time_truth.yaml#

# ground truth time model for data generation

MonoExpPosIRF:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [5, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]

# MonoExpPosIRF with the shift amplitude at 5: one entry per dataset
MonoExpPosIRF_A5:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [5, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]

# MonoExpPosIRF with the shift amplitude at 2: one entry per dataset
MonoExpPosIRF_A2:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [2, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]

# MonoExpPosIRF with the shift amplitude at 1: one entry per dataset
MonoExpPosIRF_A1:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [1, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]

# MonoExpPosIRF with the shift amplitude at 0.5: one entry per dataset
MonoExpPosIRF_A0p5:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [0.5, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]

# MonoExpPosIRF with the shift amplitude at 0.2: one entry per dataset
MonoExpPosIRF_A0p2:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [0.2, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]

# MonoExpPosIRF with the shift amplitude at 0.1: one entry per dataset
MonoExpPosIRF_A0p1:
    gaussCONV:
        SD: [3, True, 1.0E-6, 10]
    expFun:
        A: [0.1, True, 0, 15]
        tau: [50, True, 1, 100]
        t0: [0, False]