How To: Writing Analysis Code with Qlattice¶
Project Structure¶
project/
├── run.py # Data generation (measurement) code
├── analysis/
│ └── my_analysis.py # Analysis scripts
├── results/ # Data output directory
│ └── {job_tag}/
│ └── {measurement}/
│ └── traj-{traj}/
│ └── *.lat # LatData files
└── how_to_analysis.md # This file
1. Setup¶
Imports¶
import glob
import os
import numpy as np
import qlat as q
import qlat_scripts.v1 as qs
Load Path Configuration¶
Always set load_path_list at module level before any data loading:
qs.load_path_list[:] = [
"results",
"../results",
]
MPI Lifecycle¶
Every qlattice script must start/end with MPI:
if __name__ == "__main__":
q.begin_with_mpi()
# ... analysis code ...
q.end_with_mpi()
2. Discovering Available Data¶
Finding Trajectories¶
Use qs.get_load_path() to resolve paths across multiple search directories:
@q.cache_call(maxsize=128)
@q.timer
def get_traj_list(job_tag):
path = qs.get_load_path(f"{job_tag}/my_measurement")
traj_list = []
if path is None:
return traj_list
pattern = os.path.join(path, "traj-*")
for d in glob.glob(pattern):
basename = os.path.basename(d)
if os.path.isdir(d) and basename.startswith("traj-"):
try:
traj = int(basename[5:])
traj_list.append(traj)
except ValueError:
pass
traj_list.sort()
return traj_list
Key points:
qs.get_load_path(fn)returns the first matching path orNoneAlways check for
Nonebefore using the pathSort trajectory lists for reproducibility
Finding Files for a Trajectory¶
fn = f"{job_tag}/my_measurement/traj-{traj}/data.lat"
path = qs.get_load_path(fn)
assert path is not None, f"Cannot find {fn}"
3. Loading Data¶
LatData Format¶
LatData files (.lat) store multi-dimensional arrays with metadata:
ld = q.LatData()
ld.load(path)
# Access metadata
info = ld.info()
# info[i] = [name, size, [labels...], ...]
# Access data as numpy array
data = ld[:] # shape: (dim0, dim1, ...)
Example: Loading Correlator Data¶
@q.timer
def load_data(job_tag, traj):
fn = f"{job_tag}/measurement/traj-{traj}/data.lat"
path = qs.get_load_path(fn)
assert path is not None
ld = q.LatData()
ld.load(path)
info = ld.info()
# Extract expression names from first dimension
expr_names = info[0][2] # list of str
# Extract sizes from remaining dimensions
sizes = [info[i][1] for i in range(1, len(info))]
# Get data array
data = ld[:].copy() # always copy to avoid issues
return expr_names, data, sizes
4. Data Processing¶
Efficient Binning with numpy¶
Use np.add.at for unbuffered accumulation (unlike += which is buffered):
@q.timer
def bin_by_coordinate(data, coord_arr):
"""
Bin complex data by coordinate values.
Parameters
----------
data : np.ndarray, shape (n_expr, *spatial_dims), dtype complex
coord_arr : np.ndarray, shape (*spatial_dims), dtype int
Returns
-------
coord_values : np.ndarray, sorted unique coordinate values
binned : np.ndarray, shape (n_expr, n_bins), dtype complex
"""
coord_flat = coord_arr.ravel()
coord_values = np.unique(coord_flat)
max_coord = int(coord_values.max()) + 1
n_expr = data.shape[0]
# Sum per bin for each expression
data_flat = data.reshape(n_expr, -1)
sums = np.zeros((n_expr, max_coord), dtype=complex)
for e in range(n_expr):
np.add.at(sums[e], coord_flat, data_flat[e])
# Keep only valid bins
valid = np.bincount(coord_flat, minlength=max_coord) > 0
binned = sums[:, valid]
coord_values = np.where(valid)[0]
return coord_values, binned
Key points:
Use sum not mean for jackknife analysis (jackknife needs raw sums)
np.add.athandles repeated indices correctlyFilter out empty bins with
np.bincount
Building Coordinate Arrays¶
# For a grid with dimensions [size_t, size_z, size_y, size_x]
coords = [np.arange(s) for s in sizes]
grids = np.meshgrid(*coords, indexing="ij")
x_sqr = sum(g**2 for g in grids) # x^2 + y^2 + z^2 + t^2
5. Jackknife Error Estimation¶
Using q.g_mk_jk¶
The qlattice jackknife utilities support two modes: randomized jackknife ("rjk", the default) and Super-Jackknife ("super"). The default "rjk" mode is recommended for most use cases:
@q.timer
def collect_and_jackknife(job_tag, traj_list):
"""
Load data for all trajectories and apply jackknife.
Returns
-------
metadata : tuple
Expression names, coordinate values, etc.
jk_arr : np.ndarray, shape (1 + n_rand_sample, n_expr, n_bins)
Index 0 is the mean; indices 1..n are jackknife replicates.
"""
all_data = []
jk_idx_list = []
metadata = None
for traj in traj_list:
expr_names, data, sizes = load_data(job_tag, traj)
if metadata is None:
metadata = (expr_names, sizes)
coord_values, binned = bin_by_coordinate(data, coord_arr)
all_data.append(binned)
jk_idx_list.append((job_tag, traj)) # IMPORTANT: use (job_tag, traj) tuples
samples = np.array(all_data) # shape: (n_traj, n_expr, n_bins)
# Apply Super-Jackknife
jk_arr = q.g_mk_jk(samples, jk_idx_list)
return metadata, jk_arr
Extracting Results¶
Do not extract avg/err until right before printing/using:
jk_arr = collect_and_jackknife(job_tag, traj_list)
# Extract avg and err only when needed
avg = q.g_jk_avg(jk_arr)
err = q.g_jk_err(jk_arr)
# Print results
print(f"Mean: {avg[0, 0]:.6e} +/- {err[0, 0]:.6e}")
Key points:
jk_idx_listmust be[(job_tag, traj), ...]tuples, not integersjk_arr[0]is the mean;jk_arr[1:]are replicatesKeep
jk_arraround for further analysis (plotting, combining datasets)q.g_mk_jkaccepts numpy arrays directly (no.tolist()needed)Use default
n_rand_sampleunless you have a reason to change it
Available jk Utilities¶
q.g_mk_jk(data_list, jk_idx_list) # Create jackknife array
q.g_jk_avg(jk_arr) # Extract mean (index 0)
q.g_jk_err(jk_arr) # Extract error estimate
q.g_jk_avg_err(jk_arr) # Extract both at once
6. Output Formatting¶
Printing Tables¶
def format_results(expr_names, coord_values, avg, err, max_display=10):
"""Print results: value +/- error."""
# Header
print(f"{'coord':>8s}", end="")
for name in expr_names:
print(f" {name:>30s}", end="")
print()
# Data rows
for k, x in enumerate(coord_values):
if x > max_display:
break
print(f"{x:8d}", end="")
for e in range(len(expr_names)):
v = avg[e, k]
s = err[e, k]
print(f" ({v.real:12.6e} +/- {s.real:12.6e})", end="")
print()
7. Complete Template¶
#!/usr/bin/env python3
import glob
import os
import numpy as np
import qlat as q
import qlat_scripts.v1 as qs
qs.load_path_list[:] = [
"results",
"../results",
]
@q.cache_call(maxsize=128)
@q.timer
def get_traj_list(job_tag):
"""Discover available trajectories for a measurement."""
path = qs.get_load_path(f"{job_tag}/my_measurement")
traj_list = []
if path is None:
return traj_list
pattern = os.path.join(path, "traj-*")
for d in glob.glob(pattern):
basename = os.path.basename(d)
if os.path.isdir(d) and basename.startswith("traj-"):
try:
traj = int(basename[5:])
traj_list.append(traj)
except ValueError:
pass
traj_list.sort()
return traj_list
@q.timer
def load_data(job_tag, traj):
"""Load LatData file and return (names, data, metadata)."""
fn = f"{job_tag}/my_measurement/traj-{traj}/data.lat"
path = qs.get_load_path(fn)
assert path is not None, f"Cannot find {fn}"
ld = q.LatData()
ld.load(path)
info = ld.info()
names = info[0][2]
data = ld[:].copy()
return names, data
@q.timer
def process_data(data):
"""Process raw data into analysis format."""
# Your processing here
# Example: return np.real(data)
return data
@q.timer
def collect_data(job_tag, traj_list):
"""Load all trajectories, process, and apply jackknife."""
all_data = []
jk_idx_list = []
names = None
for traj in traj_list:
n, raw = load_data(job_tag, traj)
if names is None:
names = n
processed = process_data(raw)
all_data.append(processed)
jk_idx_list.append((job_tag, traj))
samples = np.array(all_data)
jk_arr = q.g_mk_jk(samples, jk_idx_list)
return names, jk_arr
def format_results(names, avg, err):
"""Print formatted results table."""
for i, name in enumerate(names):
print(f"{name}: {avg[i].real:.6e} +/- {err[i].real:.6e}")
if __name__ == "__main__":
q.begin_with_mpi()
job_tag = "48I"
traj_list = get_traj_list(job_tag)
print(f"{job_tag}: {len(traj_list)} trajectories")
if len(traj_list) > 0:
names, jk_arr = collect_data(job_tag, traj_list)
avg = q.g_jk_avg(jk_arr)
err = q.g_jk_err(jk_arr)
format_results(names, avg, err)
q.end_with_mpi()
8. Common Pitfalls¶
Mistake |
Fix |
|---|---|
Forgetting |
Always wrap main in MPI lifecycle |
Using |
Use |
Converting to mean before jackknife |
Keep sums; jackknife needs raw data |
Using integer indices for |
Use |
Extracting avg/err inside loop |
Extract once at the end from |
Calling |
Pass numpy arrays directly |
Not checking |
Always check for |
Forgetting to copy LatData |
Use |
9. Performance Tips¶
Use
@q.timeron all functions to profileUse
@q.cache_call(maxsize=N)for expensive lookupsPrefer vectorized numpy operations over Python loops
Use
np.add.atfor binning (notnp.bincount+ indexing)Keep
jk_arrin memory for multiple analyses (don’t recompute)