3.1. Basic fitting for hyperfine beat (stage 1 bootstrap)#

For basic fitting, try a stage 1 style bootstrap. In this case, options are:

  • “basic” ignore the photoionization dynamics and just try fitting the beat to the \(l=4\), ROI=0 case, since it is already pretty close and may be assumed to be directly mapped here.

  • “advanced” set (arbitrary) parameters per final state for the probe, and fit these plus the hyperfine beat model parameters. This should allow for a match to a single set of hyperfine parameters for all observables, and fulfil the stage 1 bootstrap criteria.


From prior work and data:

3.1.1. Setup fitting model#

Follow the modelling notebook (Hyperfine beat and (electronic) alignment modelling), but wrap functions for fitting.

New functions are in qbanalysis.basic_fitting.py.

3.1.1.1. Imports#

# Load packages
# Main functions used herein from qbanalysis.hyperfine
from qbanalysis.hyperfine import *
import numpy as np
from epsproc.sphCalc import setBLMs

from pathlib import Path

dataPath = Path('/tmp/xe_analysis')
# dataTypes = ['BLMall', 'BLMerr', 'BLMerrCycle']   # Read these types, should just do dir scan here.

# # Read from HDF5/NetCDF files
# # TO FIX: this should be identical to loadFinalDataset(dataPath), but gives slightly different plots - possibly complex/real/abs confusion?
# dataDict = {}
# for item in dataTypes:
#     dataDict[item] = IO.readXarray(fileName=f'Xe_dataset_{item}.nc', filePath=dataPath.as_posix()).real
#     dataDict[item].name = item

# Read from raw data files
from qbanalysis.dataset import loadFinalDataset
dataDict = loadFinalDataset(dataPath)

# Use Pandas and load Xe local data (ODS)
# These values were detemermined from the experimental data as detailed in ref. [4].
from qbanalysis.dataset import loadXeProps
xeProps = loadXeProps()
2025-06-03 16:50:55.544 | INFO     | qbanalysis.config:<module>:11 - PROJ_ROOT path is: /home/runner/work/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV
* sparse not found, sparse matrix forms not available. 
* natsort not found, some sorting functions not available. 
* Setting plotter defaults with epsproc.basicPlotters.setPlotters(). Run directly to modify, or change options in local env.
* Set Holoviews with bokeh.
* pyevtk not found, VTK export not available. 
* pyshtools not found, SHtools functions not available. If required, run "pip install pyshtools" or "conda install -c conda-forge pyshtools" to install.
2025-06-03 16:51:16.897 | INFO     | qbanalysis.hyperfine:<module>:31 - Using uncertainties modules, Sympy maths functions will be forced to float outputs.
2025-06-03 16:51:16.973 | INFO     | qbanalysis.dataset:loadDataset:268 - Loaded data cpBasex_results_cycleSummed_rot90_quad1_ROI_results_with_FT_NFFT1024_hanningWindow_270717.mat.
2025-06-03 16:51:17.022 | INFO     | qbanalysis.dataset:loadDataset:268 - Loaded data cpBasex_results_allCycles_ROIs_with_FTs_NFFT1024_hanningWindow_270717.mat.
2025-06-03 16:51:17.344 | INFO     | qbanalysis.dataset:loadFinalDataset:244 - Processed data to Xarray OK.
2025-06-03 16:51:17.373 | INFO     | qbanalysis.dataset:loadXeProps:71 - Loaded Xe data from /home/runner/work/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/dataLocal/Xe_data_table_fixedFractions.ods.

**Xe measured level splittings and the hyperfine constants.**
Statistical uncertainty estimates are given for the measurements. (See manuscript for details).
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/xarray/core/concat.py:500: FutureWarning: unique with argument that is not not a Series, Index, ExtensionArray, or np.ndarray is deprecated and will raise in a future version.
  common_dims = tuple(pd.unique([d for v in vars for d in v.dims]))
/home/runner/work/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/qbanalysis/dataset.py:82: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  tidied = rawXeHyperfineResults.applymap(lambda x: x.replace(' ','') if isinstance(x, str) else x)
/home/runner/work/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV/qbanalysis/dataset.py:87: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  tidied[uList] = tidied[uList].applymap(lambda x: ufloat_fromstr(x))  # OK
A/MHz B/MHz Splitting/cm−1
Isotope I F F′
129 0.5 0.5 1.5 -5723+/-9 nan+/-nan 0.2863+/-0.0005
131 1.5 1.5 0.5 1697+/-30 -8+/-7 0.0855+/-0.0010
2.5 1.5 1697+/-30 -8+/-7 0.1411+/-0.0029
0.5 1697+/-30 -8+/-7 0.2276+/-0.0029

3.1.1.2. Init parameters & test#

Here use xeProps to set and define fit paramters. Note in the original work the splittings were determined by FT of the data, and A, B parameters via Eqn. 2 therein.

# Set splittings
fitParamsCol = 'Splitting/cm−1'
xePropsFit = xeProps.copy()

xeSplittings = xePropsFit[fitParamsCol].to_numpy()
Hide code cell content
# # Test beat model with changed params...
# xeSplittings = np.random.randn(4)
# xeSplittings
Hide code cell content
# xePropsFit[fitParamsCol] = 0.1*np.abs(xeSplittings)
# xePropsFit
Hide code cell content
# # OPTIONAL: Test beat model with changed params...

# # Set arb params
# xeSplittings = np.random.randn(4)
# xePropsFit[fitParamsCol] = 0.1*np.abs(xeSplittings)

# # Compute model with new params
# modelDict = computeModel(xePropsFit)
# modelDictSum, modelDA = computeModelSum(modelDict)

# # Plot model
# plotOpts = {'width':800}
# modelDA = stackModelToDA(modelDictSum)
# plotHyperfineModel(modelDA, **plotOpts).opts(title="Isotope comparison + sum")

3.1.2. Run fits with Scipy Least Squares#

Use the wrapper :py:func:qbanalysis.basic_fitting.calcFitModel() with scipy.optimize.least_squares. The wrapper uses computeModelSum() as above, and computes the residuals.

For the basic case, no ionization model is included, so this fit is only to see how well the form of the hyperfine beat can be matched to the \(K=4\) case for ROI 0, and how much the level splittings are modified from the previous case (determined by FT).

Hide code cell content
# Hide future warnings from Xarray concat for fitting on some platforms
import warnings
# warnings.filterwarnings('ignore')  # ALL WARNINGS
# warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=FutureWarning)
# Import functions
from qbanalysis.basic_fitting import *
import scipy

#*** Init params - either random or from previous best
# NOTE: this needs to be a 1D Numpy array.
# x0 = np.abs(np.random.random(4))  # Randomise inputs

# Seed with existing params - note this can't be Uncertainties objects
xePropsFit = xeProps.copy()
x0 = unumpy.nominal_values(xePropsFit[fitParamsCol].to_numpy())  # Test with previous vals

#*** Run a fit
fitOut = scipy.optimize.least_squares(calcBasicFitModel, x0, bounds = (0.01,0.5), verbose = 2,
                                      kwargs = {'xePropsFit':xePropsFit, 'dataDict':dataDict})
fitOut.success
2025-06-03 16:51:17.419 | INFO     | qbanalysis.basic_fitting:<module>:21 - Using uncertainties modules, Sympy maths functions will be forced to float outputs.
   Iteration     Total nfev        Cost      Cost reduction    Step norm     Optimality   
       0              1         1.8508e-04                                    2.08e-02    
       1              2         4.0425e-05      1.45e-04       2.74e-03       3.53e-03    
       2              3         2.5587e-05      1.48e-05       1.63e-03       7.16e-04    
       3              4         2.3896e-05      1.69e-06       1.12e-03       2.73e-04    
       4              9         2.3837e-05      5.90e-08       9.90e-04       1.06e-04    
       5             11         2.3743e-05      9.33e-08       4.70e-04       2.70e-05    
       6             13         2.3727e-05      1.64e-08       2.33e-04       9.02e-06    
       7             15         2.3725e-05      1.51e-09       1.17e-04       1.75e-06    
       8             17         2.3725e-05      5.60e-11       1.69e-05       1.10e-06    
       9             19         2.3725e-05      8.83e-13       3.91e-06       4.19e-07    
      10             21         2.3725e-05      1.22e-12       1.89e-06       9.73e-08    
      11             23         2.3725e-05      1.80e-13       8.94e-07       2.21e-08    
`ftol` termination condition is satisfied.
Function evaluations 23, initial cost 1.8508e-04, final cost 2.3725e-05, first-order optimality 2.21e-08.
True
# Using Scipy, the fit details are in fitOut, and results in fitOut.x
fitOut.x
array([0.29126303, 0.08466675, 0.1411    , 0.22949392])
fitOut
     message: `ftol` termination condition is satisfied.
     success: True
      status: 2
         fun: [ 8.541e-07  2.215e-05 ...  3.658e-04  5.021e-04]
           x: [ 2.913e-01  8.467e-02  1.411e-01  2.295e-01]
        cost: 2.3725378584475683e-05
         jac: [[ 0.000e+00  0.000e+00  0.000e+00  0.000e+00]
               [-9.299e-04  2.320e-04  0.000e+00 -5.137e-04]
               ...
               [ 5.836e-01 -3.171e-01  0.000e+00  2.142e-01]
               [ 7.487e-01 -3.187e-01  0.000e+00  9.719e-02]]
        grad: [-4.296e-09 -5.315e-08  0.000e+00  4.667e-09]
  optimality: 2.2076459731634488e-08
 active_mask: [0 0 0 0]
        nfev: 23
        njev: 12
# Check results - run model again with best fits
plotOpts = {'width':800}
xePropsFit, modelFit, modelFitSum, modelIn, dataIn, res = calcBasicFitModel(fitOut.x, xePropsFit, dataDict, fitFlag=False)

# Fitted model & components
# (plotHyperfineModel(modelFit['129Xe'], **plotOpts) * plotHyperfineModel(modelFit['131Xe'], **plotOpts) * plotHyperfineModel(modelFitSum, **plotOpts)).opts(title="Isotope comparison + sum")

# Compare fit results with dataset
from qbanalysis.plots import plotFinalDatasetBLMt
# plotFinalDatasetBLMt(**dataDict, **plotOpts) * plotHyperfineModel(modelFitSum, **plotOpts).select(K=2).opts(**plotOpts)
(plotHyperfineModel(modelFitSum, **plotOpts,).select(K=2).opts(**plotOpts) * plotFinalDatasetBLMt(**dataDict, **plotOpts).select(l=4)).opts(title="Fit (K=2) plus data (l=4)")
# Check new results vs. reference case...
compareResults(xeProps, xePropsFit)
original fit diff
Isotope I F F′
129 0.5 0.5 1.5 0.2863+/-0.0005 0.291263 -0.004963
131 1.5 1.5 0.5 0.0855+/-0.0010 0.084667 0.000833
2.5 1.5 0.1411+/-0.0029 0.144827 -0.003727
0.5 0.2276+/-0.0029 0.229494 -0.001894

Here we can see that - as expected - the fit is pretty good for the \(l=4\), \(ROI=0\) data. The fitted values are slightly different to the previous results (obtained via FT of the time-domain data).

3.1.3. Determine A & B parameters#

To further compare these new splittings with the previous results and literature, the A & B hyperfine parameters can be determined.

From the measurements, the hyperfine coupling constants can be determined by fitting to the usual form (see, e.g., ref. \cite{D_Amico_1999}):

(3.1)#\[\begin{equation} \Delta E_{(F,F-1)}=AF+\frac{3}{2}BF\left(\frac{F^{2}+\frac{1}{2}-J(J+1)-I(I+1)}{IJ(2J-1)(2I-1)}\right) \end{equation}\]

Note, for \(^{129}\rm{Xe}\), \(\Delta E_{(F,F-1)}=AF\) only (\(B=0\)).

# Extract parameters from fit results (splittings)
# This again uses scipy.optimize.least_squares() under the hood.
xePropsFit = extractABParams(xePropsFit)
xePropsFit.style.set_caption("Updated results")
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/scipy/optimize/_lsq/least_squares.py:825: UserWarning: Setting `gtol` below the machine epsilon (2.22e-16) effectively disables the corresponding termination condition.
  ftol, xtol, gtol = check_tolerance(ftol, xtol, gtol, method)
`gtol` termination condition is satisfied.
Function evaluations 17, initial cost 1.0194e-06, final cost 9.7941e-26, first-order optimality 1.42e-19.
Updated results
        A/MHz B/MHz Splitting/cm−1
Isotope I F F′      
129 0.500000 0.500000 1.500000 -5821.230642 nan+/-nan 0.291263
131 1.500000 1.500000 0.500000 1729.302016 37.134330 0.084667
2.500000 1.500000 1729.302016 37.134330 0.144827
0.500000 1729.302016 37.134330 0.229494

3.1.4. Compare results with previous values & literature#

3.1.4.1. Diffs from previous values#

# Check new results vs. reference case...
# TODO: neater comparison!
display(compareResults(xeProps, xePropsFit, fitParamsCol="A/MHz").style.set_caption("Comparison: A/MHz"))
display(compareResults(xeProps, xePropsFit, fitParamsCol="B/MHz").style.set_caption("Comparison: B/MHz"))
Comparison: A/MHz
        original fit diff
Isotope I F F′      
129 0.500000 0.500000 1.500000 -5723+/-9 -5821.230642 98.230642
131 1.500000 1.500000 0.500000 1697+/-30 1729.302016 -32.302016
2.500000 1.500000 1697+/-30 1729.302016 -32.302016
0.500000 1697+/-30 1729.302016 -32.302016
Comparison: B/MHz
        original fit diff
Isotope I F F′      
129 0.500000 0.500000 1.500000 nan+/-nan nan+/-nan nan
131 1.500000 1.500000 0.500000 -8+/-7 37.134330 -45.134330
2.500000 1.500000 -8+/-7 37.134330 -45.134330
0.500000 -8+/-7 37.134330 -45.134330

3.1.4.2. Check vs. literature values#

These values can be compared with the previous case (as above), and literature values, per Table 1 in the previous manuscript.

xePropsFit.droplevel(['I','F′','F'])[0:2][['A/MHz','B/MHz']]
A/MHz B/MHz
Isotope
129 -5821.230642 nan+/-nan
131 1729.302016 37.13433

Xe table

Here it is clear that the new values are much closer to the previous literature values, although all are slightly larger. In this case there is also no error propagation in the fitting, which will be tackled in the following notebook.

3.1.5. Versions#

import scooby
scooby.Report(additional=['qbanalysis','pemtk','epsproc', 'holoviews', 'hvplot', 'xarray', 'matplotlib', 'bokeh'])
Tue Jun 03 16:51:21 2025 UTC
OS Linux (Ubuntu 24.04) CPU(s) 4 Machine x86_64 Architecture 64bit
RAM 15.6 GiB Environment Jupyter File system ext4
Python 3.10.11 (main, Sep 30 2024, 21:36:13) [GCC 13.2.0]
qbanalysis 0.0.1 pemtk Module not found epsproc 1.3.2.dev0 holoviews 1.20.2
hvplot 0.11.3 xarray 2022.3.0 matplotlib 3.5.3 bokeh 3.7.3
numpy 1.23.5 scipy 1.15.3 IPython 8.37.0 scooby 0.10.1
# # Check current Git commit for local ePSproc version
# from pathlib import Path
# !git -C {Path(qbanalysis.__file__).parent} branch
# !git -C {Path(qbanalysis.__file__).parent} log --format="%H" -n 1
# # Check current remote commits
# !git ls-remote --heads https://github.com/phockett/qbanalysis
# Check current Git commit for local code version
import qbanalysis
!git -C {Path(qbanalysis.__file__).parent} branch
!git -C {Path(qbanalysis.__file__).parent} log --format="%H" -n 1
* master
4dc763f8848565a5c3d947470fb3eb687d07ba5d
# Check current remote commits
!git ls-remote --heads https://github.com/phockett/Quantum-Beat_Photoelectron-Imaging_Spectroscopy_of_Xe_in_the_VUV
4dc763f8848565a5c3d947470fb3eb687d07ba5d	refs/heads/master
2ff23ede221ac1a0ae8b5351c6c505a6ecd1b65d	refs/heads/uncertainties