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:
Forbes, R. et al. (2018) ‘Quantum-beat photoelectron-imaging spectroscopy of Xe in the VUV’, Physical Review A, 97(6), p. 063417. Available at: https://doi.org/10.1103/PhysRevA.97.063417. arXiv: http://arxiv.org/abs/1803.01081, Authorea (original HTML version): https://doi.org/10.22541/au.156045380.07795038
Data (OSF): https://osf.io/ds8mk/
Quantum Metrology with Photoelectrons (Github repo), particularly the Alignment 3 notebook. Functions from this notebook have been incorporated in the current project, under
qbanalysis.hyperfine.
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()
Show code cell content
# # Test beat model with changed params...
# xeSplittings = np.random.randn(4)
# xeSplittings
Show code cell content
# xePropsFit[fitParamsCol] = 0.1*np.abs(xeSplittings)
# xePropsFit
Show 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).
Show 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}):
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.
| 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"))
| 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 |
| 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 |

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