In this tutorial, we explore a LabPlot-inspired scientific data analysis workflow in Python while preserving the structure and terminology of LabPlot’s aspect tree, analysis kernels, plotting system, and project model. We build reusable components to import tabular data, compute descriptive statistics, smooth and differentiate signals, perform Fourier analysis and filtering, detect peaks, integrate curves, reduce data, and fit nonlinear models with detailed statistical diagnostics. We then apply these tools to a realistic spectroscopy example: removing periodic interference, identifying overlapping peaks, fitting a multi-Gaussian model, inspecting residuals, visualizing results through themed worksheets, exporting figures, and saving project data in LabPlot-compatible .lml-style files. Finally, we extend the same workflow to batch processing so we can analyze multiple temperature-dependent spectra and fit secondary trends across the resulting measurements.
Copy CodeCopiedUse a different Browserimport os, sys, gzip, bz2, lzma, time, math, textwrap, warnings
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from enum import Enum
import numpy as np, pandas as pd, matplotlib, matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator
import scipy
from scipy import signal, stats, optimize
warnings.filterwarnings(“ignore”, category=RuntimeWarning)
np.random.seed(20260815)
IN_COLAB = “google.colab” in sys.modules
OUT = “/content/labplot_out” if IN_COLAB else os.path.join(os.getcwd(), “labplot_out”)
os.makedirs(OUT, exist_ok=True)
try: from pylabplot import *; HAVE_SDK = True
except Exception: HAVE_SDK = False
banner = lambda t: print(“n” + “=” * 76 + f”n {t}n” + “=” * 76)
banner(“environment”)
print(f” numpy {np.__version__} | scipy {scipy.__version__} | mpl {matplotlib.__version__} | ”
f”colab={IN_COLAB} | pylabplot={‘yes’ if HAVE_SDK else ‘no -> emulation’}n -> {OUT}”)
class PlotDesignation(Enum):
NoDesignation = 0; X = 1; Y = 2; Z = 3
XError = 4; XErrorMinus = 5; XErrorPlus = 6
YError = 7; YErrorMinus = 8; YErrorPlus = 9
class ColumnMode(Enum):
Double = 0; Text = 1; Integer = 2; BigInt = 3; DateTime = 4
class AbstractAspect:
def __init__(self, name, comment=””):
self._name, self.comment, self.parent, self.children = name, comment, None, []
def name(self): return self._name
def addChild(self, a): a.parent = self; self.children.append(a); return a
def tree(self, d=0):
s = ” ” * d + f”{‘|- ‘ if d else ”}{type(self).__name__:<20} {self._name}”
if isinstance(self, Column):
s += f” [{self.columnMode.name}, {self.rowCount()} rows, {self.plotDesignation.name}]”
return “n”.join([s] + [c.tree(d+1) for c in self.children])
class Column(AbstractAspect):
“””LabPlot’s fundamental data source: a typed vector + a plot designation.”””
def __init__(self, name, values=None, mode=ColumnMode.Double,
designation=PlotDesignation.NoDesignation):
super().__init__(name)
self.columnMode, self.plotDesignation = mode, designation
self._d = np.asarray([] if values is None else values, float)
def values(self): return self._d
def rowCount(self): return len(self._d)
def clean(self): return self._d[np.isfinite(self._d)]
def statistics(self):
“””The 20 quantities in LabPlot’s Column Statistics dialog.”””
x = self.clean(); n = x.size
if not n: return {}
q1, med, q3 = np.percentile(x, [25, 50, 75]); iqr, pos = q3 – q1, x[x > 0]
h = 2 * iqr / n**(1/3) if iqr > 0 else 0
c, _ = np.histogram(x, bins=int(np.clip(np.ptp(x)/h, 1, 1000)) if h else 10)
p = c[c > 0] / c.sum(); v, k = np.unique(np.round(x, 12), return_counts=True)
return {“Count”: n, “Minimum”: x.min(), “Maximum”: x.max(), “Arithmetic mean”: x.mean(),
“Geometric mean”: stats.gmean(pos) if pos.size else np.nan,
“Harmonic mean”: stats.hmean(pos) if pos.size else np.nan,
“Contraharmonic mean”: (x**2).sum() / x.sum() if x.sum() else np.nan,
“Mode”: v[k.argmax()] if k.max() > 1 else np.nan, “First quartile”: q1,
“Median”: med, “Third quartile”: q3, “Interquartile range”: iqr,
“Trimean”: (q1 + 2*med + q3) / 4, “Variance”: x.var(ddof=1),
“Standard deviation”: x.std(ddof=1), “Skewness”: stats.skew(x),
“Mean absolute deviation”: np.abs(x – x.mean()).mean(),
“Median absolute deviation”: np.median(np.abs(x – med)),
“Kurtosis”: stats.kurtosis(x, fisher=False),
“Entropy”: float(-(p * np.log2(p)).sum())}
def sparkline(self, w=26):
“””LabPlot 2.11+ draws these in the column header; text version.”””
b, x = “_.-~^”, self.clean()
s = x[np.linspace(0, x.size-1, min(w, x.size)).astype(int)] if x.size > 1 else x
return “” if s.size < 2 or np.ptp(s) == 0 else “”.join(
b[i] for i in ((s – s.min()) / np.ptp(s) * 4).round().astype(int))
class Spreadsheet(AbstractAspect):
def columns(self): return [c for c in self.children if isinstance(c, Column)]
def column(self, k):
cs = self.columns()
return cs[k] if isinstance(k, int) else next(c for c in cs if c.name() == k)
def columnCount(self): return len(self.columns())
def rowCount(self): return max([c.rowCount() for c in self.columns()], default=0)
def appendColumn(self, n, v, d=PlotDesignation.Y):
return self.addChild(Column(n, v, designation=d))
def toDataFrame(self):
return pd.DataFrame({c.name(): c.values() for c in self.columns()})
def info(self):
print(f” Spreadsheet ‘{self._name}’: {self.rowCount()} rows x {self.columnCount()} cols”)
for c in self.columns():
s = c.statistics()
print(f” {c.name():<12}{c.plotDesignation.name:<6}min {s[‘Minimum’]:>9.4g} max ”
f”{s[‘Maximum’]:>9.4g} mean {s[‘Arithmetic mean’]:>9.4g} {c.sparkline()}”)
class Project(AbstractAspect):
XML_VERSION = 15
def __init__(self, name=”project”, author=””):
super().__init__(name); self.author, self.version = author, “2.12.1”
def spreadsheets(self): return [c for c in self.children if isinstance(c, Spreadsheet)]
class AsciiFilter:
“””LabPlot’s text import: separator auto-detect, comments, row/col limits.”””
def __init__(self, separator=”auto”, commentCharacter=”#”, headerEnabled=True,
startRow=1, endRow=-1, startColumn=1, endColumn=-1):
self.separator, self.commentCharacter = separator, commentCharacter
self.headerEnabled, self.startRow, self.endRow = headerEnabled, startRow, endRow
self.startColumn, self.endColumn = startColumn, endColumn
def readDataFromFile(self, path, dataSource):
with open(path, encoding=”utf-8″, errors=”replace”) as fh:
lines = [l.rstrip(“n”) for l in fh
if l.strip() and not l.lstrip().startswith(self.commentCharacter)]
lines = lines[self.startRow – 1: None if self.endRow < 0 else self.endRow]
if not lines: raise ValueError(“AsciiFilter: nothing to import”)
sep = (next((s for s in (“,”, “;”, “t”, “|”) if s in lines[0]), None)
if self.separator == “auto” else self.separator)
split = lambda l: [p.strip() for p in (l.split(sep) if sep else l.split()) if p.strip()]
header = split(lines[0]) if self.headerEnabled else None
rows = [split(l) for l in (lines[1:] if self.headerEnabled else lines)]
ncol = max(map(len, rows))
c0, c1 = self.startColumn – 1, ncol if self.endColumn < 0 else self.endColumn
for j in range(c0, min(c1, ncol)):
vals = []
for r in rows:
try: vals.append(float(r[j]))
except (IndexError, ValueError): vals.append(np.nan)
dataSource.addChild(Column(
header[j] if header and j < len(header) else f”Column {j+1}”, vals,
designation=PlotDesignation.X if j == c0 else PlotDesignation.Y))
return dataSource
We set up the Python environment, configure reproducibility, and establish the output directory for the tutorial. We recreate LabPlot’s core aspect-tree structure using projects, spreadsheets, columns, plot designations, and column modes. We also implement the AsciiFilter workflow to import structured text data into our LabPlot-style data model.
Copy CodeCopiedUse a different Browserclass nsl_smooth:
“””Analysis -> Smooth (Savitzky-Golay; LabPlot also offers moving average/percentile).”””
@staticmethod
def savitzky_golay(y, points=11, order=3, deriv=0):
points += points % 2 == 0
return signal.savgol_filter(y, points, min(order, points-1), deriv=deriv, mode=”interp”)
class nsl_diff:
“””Analysis -> Differentiate: order 1..6; SG differentiation for noisy data.”””
@staticmethod
def derive(x, y, order=1, smooth_points=0, sg_order=3):
if smooth_points:
return nsl_smooth.savitzky_golay(y, smooth_points, sg_order, deriv=order)
/ np.gradient(x) ** order
out = np.asarray(y, float)
for _ in range(order): out = np.gradient(out, x, edge_order=2)
return out
def simpson(x, y):
“””Composite Simpson on a non-uniform grid (Cartwright’s formula).”””
n = len(x) – 1
if n < 2: return float(np.trapezoid(y, x) if hasattr(np, “trapezoid”) else np.trapz(y, x))
tot, i = 0.0, 0
while i + 2 <= n:
h0, h1 = x[i+1] – x[i], x[i+2] – x[i+1]; hp, hd, hm = h1 + h0, h1 / h0, h1 * h0
tot += hp / 6 * ((2-hd) * y[i] + hp**2 / hm * y[i+1] + (2 – 1/hd) * y[i+2]); i += 2
return tot + ((x[n]-x[n-1]) * (y[n]+y[n-1]) / 2 if i < n else 0)
class nsl_int:
“””Analysis -> Integrate: rectangle / trapezoid / Simpson, cumulative.”””
@staticmethod
def integrate(x, y, method=”trapezoid”, absolute=False):
yy = np.abs(y) if absolute else np.asarray(y, float)
seg = np.diff(x) * (yy[:-1] if method == “rectangle” else (yy[:-1] + yy[1:]) / 2)
cum = np.r_[0.0, np.cumsum(seg)]
return cum * simpson(x, yy) / cum[-1] if method == “simpson” and cum[-1] else cum
class nsl_dft:
“””Analysis -> Fourier Transform: amplitude/magnitude/power/dB, 5 windows.”””
WIN = {“rectangular”: np.ones,
“hann”: lambda n: signal.windows.hann(n, sym=False),
“hamming”: lambda n: signal.windows.hamming(n, sym=False),
“blackman”: lambda n: signal.windows.blackman(n, sym=False),
“flattop”: lambda n: signal.windows.flattop(n, sym=False)}
@staticmethod
def transform(x, y, output=”amplitude”, window=”rectangular”):
n, dt = len(y), float(np.mean(np.diff(x)))
w = nsl_dft.WIN[window](n); cg = w.mean()
Y = np.fft.rfft(y * w); f = np.fft.rfftfreq(n, dt); m = np.abs(Y)
v = {“magnitude”: lambda: m, “power”: lambda: m**2 / (n*cg)**2,
“phase”: lambda: np.angle(Y), “amplitude”: lambda: np.r_[m[0]/(n*cg), 2*m[1:]/(n*cg)],
“dB”: lambda: 20*np.log10(np.maximum(m / (m.max() or 1), 1e-16))}[output]()
return f, v
class nsl_filter:
“””Analysis -> Fourier Filter: low/high/band pass + band reject; ideal or Butterworth.”””
@staticmethod
def apply(x, y, type=”lowpass”, form=”butterworth”, cutoff=.1, cutoff2=.3, order=3):
n = len(y); f = np.fft.rfftfreq(n, float(np.mean(np.diff(x)))); eps = 1e-30
if type == “lowpass”: r = f / cutoff
elif type == “highpass”: r = cutoff / np.maximum(f, eps)
else:
f0, bw = math.sqrt(cutoff * cutoff2), cutoff2 – cutoff
r = np.abs((f**2 – f0**2) / np.maximum(f * bw, eps))
if type == “bandreject”: r = 1 / np.maximum(r, eps)
H = (r <= 1).astype(float) if form == “ideal” else 1 / np.sqrt(1 + r ** (2 * order))
return np.fft.irfft(np.fft.rfft(y) * H, n=n)
class nsl_hilbert:
“””Analysis -> Hilbert Transform (LabPlot 2.9+).”””
@staticmethod
def transform(y, output=”envelope”):
a = signal.hilbert(y)
return {“imag”: a.imag, “real”: a.real, “envelope”: np.abs(a),
“phase”: np.unwrap(np.angle(a))}[output]
class nsl_geom:
“””Analysis -> Data Reduction: Douglas-Peucker, iterative (no recursion limit).”””
@staticmethod
def douglas_peucker(x, y, tol):
n = len(x); keep = np.zeros(n, bool); keep[[0, -1]] = True; stack = [(0, n-1)]
while stack:
i, j = stack.pop()
if j <= i + 1: continue
dx, dy = x[j]-x[i], y[j]-y[i]; den = math.hypot(dx, dy); sl = slice(i+1, j)
d = (np.hypot(x[sl]-x[i], y[sl]-y[i]) if den == 0
else np.abs(dy*(x[sl]-x[i]) – dx*(y[sl]-y[i])) / den)
if d.size and d.max() > tol:
k = i + 1 + int(d.argmax()); keep[k] = True; stack += [(i, k), (k, j)]
return np.flatnonzero(keep)
class nsl_peak:
“””Analysis -> Peak Find (LabPlot 2.11+); seeds multi-peak fits.”””
@staticmethod
def find(x, y, prominence=None, distance=None):
pk, pr = signal.find_peaks(y, prominence=prominence, distance=distance)
w = signal.peak_widths(y, pk, rel_height=.5)[0] if pk.size else np.array([])
return pk, {“positions”: x[pk], “heights”: y[pk],
“prominences”: pr.get(“prominences”, np.array([])),
“fwhm”: w * float(np.mean(np.diff(x)))}
class nsl_fit_model:
“””LabPlot’s model catalogue (Basic / Peak / Growth / Distribution).”””
@staticmethod
def gaussian(x, a, mu, s):
return a / (math.sqrt(2 * np.pi) * s) * np.exp(-(x – mu) ** 2 / (2 * s ** 2))
@staticmethod
def lorentz(x, a, mu, g):
return a / np.pi * (g / 2) / ((x – mu) ** 2 + (g / 2) ** 2)
@dataclass
class FitResult:
names: list; values: np.ndarray; errors: np.ndarray; t: np.ndarray; p: np.ndarray
margin: np.ndarray; gof: dict; dof: int; nfev: int; status: str
elapsed: float; unweighted: bool
residuals: np.ndarray = field(repr=False, default=None)
cov: np.ndarray = field(repr=False, default=None)
def report(self, title=”Fit result”):
print(f”n{‘-‘*76}n {title}n{‘-‘*76}”)
print(f” {self.status} | nfev {self.nfev} | dof {self.dof} | {self.elapsed*1e3:.1f} ms”)
print(f”n {‘param’:<8}{‘value’:>13}{‘error’:>11}{‘err%’:>8}{‘t’:>8}{‘P>|t|’:>10}{‘95% CI’:>26}”)
for i, n in enumerate(self.names):
v, e, m = self.values[i], self.errors[i], self.margin[i]
print(f” {n:<8}{v:>13.6g}{e:>11.4g}{abs(100*e/v) if v else np.inf:>7.2f}%”
f”{self.t[i]:>8.1f}{self.p[i]:>10.2g}{f'[{v-m:.5g},{v+m:.5g}]’:>26}”)
print(“n goodness of fit”)
it = list(self.gof.items())
for i in range(0, len(it), 2):
r = f”{it[i+1][0]:<22}{it[i+1][1]:>14.6g}” if i + 1 < len(it) else “”
print(f” {it[i][0]:<22}{it[i][1]:>14.6g} {r}”)
if self.unweighted:
print(” note: no y-errors given, so chi^2 == SSE and ‘P > chi^2′ is not a realn”
” test. Pass yerr= for a meaningful reduced chi^2.”)
print(“-” * 76)
class nsl_fit:
@staticmethod
def fit(model, x, y, p0, yerr=None, bounds=None, paramNames=None, conf=.95):
“””GSL’s multifit_nlinear == scipy least_squares(method=’lm’).”””
t0 = time.perf_counter()
x, y, p0 = np.asarray(x, float), np.asarray(y, float), np.asarray(p0, float)
sig = np.ones_like(y) if yerr is None else np.asarray(yerr, float)
res = lambda p: (model(x, *p) – y) / sig
kw = dict(max_nfev=500*len(p0), **({“method”: “lm”} if bounds is None
else {“bounds”: bounds}))
out = optimize.least_squares(res, p0, **kw)
p = out.x; n, k = len(y), len(p); dof = max(n – k, 1); r = y – model(x, *p)
sse = float((r**2).sum()); chisq = float(((r / sig)**2).sum()); red = chisq / dof
try: cov = np.linalg.inv(out.jac.T @ out.jac)
except np.linalg.LinAlgError: cov = np.linalg.pinv(out.jac.T @ out.jac)
cov = cov * (red if yerr is None else 1.0); err = np.sqrt(np.abs(np.diag(cov)))
tv = np.divide(p, err, out=np.full_like(p, np.inf), where=err > 0)
sst = float(((y – y.mean())**2).sum()); r2 = 1 – sse/sst if sst else np.nan
F = (r2 / max(k-1, 1)) / ((1-r2) / dof) if r2 < 1 else np.inf
logL = -.5*n * (math.log(2*math.pi) + math.log(sse/n) + 1); aic = 2*k – 2*logL
gof = {“sum sq. residuals”: sse, “mean squared error”: sse/n, “root MSE”: math.sqrt(sse/n),
“mean abs. error”: float(np.abs(r).mean()), “residual std dev”: math.sqrt(sse/dof),
“R^2”: r2, “adjusted R^2”: 1 – (1-r2)*(n-1)/dof, “chi^2”: chisq,
“reduced chi^2”: red, “P > chi^2”: stats.chi2.sf(chisq, dof), “F statistic”: F,
“P > F”: stats.f.sf(F, max(k-1, 1), dof), “log-likelihood”: logL, “AIC”: aic,
“AICc”: aic + 2*k*(k+1)/max(n-k-1, 1), “BIC”: k*math.log(n) – 2*logL}
return FitResult(paramNames or [f”p{i}” for i in range(k)], p, err, tv,
2 * stats.t.sf(np.abs(tv), dof),
stats.t.ppf(.5 + conf/2, dof) * err, gof, dof, int(out.nfev),
out.message, time.perf_counter() – t0, yerr is None, r, cov)
@staticmethod
def confidenceBand(model, x, res, level=.95, eps=1e-7):
“””Delta method sqrt(diag(J C J^T)) * t — LabPlot’s CI overlay.”””
p = res.values; J = np.empty((len(x), len(p)))
for i in range(len(p)):
dp = np.zeros_like(p); dp[i] = eps * max(abs(p[i]), 1)
J[:, i] = (model(x, *(p + dp)) – model(x, *(p – dp))) / (2 * dp[i])
v = np.einsum(“ij,jk,ik->i”, J, res.cov, J)
return stats.t.ppf(.5 + level/2, res.dof) * np.sqrt(np.maximum(v, 0))
@staticmethod
def distributionFitML(data, dist=”norm”):
“””nsl_fit_algorithm_ml — max-likelihood distribution fit (the SDK demo).”””
d = getattr(stats, dist); pr = d.fit(data); ks = stats.kstest(data, dist, args=pr)
ll = float(d.logpdf(data, *pr).sum())
return {“params”: pr, “logLik”: ll, “AIC”: 2 * len(pr) – 2 * ll,
“KS_stat”: ks.statistic, “KS_p”: ks.pvalue, “pdf”: lambda t: d.pdf(t, *pr)}
We implement the main numerical analysis kernels that let us smooth, differentiate, integrate, transform, filter, reduce, and inspect scientific signals. We add peak detection along with Gaussian and Lorentzian models for advanced curve analysis. We also build nonlinear fitting utilities that calculate parameter uncertainties, confidence intervals, goodness-of-fit statistics, and maximum-likelihood distribution fits.
Copy CodeCopiedUse a different BrowserTHEMES = {
“BlackOnWhite”: dict(bg=”#ffffff”, fg=”#000000″, grid=”#c8c8c8″,
cycle=[“#3465a4”, “#cc0000”, “#4e9a06”, “#f57900”, “#75507b”, “#06989a”]),
“Dracula”: dict(bg=”#282a36″, fg=”#f8f8f2″, grid=”#44475a”,
cycle=[“#8be9fd”, “#ff79c6”, “#50fa7b”, “#ffb86c”, “#bd93f9”, “#f1fa8c”]),
“SolarizedDark”: dict(bg=”#002b36″, fg=”#93a1a1″, grid=”#0f4b57″,
cycle=[“#268bd2”, “#dc322f”, “#859900”, “#b58900”, “#6c71c4”, “#2aa198″])}
class XYCurve(AbstractAspect):
def __init__(self, name, x=None, y=None, lineStyle=”-“, lineWidth=1.6,
symbolStyle=None, symbolSize=4., color=None, alpha=1., zorder=2):
super().__init__(name)
self.xColumn, self.yColumn, self.color, self.alpha = x, y, color, alpha
self.lineStyle, self.lineWidth = lineStyle, lineWidth
self.symbolStyle, self.symbolSize, self.zorder = symbolStyle, symbolSize, zorder
self.yErrorColumn = self.fillBetween = None
def setXColumn(self, c): self.xColumn = c; return self
def setYColumn(self, c): self.yColumn = c; return self
@staticmethod
def _v(c): return c.values() if isinstance(c, Column) else np.asarray(c, float)
def draw(self, ax, color):
c = self.color or color; X, Y = self._v(self.xColumn), self._v(self.yColumn)
if self.fillBetween is not None:
ax.fill_between(X, *self.fillBetween, color=c, alpha=.2, lw=0, zorder=self.zorder-1)
if self.yErrorColumn is not None:
ax.errorbar(X, Y, yerr=self._v(self.yErrorColumn), fmt=”none”, ecolor=c,
elinewidth=.8, capsize=2, alpha=.7, zorder=self.zorder)
ax.plot(X, Y, linestyle=self.lineStyle or “none”, marker=self.symbolStyle or “none”,
markersize=self.symbolSize, linewidth=self.lineWidth, color=c, alpha=self.alpha,
label=self._name, zorder=self.zorder, markeredgewidth=0)
class Histogram(AbstractAspect):
“””normalization: ‘Count’ | ‘Probability’ | ‘CountDensity’ | ‘ProbabilityDensity’.”””
def __init__(self, name, dataColumn=None, bins=”auto”, normalization=”ProbabilityDensity”):
super().__init__(name)
self.dataColumn, self.bins, self.normalization = dataColumn, bins, normalization
def draw(self, ax, color):
d = (self.dataColumn.clean() if isinstance(self.dataColumn, Column)
else np.asarray(self.dataColumn, float))
ax.hist(d, bins=self.bins, color=color, alpha=.55, edgecolor=color, lw=.8,
label=self._name, zorder=1, density=”Density” in self.normalization
or self.normalization == “Probability”)
class CartesianPlot(AbstractAspect):
class Type(Enum):
FourAxes = 0; TwoAxes = 1
def __init__(self, name, title=None, xLabel=”x”, yLabel=”y”, logX=False, logY=False):
super().__init__(name); self.type = CartesianPlot.Type.FourAxes
self.title, self.xLabel, self.yLabel = title or name, xLabel, yLabel
self.logX, self.logY, self.legend = logX, logY, None
self.xRange, self.yRange, self.labels = None, None, []
def setType(self, t): self.type = t; return self
def addLegend(self, loc=”best”): self.legend = loc; return self
def setRange(self, x=None, y=None): self.xRange, self.yRange = x, y; return self
def addTextLabel(self, txt, x, y): self.labels.append((txt, x, y)); return self
def _render(self, ax, th):
ax.set_facecolor(th[“bg”])
for i, ch in enumerate(self.children): ch.draw(ax, th[“cycle”][i % len(th[“cycle”])])
ax.set_title(self.title, color=th[“fg”], fontsize=10.5, pad=7)
ax.set_xlabel(self.xLabel, color=th[“fg”], fontsize=9.5)
ax.set_ylabel(self.yLabel, color=th[“fg”], fontsize=9.5)
for lg, sc, axis in ((self.logX, ax.set_xscale, ax.xaxis), (self.logY, ax.set_yscale, ax.yaxis)):
sc(“log”) if lg else axis.set_minor_locator(AutoMinorLocator(2))
if self.xRange: ax.set_xlim(*self.xRange)
if self.yRange: ax.set_ylim(*self.yRange)
four = self.type is CartesianPlot.Type.FourAxes
for s in (“top”, “right”): ax.spines[s].set_visible(four)
for s in ax.spines.values(): s.set_color(th[“fg”]); s.set_linewidth(.9)
ax.tick_params(which=”both”, direction=”in”, colors=th[“fg”], top=four,
right=four, labelsize=8.5)
ax.grid(True, color=th[“grid”], lw=.6, alpha=.7, zorder=0)
for t, x, y in self.labels:
ax.annotate(t, (x, y), color=th[“fg”], fontsize=7.5, ha=”center”)
if self.legend:
for t in ax.legend(loc=self.legend, fontsize=8, framealpha=.85, facecolor=th[“bg”],
edgecolor=th[“grid”]).get_texts(): t.set_color(th[“fg”])
class Worksheet(AbstractAspect):
class ExportFormat(Enum):
PDF = 0; SVG = 1; PNG = 2
def __init__(self, name, cols=None, figsize=(15, 8.5), dpi=110):
super().__init__(name); self.themeName = “BlackOnWhite”
self.cols, self.figsize, self.dpi, self._fig = cols, figsize, dpi, None
def setTheme(self, n):
if n not in THEMES: raise KeyError(f”themes: {list(THEMES)}”)
self.themeName = n; return self
def render(self):
th = THEMES[self.themeName]
ps = [c for c in self.children if isinstance(c, CartesianPlot)]
cols = self.cols or min(len(ps), 2)
fig, axes = plt.subplots(math.ceil(len(ps)/cols), cols, figsize=self.figsize, dpi=self.dpi)
fig.patch.set_facecolor(th[“bg”]); axes = np.atleast_1d(axes).ravel()
for ax, p in zip(axes, ps): p._render(ax, th)
for ax in axes[len(ps):]: ax.axis(“off”)
fig.suptitle(self._name, color=th[“fg”], fontsize=13, y=.995)
fig.tight_layout(rect=(0, 0, 1, .98)); self._fig = fig; return fig
def show(self):
(self.render() if self._fig is None else None); plt.show()
def exportToFile(self, path, format=None):
if self._fig is None: self.render()
fmt = (format.name.lower() if isinstance(format, Worksheet.ExportFormat)
else format or os.path.splitext(path)[1].lstrip(“.”))
self._fig.savefig(path, format=fmt, dpi=self.dpi, bbox_inches=”tight”,
facecolor=self._fig.get_facecolor()); return path
def _reduce(x, y, tolerance=None):
i = nsl_geom.douglas_peucker(x, y, tolerance if tolerance is not None else .02*np.ptp(y))
return x[i], y[i], {“in”: len(x), “out”: len(i), “compression”: 1 – len(i)/len(x)}
class XYAnalysisCurve(XYCurve):
OPS = {
“smooth”: lambda x, y, points=11, order=3:
(x, nsl_smooth.savitzky_golay(y, points, order), {}),
“differentiate”: lambda x, y, derivOrder=1, smoothPoints=0:
(x, nsl_diff.derive(x, y, derivOrder, smoothPoints), {}),
“integrate”: lambda x, y, method=”trapezoid”, absolute=False:
(lambda c: (x, c, {“total”: float(c[-1])}))(nsl_int.integrate(x, y, method, absolute)),
“dft”: lambda x, y, output=”amplitude”, window=”rectangular”:
nsl_dft.transform(x, y, output, window) + ({},),
“filter”: lambda x, y, type=”lowpass”, form=”butterworth”, cutoff=.1, cutoff2=.3, order=3:
(x, nsl_filter.apply(x, y, type, form, cutoff, cutoff2, order), {}),
“hilbert”: lambda x, y, output=”envelope”: (x, nsl_hilbert.transform(y, output), {}),
“reduce”: _reduce}
def __init__(self, name, xData, yData, op, style=None, **opts):
super().__init__(name, **(style or {}))
self._xin, self._yin = XYCurve._v(xData), XYCurve._v(yData)
self.op, self.opts, self.result = op, opts, None
self.recalculate()
def recalculate(self):
self.xColumn, self.yColumn, self.result =
XYAnalysisCurve.OPS[self.op](self._xin, self._yin, **self.opts)
return self
_mk = lambda op: (lambda name, x, y, style=None, **kw: XYAnalysisCurve(name, x, y, op, style, **kw))
XYSmoothCurve, XYDifferentiationCurve = _mk(“smooth”), _mk(“differentiate”)
XYIntegrationCurve = _mk(“integrate”)
XYFourierTransformCurve, XYFourierFilterCurve = _mk(“dft”), _mk(“filter”)
XYHilbertTransformCurve, XYDataReductionCurve = _mk(“hilbert”), _mk(“reduce”)
class XYFitCurve(XYCurve):
“””LabPlot’s centrepiece: non-linear fitting with the full statistics table.”””
def __init__(self, name, xData, yData, model, p0, paramNames=None, yerr=None,
bounds=None, npoints=800, **kw):
super().__init__(name, **kw)
self._xin, self._yin = XYCurve._v(xData), XYCurve._v(yData)
self.model, self.p0, self.paramNames = model, p0, paramNames
self.yerr, self.bounds, self.npoints, self.fitResult = yerr, bounds, npoints, None
def recalculate(self, conf=.95, showConfidenceInterval=True):
self.fitResult = nsl_fit.fit(self.model, self._xin, self._yin, self.p0,
self.yerr, self.bounds, self.paramNames, conf)
xf = np.linspace(self._xin.min(), self._xin.max(), self.npoints)
yf = self.model(xf, *self.fitResult.values); self.xColumn, self.yColumn = xf, yf
if showConfidenceInterval:
d = nsl_fit.confidenceBand(self.model, xf, self.fitResult, conf)
self.fillBetween = (yf – d, yf + d)
return self
class ProjectFile:
MAGIC = ((b”x1fx8b”, gzip.decompress, “gzip”), (b”BZh”, bz2.decompress, “bzip2″),
(b”xfd7zXZx00”, lzma.decompress, “xz”))
@staticmethod
def load(path):
blob = open(path, “rb”).read(); kind = “plain”
for magic, dec, nm in ProjectFile.MAGIC:
if blob.startswith(magic): blob, kind = dec(blob), nm; break
root = ET.fromstring(blob.decode(“utf-8”, “replace”))
root = root if root.tag == “project” else root.find(“.//project”)
if root is None: raise ValueError(“no project element found”)
prj = Project(os.path.basename(path), root.get(“author”, “”))
prj.version = root.get(“version”, “?”)
print(f” loaded .lml: compression={kind} version={prj.version} xmlVersion=”
f”{root.get(‘xmlVersion’,’?’)}”)
parents = {c: p for p in root.iter() for c in p}
def sheet_of(n):
n = parents.get(n)
while n is not None and n.tag != “spreadsheet”: n = parents.get(n)
return n
buckets = {}
for col in root.iter(“column”):
buckets.setdefault(id(sheet_of(col)), (sheet_of(col), []))[1].append(col)
for el, cols in buckets.values():
sp = Spreadsheet(el.get(“name”, “spreadsheet”) if el is not None else “sheet”)
for c in cols: sp.addChild(ProjectFile._column(c))
prj.addChild(sp)
return prj
@staticmethod
def _column(el):
name = el.get(“name”) or next(
(el.find(t).get(“name”) for t in (“general”, “comment”)
if el.find(t) is not None and el.find(t).get(“name”)), “Column”)
rows = el.findall(“row”)
if rows:
raw = [r.text for r in sorted(rows, key=lambda r: int(r.get(“index”, 0)))]
else:
node = next((el.find(t) for t in (“values”, “data”, “double”)
if el.find(t) is not None and el.find(t).text), None)
raw = (node.text if node is not None else el.text or “”).split()
vals = []
for v in raw:
try: vals.append(float(v))
except (TypeError, ValueError): vals.append(np.nan)
try: des = PlotDesignation(int(el.get(“designation”, 0)))
except (ValueError, TypeError): des = PlotDesignation.NoDesignation
return Column(name, vals, designation=des)
@staticmethod
def save(project, path, compression=”gzip”):
root = ET.Element(“project”, {
“version”: project.version, “xmlVersion”: str(Project.XML_VERSION),
“fileName”: os.path.basename(path), “author”: project.author,
“modificationTime”: time.strftime(“%Y-%m-%d %H:%M:%S”)})
ET.SubElement(root, “comment”).text = project.comment
for sp in project.spreadsheets():
e = ET.SubElement(root, “spreadsheet”, {“name”: sp.name()})
ET.SubElement(e, “general”, {“rowCount”: str(sp.rowCount()),
“columnCount”: str(sp.columnCount())})
for col in sp.columns():
c = ET.SubElement(e, “column”, {
“name”: col.name(), “rows”: str(col.rowCount()),
“designation”: str(col.plotDesignation.value), “mode”: str(col.columnMode.value)})
for i, v in enumerate(col.values()):
ET.SubElement(c, “row”, {“index”: str(i)}).text = repr(float(v))
xml = (b'<?xml version=”1.0″ encoding=”UTF-8″?>n<!DOCTYPE LabPlotXML>n’
+ ET.tostring(root, encoding=”utf-8″))
open(path, “wb”).write({“gzip”: gzip.compress, “bzip2”: bz2.compress,
“xz”: lzma.compress, “none”: lambda b: b}[compression](xml))
return path
We construct the visualization layer using curves, histograms, Cartesian plots, worksheets, themes, and reusable analysis-curve objects. We connect these plotting objects directly to our numerical operations so we can recalculate processed curves and fitted models programmatically. We also implement LabPlot-style project file loading and saving, including compressed .lml formats and spreadsheet reconstruction.
Copy CodeCopiedUse a different Browserbanner(“STEP 1 import an instrument file with AsciiFilter”)
wl = np.linspace(400., 700., 1500)
TRUE = [(120., 468., 6.), (75., 512., 4.5), (140., 545., 9.), (55., 604., 5.)]
clean = 18. – .012 * (wl – 400)
for a, mu, s in TRUE: clean = clean + nsl_fit_model.gaussian(wl, a, mu, s)
counts = clean + 2.2 * np.sin(2*np.pi * wl / 3.7) + np.random.normal(0, 1.1, wl.size)
raw = os.path.join(OUT, “spectrum.dat”)
with open(raw, “w”) as fh:
fh.write(“# SpecMaster-9000, 500 ms integrationnwavelengthtcountsn”)
fh.writelines(f”{a:.4f}t{b:.5f}n” for a, b in zip(wl, counts))
project = Project(“spectroscopy demo”, “LabPlot Colab tutorial”)
data = project.addChild(Spreadsheet(“data”))
AsciiFilter().readDataFromFile(raw, data)
x, y = data.column(“wavelength”), data.column(“counts”)
data.info()
banner(“STEP 2 column statistics”)
it = list(y.statistics().items())
for i in range(0, len(it), 2):
print(f” {it[i][0]:<26}{it[i][1]:>13.6g} ” +
(f”{it[i+1][0]:<26}{it[i+1][1]:>13.6g}” if i + 1 < len(it) else “”))
banner(“STEP 3-4 FFT finds the fringe; a band-reject notch removes it”)
freq, amp = nsl_dft.transform(x.values(), y.values(), “amplitude”, “hann”)
i0 = int(np.argmax(amp[5:])) + 5; f0 = freq[i0]
print(f” dominant component {f0:.4f} 1/nm -> period {1/f0:.3f} nm (injected 3.700), ”
f”amplitude {amp[i0]:.3f} (injected 2.200)”)
yf = XYFourierFilterCurve(“fringe removed”, x, y, type=”bandreject”, form=”butterworth”,
cutoff=f0*.82, cutoff2=f0*1.22, order=6).yColumn
print(f” notch {f0*.82:.3f}-{f0*1.22:.3f} 1/nm | residual std vs truth ”
f”{np.std(y.values()-clean):.3f} -> {np.std(yf-clean):.3f}”)
banner(“STEP 5 smooth + 2nd derivative -> locate peaks objectively”)
smooth = XYSmoothCurve(“SG smoothed”, x, yf, points=41, order=3)
d2 = XYDifferentiationCurve(“2nd derivative”, x, smooth.yColumn, derivOrder=2, smoothPoints=61)
pk, pr = nsl_peak.find(x.values(), -d2.yColumn, prominence=np.ptp(d2.yColumn)*.20, distance=25)
print(f” {len(pk)} peaks in -y” | found ” + “, “.join(f”{v:7.2f}” for v in pr[“positions”]) +
“n | truth ” + “, “.join(f”{t[1]:7.2f}” for t in TRUE))
banner(“STEP 6 non-linear multi-peak fit (Levenberg-Marquardt)”)
NPEAK = 4
centres = np.sort(pr[“positions”][np.argsort(pr[“heights”])[::-1][:NPEAK]])
def multi_gauss(xx, c0, c1, *p):
“””Linear baseline + NPEAK Gaussians — the ‘Custom’ model you’d type in.”””
out = c0 + c1 * xx
for i in range(NPEAK): out = out + nsl_fit_model.gaussian(xx, *p[3*i:3*i+3])
return out
p0 = [18., -.012]
for mu in centres:
j = int(np.argmin(np.abs(x.values() – mu)))
p0 += [max(smooth.yColumn[j] – 12, 5.) * 15., float(mu), 6.]
names = [“b0”, “b1″] + sum([[f”A{i+1}”, f”mu{i+1}”, f”sg{i+1}”] for i in range(NPEAK)], [])
lo = [-np.inf, -np.inf] + sum([[0., m – 12, .5] for m in centres], [])
hi = [np.inf, np.inf] + sum([[np.inf, m + 12, 40.] for m in centres], [])
fit = XYFitCurve(“fit + 95% CI”, x, yf, multi_gauss, p0, names, bounds=(lo, hi), lineWidth=2.)
fit.recalculate()
fit.fitResult.report(“XYFitCurve :: 4 Gaussians + linear baseline”)
pv = fit.fitResult.values
print(f”n {‘peak’:<6}{‘area’:>10}{‘true’:>8}{‘centre’:>11}{‘true’:>9}{‘sigma’:>9}{‘true’:>8}”)
for i, (a, mu, s) in enumerate(TRUE):
print(f” {i+1:<6}{pv[2+3*i]:>10.2f}{a:>8.1f}{pv[3+3*i]:>11.3f}{mu:>9.1f}{pv[4+3*i]:>9.3f}{s:>8.1f}”)
banner(“STEP 7 integration, data reduction, Hilbert envelope”)
base = pv[0] + pv[1] * x.values(); net = yf – base
tot = XYIntegrationCurve(“cumulative”, x, net, method=”simpson”)
print(f” total net signal (Simpson) {tot.result[‘total’]:.2f}; per-peak analytic vs numeric:”)
for i, (a, mu, s) in enumerate(TRUE):
A, M, S = pv[2+3*i], pv[3+3*i], pv[4+3*i]; m = np.abs(x.values() – M) < 3.5 * S
print(f” peak {i+1}: {A:7.2f} vs {simpson(x.values()[m], net[m]):7.2f} (true {a:.0f})”)
print(” peaks 2/3 overlap, so their numeric windows double-count the shared area — whichn”
” is exactly why you fit a multi-peak model instead of integrating windows by hand.”)
red = XYDataReductionCurve(“reduced”, x, smooth.yColumn, tolerance=.4)
err = np.max(np.abs(np.interp(x.values(), red.xColumn, red.yColumn) – smooth.yColumn))
env = XYHilbertTransformCurve(“envelope”, x, y.values() – smooth.yColumn)
print(f” Douglas-Peucker tol=0.4: {red.result[‘in’]} -> {red.result[‘out’]} pts ”
f”({red.result[‘compression’]*100:.1f}% dropped), max error {err:.4f}n”
f” Hilbert envelope of the removed fringe: mean {env.yColumn.mean():.3f} counts”
f” (injected amplitude 2.200)”)
banner(“STEP 8 residual diagnostics”)
res = fit.fitResult.residuals
ml = nsl_fit.distributionFitML(res, “norm”)
dw = float(np.sum(np.diff(res)**2) / np.sum(res**2))
print(f” ML normal mu={ml[‘params’][0]:+.4f} sigma={ml[‘params’][1]:.4f} AIC={ml[‘AIC’]:.1f}n”
f” KS D={ml[‘KS_stat’]:.4f} p={ml[‘KS_p’]:.4f} | Shapiro W=”
f”{stats.shapiro(res[:5000]).statistic:.4f} | Durbin-Watson d={dw:.3f}n -> ”
f”{‘consistent with white Gaussian noise’ if ml[‘KS_p’]>.05 and 1.5<dw<2.5 else ‘structure remains’}”)
We generate a realistic noisy spectroscopy dataset containing a sloping baseline, overlapping Gaussian peaks, periodic interference, and random noise. We use Fourier analysis, band-reject filtering, smoothing, differentiation, and peak detection to isolate key spectral features before fitting. We then perform a constrained multi-Gaussian fit, integrate the recovered signal, reduce the data, calculate a Hilbert envelope, and statistically evaluate the fit residuals.
Copy CodeCopiedUse a different Browserbanner(“STEP 9 Worksheet -> CartesianPlots -> theme -> export”)
ws = Worksheet(“Spectroscopy analysis”, cols=3, figsize=(15, 8.5))
ws.setTheme(“Dracula”)
p1 = CartesianPlot(“raw & fit”, “Raw spectrum + multi-Gaussian fit”, “wavelength (nm)”, “counts”)
p1.addLegend(“upper right”).addChild(XYCurve(“raw”, x, y, lineWidth=.6, alpha=.45))
p1.addChild(XYCurve(“fringe removed”, x, yf, lineWidth=.9, alpha=.8)); p1.addChild(fit)
p2 = CartesianPlot(“components”, “Resolved components”, “wavelength (nm)”, “counts”)
p2.addLegend(“upper right”).addChild(XYCurve(“baseline”, x, base, lineStyle=”–“, lineWidth=1.2))
for i in range(NPEAK):
A, M, S = pv[2+3*i], pv[3+3*i], pv[4+3*i]
p1.addTextLabel(f”{M:.1f}”, M, multi_gauss(M, *pv) + 1.2)
p2.addChild(XYCurve(f”peak {i+1} ({M:.1f} nm)”, x,
base + nsl_fit_model.gaussian(x.values(), A, M, S), lineWidth=1.3))
f2, a2 = nsl_dft.transform(x.values(), yf, “amplitude”, “hann”)
p3 = CartesianPlot(“fft”, “Amplitude spectrum (Hann)”, “spatial frequency (1/nm)”,
“amplitude”, logY=True).addLegend(“upper right”).setRange(x=(0, .6))
p3.addChild(XYCurve(“raw”, freq, np.maximum(amp, 1e-4), lineWidth=1.))
p3.addChild(XYCurve(“filtered”, f2, np.maximum(a2, 1e-4), lineWidth=1.))
p3.addTextLabel(f”fringen{f0:.3f} 1/nm”, f0, amp[i0] * 1.6)
p4 = CartesianPlot(“d2”, “Second derivative (peak detection)”, “wavelength (nm)”, “d2(counts)/dx2”)
p4.addLegend(“lower right”).addChild(XYCurve(“-y””, x, -d2.yColumn, lineWidth=1.))
p4.addChild(XYCurve(“detected”, x.values()[pk], -d2.yColumn[pk], lineStyle=None,
symbolStyle=”o”, symbolSize=6.))
tt = np.linspace(res.min(), res.max(), 400)
p5 = CartesianPlot(“residuals”, “Fit residuals + ML normal”, “residual (counts)”,
“probability density”).addLegend(“upper right”)
p5.addChild(Histogram(“residuals”, res, bins=45))
p5.addChild(XYCurve(f”N({ml[‘params’][0]:.2f}, {ml[‘params’][1]:.2f})”, tt, ml[“pdf”](tt), lineWidth=2.))
for p in (p1, p2, p3, p4, p5): ws.addChild(p)
ws.render()
for ext, f in ((“.png”, None), (“.pdf”, Worksheet.ExportFormat.PDF),
(“.svg”, Worksheet.ExportFormat.SVG)):
ws.exportToFile(os.path.join(OUT, “worksheet” + ext), f)
print(” exported worksheet.png / .pdf / .svg”); ws.show()
banner(“STEP 10 project tree + .lml round-trip”)
r = project.addChild(Spreadsheet(“analysis results”))
r.appendColumn(“wavelength”, x.values(), PlotDesignation.X)
for n, v in ((“filtered”, yf), (“smoothed”, smooth.yColumn), (“baseline”, base),
(“fit”, multi_gauss(x.values(), *pv)),
(“residuals”, yf – multi_gauss(x.values(), *pv)), (“cumulative”, tot.yColumn)):
r.appendColumn(n, v)
pp = project.addChild(Spreadsheet(“fit parameters”))
pp.appendColumn(“value”, pv)
pp.appendColumn(“error”, fit.fitResult.errors, PlotDesignation.YError)
project.addChild(ws)
print(project.tree())
for c, ext in ((“gzip”, “.lml.gz”), (“xz”, “.lml.xz”), (“none”, “.lml”)):
f = ProjectFile.save(project, os.path.join(OUT, “spectroscopy” + ext), c)
print(f” saved {os.path.basename(f):<24}{os.path.getsize(f)/1024:>8.1f} kB ({c})”)
back = ProjectFile.load(os.path.join(OUT, “spectroscopy.lml.gz”))
o, b = y.values(), back.spreadsheets()[0].column(“counts”).values()
print(f” round-trip: max |delta| = {np.max(np.abs(o-b)):.3e} {‘OK’ if np.allclose(o,b) else ‘BAD’}”)
r.toDataFrame().to_csv(os.path.join(OUT, “analysis_results.csv”), index=False)
We organize the spectroscopy results into a themed worksheet containing the raw spectrum, fitted components, Fourier spectrum, detected peaks, and residual distribution. We export the complete visualization to PNG, PDF, and SVG formats so we have reusable graphical outputs. We also store our processed measurements and fitted parameters inside the project, save them in several .lml formats, and verify that the project data survives a complete round trip.
Copy CodeCopiedUse a different Browserbanner(“STEP 11 batch: import -> filter -> fit -> secondary fit”)
bd = os.path.join(OUT, “batch”); os.makedirs(bd, exist_ok=True)
temps = [20, 40, 60, 80, 100, 120]
for T in temps:
yy = 18. – .012 * (wl – 400)
for a, mu, s in TRUE:
yy = yy + nsl_fit_model.gaussian(wl, a * math.exp(-(T-20)/140), mu + .045*(T-20),
s * (1 + .004*(T-20)))
with open(os.path.join(bd, f”run_{T:03d}C.dat”), “w”) as fh:
fh.write(f”# T = {T} Cnwavelengthtcountsn”)
fh.writelines(f”{a:.4f}t{b:.5f}n”
for a, b in zip(wl, yy + np.random.normal(0, 1.1, wl.size)))
def analyse(path):
sp = Spreadsheet(os.path.basename(path)); AsciiFilter().readDataFromFile(path, sp)
q = XYFitCurve(“fit”, sp.column(0), sp.column(1), multi_gauss, p0, names, bounds=(lo, hi))
.recalculate(showConfidenceInterval=False).fitResult
return {“file”: os.path.basename(path), “area”: q.values[8], “area_err”: q.errors[8],
“centre”: q.values[9], “centre_err”: q.errors[9], “R2”: q.gof[“R^2”]}
t0 = time.perf_counter()
df = pd.DataFrame([analyse(os.path.join(bd, f)) for f in sorted(os.listdir(bd))])
df.insert(1, “T_C”, temps)
print(df.to_string(index=False, float_format=lambda v: f”{v:9.4f}”),
f”n {len(df)} files in {time.perf_counter()-t0:.2f} s”)
arr = nsl_fit.fit(lambda T, A, T0: A * np.exp(-(T-20)/T0), df.T_C.values, df.area.values,
[140., 140.], yerr=df.area_err.values, paramNames=[“A”, “T0”])
arr.report(“Secondary fit :: peak-3 area vs temperature”)
print(f” injected quench constant 140.0 -> recovered {arr.values[1]:.1f} +- {arr.errors[1]:.1f}”)
ws2 = Worksheet(“Batch results”, figsize=(6.5, 4.2)); ws2.setTheme(“SolarizedDark”)
q1 = CartesianPlot(“area”, “Peak area vs temperature”, “temperature (C)”, “fitted area”).addLegend()
c = XYCurve(“measured”, df.T_C.values, df.area.values, lineStyle=None, symbolStyle=”o”, symbolSize=6)
c.yErrorColumn = df.area_err.values; q1.addChild(c); Tg = np.linspace(20, 120, 200)
q1.addChild(XYCurve(f”A*exp(-(T-20)/{arr.values[1]:.0f})”, Tg,
arr.values[0] * np.exp(-(Tg-20)/arr.values[1]), lineWidth=2.))
ws2.addChild(q1); ws2.render()
ws2.exportToFile(os.path.join(OUT, “batch_results.png”)); ws2.show()
sl = np.polyfit(df.T_C, df.centre, 1)[0]
print(f” measured centre drift {sl*1000:.2f} pm/C (injected 45.0)”)
banner(“APPENDIX the same workflow on the real pylabplot SDK”)
print(textwrap.dedent(“””
from pylabplot import * # every name below is identical
spreadsheet = Spreadsheet(“data”)
AsciiFilter().readDataFromFile(“spectrum.dat”, spreadsheet)
worksheet = Worksheet(“worksheet”); plotArea = CartesianPlot(“plot area”)
plotArea.setType(CartesianPlot.Type.FourAxes); plotArea.addLegend()
worksheet.addChild(plotArea)
curve = XYCurve(“spectrum”)
curve.setXColumn(spreadsheet.column(0)); curve.setYColumn(spreadsheet.column(1))
plotArea.addChild(curve)
fitCurve = XYFitCurve(“fit”); fitData = fitCurve.fitData()
fitData.modelCategory = nsl_fit_model_peak; fitData.modelType = nsl_fit_model_gaussian
fitData.degree = 4
XYFitCurve.initFitData(fitData); fitCurve.setFitData(fitData); fitCurve.recalculate()
worksheet.setTheme(“Dracula”)
worksheet.exportToFile(“result.pdf”, Worksheet.ExportFormat.PDF)
# pylabplot ships INSIDE a LabPlot install, not on PyPI, and upstream still marks
# the SDK experimental — no API/ABI stability guarantee yet.
“””).strip())
banner(“done”)
for f in sorted(os.listdir(OUT)):
if os.path.isfile(q := os.path.join(OUT, f)):
print(f” {f:<26}{os.path.getsize(q)/1024:>9.1f} kB”)
if IN_COLAB: print(f”n from google.colab import files; files.download(‘{OUT}/worksheet.pdf’)”)
We extend the workflow from a single spectrum to a batch of temperature-dependent synthetic measurements and automatically analyze every file. We extract fitted peak areas and centers, perform a secondary exponential fit, visualize the temperature dependence, and measure the recovered spectral drift. We finally connect our emulated workflow to equivalent pylabplot SDK concepts and list the generated tutorial outputs.
In conclusion, we built a complete scientific analysis pipeline that mirrors many of LabPlot’s core concepts while letting us run the workflow directly in Python. We moved from structured data import and statistical inspection to signal processing, Fourier-domain filtering, peak detection, nonlinear multi-peak fitting, integration, residual analysis, visualization, project serialization, and automated batch processing. By combining these stages, we can transform noisy experimental measurements into interpretable parameters, publication-ready plots, reusable project outputs, and higher-level trends such as temperature-dependent peak behavior. We also established a practical bridge between the Python implementation and the real pylabplot SDK, giving us a foundation for transferring the same workflow to a native LabPlot environment.
Check out the FULL CODES here. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.
Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us
The post Scientific Data Analysis with LabPlot in Python: Signal Processing, Spectral Peak Fitting, Visualization, and Batch Automation appeared first on MarkTechPost.
