Scientific Data Analysis with LabPlot in Python: Signal Processing, Spectral Peak Fitting, Visualization, and Batch Automation
In this tutorial, we discover a LabPlot-inspired scientific information evaluation workflow in Python whereas preserving the construction and terminology of LabPlot’s side tree, evaluation kernels, plotting system, and undertaking mannequin. We construct reusable parts to import tabular information, compute descriptive statistics, easy and differentiate alerts, carry out Fourier evaluation and filtering, detect peaks, combine curves, cut back information, and match nonlinear fashions with detailed statistical diagnostics. We then apply these instruments to a practical spectroscopy instance: eradicating periodic interference, figuring out overlapping peaks, becoming a multi-Gaussian mannequin, inspecting residuals, visualizing outcomes by themed worksheets, exporting figures, and saving undertaking information in LabPlot-compatible .lml-style recordsdata. Finally, we lengthen the identical workflow to batch processing so we are able to analyze a number of temperature-dependent spectra and match secondary tendencies throughout the ensuing measurements.
import os, sys, gzip, bz2, lzma, time, math, textwrap, warnings
import xml.etree.ElementTree as ET
from dataclasses import dataclass, subject
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 sign, stats, optimize
warnings.filterwarnings("ignore", class=RuntimeWarning)
np.random.seed(20260815)
IN_COLAB = "google.colab" in sys.modules
OUT = "/content material/labplot_out" if IN_COLAB else os.path.be a part of(os.getcwd(), "labplot_out")
os.makedirs(OUT, exist_ok=True)
attempt: from pylabplot import *; HAVE_SDK = True
besides Exception: HAVE_SDK = False
banner = lambda t: print("n" + "=" * 76 + f"n {t}n" + "=" * 76)
banner("setting")
print(f" numpy {np.__version__} | scipy {scipy.__version__} | mpl {matplotlib.__version__} | "
f"colab={IN_COLAB} | pylabplot={'sure' 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; LargeInt = 3; DateTime = 4
class AbstractAspect:
def __init__(self, title, remark=""):
self._name, self.remark, self.mum or dad, self.youngsters = title, remark, None, []
def title(self): return self._name
def addChild(self, a): a.mum or dad = self; self.youngsters.append(a); return a
def tree(self, d=0):
s = " " * d + f"- ' if d else ''{kind(self).__name__:<20} {self._name}"
if isinstance(self, Column):
s += f" [{self.columnMode.name}, {self.rowCount()} rows, {self.plotDesignation.name}]"
return "n".be a part of([s] + [c.tree(d+1) for c in self.children])
class Column(AbstractAspect):
"""LabPlot's elementary information supply: a typed vector + a plot designation."""
def __init__(self, title, values=None, mode=ColumnMode.Double,
designation=PlotDesignation.NoDesignation):
tremendous().__init__(title)
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 clear(self): return self._d[np.isfinite(self._d)]
def statistics(self):
"""The 20 portions in LabPlot's Column Statistics dialog."""
x = self.clear(); n = x.dimension
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, okay = np.distinctive(np.spherical(x, 12), return_counts=True)
return {"Count": n, "Minimum": x.min(), "Maximum": x.max(), "Arithmetic imply": x.imply(),
"Geometric imply": stats.gmean(pos) if pos.dimension else np.nan,
"Harmonic imply": stats.hmean(pos) if pos.dimension else np.nan,
"Contraharmonic imply": (x**2).sum() / x.sum() if x.sum() else np.nan,
"Mode": v[k.argmax()] if okay.max() > 1 else np.nan, "First quartile": q1,
"Median": med, "Third quartile": q3, "Interquartile vary": 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.imply()).imply(),
"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+ attracts these in the column header; textual content model."""
b, x = "_.-~^", self.clear()
s = x[np.linspace(0, x.size-1, min(w, x.size)).astype(int)] if x.dimension > 1 else x
return "" if s.dimension < 2 or np.ptp(s) == 0 else "".be a part of(
b[i] for i in ((s - s.min()) / np.ptp(s) * 4).spherical().astype(int))
class Spreadsheet(AbstractAspect):
def columns(self): return [c for c in self.children if isinstance(c, Column)]
def column(self, okay):
cs = self.columns()
return cs[k] if isinstance(okay, int) else subsequent(c for c in cs if c.title() == okay)
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 toDataBody(self):
return pd.DataBody({c.title(): c.values() for c in self.columns()})
def information(self):
print(f" Spreadsheet '{self._name}': {self.rowCount()} rows x {self.columnCount()} cols")
for c in self.columns():
s = c.statistics()
print(f" {c.title():<12}{c.plotDesignation.title:<6}min {s['Minimum']:>9.4g} max "
f"{s['Maximum']:>9.4g} imply {s['Arithmetic mean']:>9.4g} {c.sparkline()}")
class Project(AbstractAspect):
XML_VERSION = 15
def __init__(self, title="undertaking", writer=""):
tremendous().__init__(title); self.writer, self.model = writer, "2.12.1"
def spreadsheets(self): return [c for c in self.children if isinstance(c, Spreadsheet)]
class AsciiFilter:
"""LabPlot's textual content import: separator auto-detect, feedback, row/col limits."""
def __init__(self, separator="auto", commentCharacter="#", headerEnabled=True,
startRow=1, endRow=-1, beginColumn=1, finishColumn=-1):
self.separator, self.commentCharacter = separator, commentCharacter
self.headerEnabled, self.startRow, self.endRow = headerEnabled, startRow, endRow
self.beginColumn, self.finishColumn = beginColumn, finishColumn
def learnDataFromFile(self, path, dataSource):
with open(path, encoding="utf-8", errors="substitute") as fh:
traces = [l.rstrip("n") for l in fh
if l.strip() and not l.lstrip().startswith(self.commentCharacter)]
traces = traces[self.startRow - 1: None if self.endRow < 0 else self.endRow]
if not traces: elevate ValueError("AsciiFilter: nothing to import")
sep = (subsequent((s for s in (",", ";", "t", "|") if s in traces[0]), None)
if self.separator == "auto" else self.separator)
cut up = lambda l: [p.strip() for p in (l.split(sep) if sep else l.split()) if p.strip()]
header = cut up(traces[0]) if self.headerEnabled else None
rows = [split(l) for l in (lines[1:] if self.headerEnabled else traces)]
ncol = max(map(len, rows))
c0, c1 = self.beginColumn - 1, ncol if self.finishColumn < 0 else self.finishColumn
for j in vary(c0, min(c1, ncol)):
vals = []
for r in rows:
attempt: vals.append(float(r[j]))
besides (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 arrange the Python setting, configure reproducibility, and set up the output listing for the tutorial. We recreate LabPlot’s core aspect-tree construction utilizing initiatives, spreadsheets, columns, plot designations, and column modes. We additionally implement the AsciiFilter workflow to import structured textual content information into our LabPlot-style information mannequin.
class nsl_smooth:
"""Analysis -> Smooth (Savitzky-Golay; LabPlot additionally affords transferring common/percentile)."""
@staticmethod
def savitzky_golay(y, factors=11, order=3, deriv=0):
factors += factors % 2 == 0
return sign.savgol_filter(y, factors, min(order, points-1), deriv=deriv, mode="interp")
class nsl_diff:
"""Analysis -> Differentiate: order 1..6; SG differentiation for noisy information."""
@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 vary(order): out = np.gradient(out, x, edge_order=2)
return out
def simpson(x, y):
"""Composite Simpson on a non-uniform grid (Cartwright's system)."""
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
whereas 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 combine(x, y, technique="trapezoid", absolute=False):
yy = np.abs(y) if absolute else np.asarray(y, float)
seg = np.diff(x) * (yy[:-1] if technique == "rectangle" else (yy[:-1] + yy[1:]) / 2)
cum = np.r_[0.0, np.cumsum(seg)]
return cum * simpson(x, yy) / cum[-1] if technique == "simpson" and cum[-1] else cum
class nsl_dft:
"""Analysis -> Fourier Transform: amplitude/magnitude/energy/dB, 5 home windows."""
WIN = {"rectangular": np.ones,
"hann": lambda n: sign.home windows.hann(n, sym=False),
"hamming": lambda n: sign.home windows.hamming(n, sym=False),
"blackman": lambda n: sign.home windows.blackman(n, sym=False),
"flattop": lambda n: sign.home windows.flattop(n, sym=False)}
@staticmethod
def remodel(x, y, output="amplitude", window="rectangular"):
n, dt = len(y), float(np.imply(np.diff(x)))
w = nsl_dft.WIN[window](n); cg = w.imply()
Y = np.fft.rfft(y * w); f = np.fft.rfftfreq(n, dt); m = np.abs(Y)
v = {"magnitude": lambda: m, "energy": lambda: m**2 / (n*cg)**2,
"part": lambda: np.angle(Y), "amplitude": lambda: np.r_[m[0]/(n*cg), 2*m[1:]/(n*cg)],
"dB": lambda: 20*np.log10(np.most(m / (m.max() or 1), 1e-16))}[output]()
return f, v
class nsl_filter:
"""Analysis -> Fourier Filter: low/excessive/band move + band reject; supreme or Butterworth."""
@staticmethod
def apply(x, y, kind="lowpass", type="butterworth", cutoff=.1, cutoff2=.3, order=3):
n = len(y); f = np.fft.rfftfreq(n, float(np.imply(np.diff(x)))); eps = 1e-30
if kind == "lowpass": r = f / cutoff
elif kind == "highpass": r = cutoff / np.most(f, eps)
else:
f0, bw = math.sqrt(cutoff * cutoff2), cutoff2 - cutoff
r = np.abs((f**2 - f0**2) / np.most(f * bw, eps))
if kind == "bandreject": r = 1 / np.most(r, eps)
H = (r <= 1).astype(float) if type == "supreme" 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 remodel(y, output="envelope"):
a = sign.hilbert(y)
return {"imag": a.imag, "actual": a.actual, "envelope": np.abs(a),
"part": np.unwrap(np.angle(a))}[output]
class nsl_geom:
"""Analysis -> Data Reduction: Douglas-Peucker, iterative (no recursion restrict)."""
@staticmethod
def douglas_peucker(x, y, tol):
n = len(x); preserve = np.zeros(n, bool); preserve[[0, -1]] = True; stack = [(0, n-1)]
whereas stack:
i, j = stack.pop()
if j <= i + 1: proceed
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.dimension and d.max() > tol:
okay = i + 1 + int(d.argmax()); preserve[k] = True; stack += [(i, k), (k, j)]
return np.flatnonzero(preserve)
class nsl_peak:
"""Analysis -> Peak Find (LabPlot 2.11+); seeds multi-peak matches."""
@staticmethod
def discover(x, y, prominence=None, distance=None):
pk, pr = sign.find_peaks(y, prominence=prominence, distance=distance)
w = sign.peak_widths(y, pk, rel_height=.5)[0] if pk.dimension else np.array([])
return pk, {"positions": x[pk], "heights": y[pk],
"prominences": pr.get("prominences", np.array([])),
"fwhm": w * float(np.imply(np.diff(x)))}
class nsl_fit_model:
"""LabPlot's mannequin 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 FitConsequence:
names: listing; values: np.ndarray; errors: np.ndarray; t: np.ndarray; p: np.ndarray
margin: np.ndarray; gof: dict; dof: int; nfev: int; standing: str
elapsed: float; unweighted: bool
residuals: np.ndarray = subject(repr=False, default=None)
cov: np.ndarray = subject(repr=False, default=None)
def report(self, title="Fit outcome"):
print(f"n{'-'*76}n {title}n{'-'*76}")
print(f" {self.standing} | nfev {self.nfev} | dof {self.dof} | {self.elapsed*1e3:.1f} ms")
print(f"n {'param':<8}{'worth':>13}{'error':>11}{'err%':>8}{'t':>8}t{'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 match")
it = listing(self.gof.objects())
for i in vary(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(" be aware: no y-errors given, so chi^2 == SSE and 'P > chi^2' will not be a realn"
" take a look at. Pass yerr= for a significant lowered chi^2.")
print("-" * 76)
class nsl_fit:
@staticmethod
def match(mannequin, x, y, p0, yerr=None, bounds=None, paramNames=None, conf=.95):
"""GSL's multifit_nlinear == scipy least_squares(technique='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: (mannequin(x, *p) - y) / sig
kw = dict(max_nfev=500*len(p0), **({"technique": "lm"} if bounds is None
else {"bounds": bounds}))
out = optimize.least_squares(res, p0, **kw)
p = out.x; n, okay = len(y), len(p); dof = max(n - okay, 1); r = y - mannequin(x, *p)
sse = float((r**2).sum()); chisq = float(((r / sig)**2).sum()); pink = chisq / dof
attempt: cov = np.linalg.inv(out.jac.T @ out.jac)
besides np.linalg.LinAlgError: cov = np.linalg.pinv(out.jac.T @ out.jac)
cov = cov * (pink if yerr is None else 1.0); err = np.sqrt(np.abs(np.diag(cov)))
television = np.divide(p, err, out=np.full_like(p, np.inf), the place=err > 0)
sst = float(((y - y.imply())**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*okay - 2*logL
gof = {"sum sq. residuals": sse, "imply squared error": sse/n, "root MSE": math.sqrt(sse/n),
"imply abs. error": float(np.abs(r).imply()), "residual std dev": math.sqrt(sse/dof),
"R^2": r2, "adjusted R^2": 1 - (1-r2)*(n-1)/dof, "chi^2": chisq,
"lowered chi^2": pink, "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*okay*(okay+1)/max(n-k-1, 1), "BIC": okay*math.log(n) - 2*logL}
return FitConsequence(paramNames or [f"p{i}" for i in range(k)], p, err, television,
2 * stats.t.sf(np.abs(television), 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(mannequin, x, res, degree=.95, eps=1e-7):
"""Delta technique sqrt(diag(J C J^T)) * t -- LabPlot's CI overlay."""
p = res.values; J = np.empty((len(x), len(p)))
for i in vary(len(p)):
dp = np.zeros_like(p); dp[i] = eps * max(abs(p[i]), 1)
J[:, i] = (mannequin(x, *(p + dp)) - mannequin(x, *(p - dp))) / (2 * dp[i])
v = np.einsum("ij,jk,ik->i", J, res.cov, J)
return stats.t.ppf(.5 + degree/2, res.dof) * np.sqrt(np.most(v, 0))
@staticmethod
def distributionFitML(information, dist="norm"):
"""nsl_fit_algorithm_ml -- max-likelihood distribution match (the SDK demo)."""
d = getattr(stats, dist); pr = d.match(information); ks = stats.kstest(information, dist, args=pr)
ll = float(d.logpdf(information, *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 principle numerical evaluation kernels that permit us easy, differentiate, combine, remodel, filter, cut back, and examine scientific alerts. We add peak detection alongside with Gaussian and Lorentzian fashions for superior curve evaluation. We additionally construct nonlinear becoming utilities that calculate parameter uncertainties, confidence intervals, goodness-of-fit statistics, and maximum-likelihood distribution matches.
THEMES = {
"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, title, x=None, y=None, lineStyle="-", lineWidth=1.6,
symbolStyle=None, symbolSize=4., shade=None, alpha=1., zorder=2):
tremendous().__init__(title)
self.xColumn, self.yColumn, self.shade, self.alpha = x, y, shade, 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, shade):
c = self.shade or shade; X, Y = self._v(self.xColumn), self._v(self.yColumn)
if self.fillBetween will not be None:
ax.fill_between(X, *self.fillBetween, shade=c, alpha=.2, lw=0, zorder=self.zorder-1)
if self.yErrorColumn will not be 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, shade=c, alpha=self.alpha,
label=self._name, zorder=self.zorder, markeredgewidth=0)
class Histogram(AbstractAspect):
"""normalization: 'Count' | 'Probability' | 'CountDensity' | 'ProbabilityDensity'."""
def __init__(self, title, informationColumn=None, bins="auto", normalization="ProbabilityDensity"):
tremendous().__init__(title)
self.informationColumn, self.bins, self.normalization = informationColumn, bins, normalization
def draw(self, ax, shade):
d = (self.informationColumn.clear() if isinstance(self.informationColumn, Column)
else np.asarray(self.informationColumn, float))
ax.hist(d, bins=self.bins, shade=shade, alpha=.55, edgecolor=shade, 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, title, title=None, xLabel="x", yLabel="y", logX=False, logY=False):
tremendous().__init__(title); self.kind = CartesianPlot.Type.FourAxes
self.title, self.xLabel, self.yLabel = title or title, xLabel, yLabel
self.logX, self.logY, self.legend = logX, logY, None
self.xRange, self.yRange, self.labels = None, None, []
def setType(self, t): self.kind = t; return self
def addLegend(self, loc="greatest"): 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.youngsters): ch.draw(ax, th["cycle"][i % len(th["cycle"])])
ax.set_title(self.title, shade=th["fg"], fontsize=10.5, pad=7)
ax.set_xlabel(self.xLabel, shade=th["fg"], fontsize=9.5)
ax.set_ylabel(self.yLabel, shade=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)
4 = self.kind is CartesianPlot.Type.FourAxes
for s in ("prime", "proper"): ax.spines[s].set_visible(4)
for s in ax.spines.values(): s.set_color(th["fg"]); s.set_linewidth(.9)
ax.tick_params(which="each", route="in", colours=th["fg"], prime=4,
proper=4, labelsize=8.5)
ax.grid(True, shade=th["grid"], lw=.6, alpha=.7, zorder=0)
for t, x, y in self.labels:
ax.annotate(t, (x, y), shade=th["fg"], fontsize=7.5, ha="heart")
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, title, cols=None, figsize=(15, 8.5), dpi=110):
tremendous().__init__(title); self.themeName = "BlackOnWhite"
self.cols, self.figsize, self.dpi, self._fig = cols, figsize, dpi, None
def setTheme(self, n):
if n not in THEMES: elevate KeyError(f"themes: {listing(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, shade=th["fg"], fontsize=13, y=.995)
fig.tight_layout(rect=(0, 0, 1, .98)); self._fig = fig; return fig
def present(self):
(self.render() if self._fig is None else None); plt.present()
def exportToFile(self, path, format=None):
if self._fig is None: self.render()
fmt = (format.title.decrease() 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 will not be 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 = {
"easy": lambda x, y, factors=11, order=3:
(x, nsl_smooth.savitzky_golay(y, factors, order), {}),
"differentiate": lambda x, y, derivOrder=1, smoothPoints=0:
(x, nsl_diff.derive(x, y, derivOrder, smoothPoints), {}),
"combine": lambda x, y, technique="trapezoid", absolute=False:
(lambda c: (x, c, {"whole": float(c[-1])}))(nsl_int.combine(x, y, technique, absolute)),
"dft": lambda x, y, output="amplitude", window="rectangular":
nsl_dft.remodel(x, y, output, window) + ({},),
"filter": lambda x, y, kind="lowpass", type="butterworth", cutoff=.1, cutoff2=.3, order=3:
(x, nsl_filter.apply(x, y, kind, type, cutoff, cutoff2, order), {}),
"hilbert": lambda x, y, output="envelope": (x, nsl_hilbert.remodel(y, output), {}),
"cut back": _reduce}
def __init__(self, title, xData, yData, op, fashion=None, **opts):
tremendous().__init__(title, **(fashion or {}))
self._xin, self._yin = XYCurve._v(xData), XYCurve._v(yData)
self.op, self.opts, self.outcome = op, opts, None
self.recalculate()
def recalculate(self):
self.xColumn, self.yColumn, self.outcome =
XYAnalysisCurve.OPS[self.op](self._xin, self._yin, **self.opts)
return self
_mk = lambda op: (lambda title, x, y, fashion=None, **kw: XYAnalysisCurve(title, x, y, op, fashion, **kw))
XYSmoothCurve, XYDifferentiationCurve = _mk("easy"), _mk("differentiate")
XYIntegrationCurve = _mk("combine")
XYFourierTransformCurve, XYFourierFilterCurve = _mk("dft"), _mk("filter")
XYHilbertTransformCurve, XYDataReductionCurve = _mk("hilbert"), _mk("cut back")
class XYFitCurve(XYCurve):
"""LabPlot's centrepiece: non-linear becoming with the complete statistics desk."""
def __init__(self, title, xData, yData, mannequin, p0, paramNames=None, yerr=None,
bounds=None, npoints=800, **kw):
tremendous().__init__(title, **kw)
self._xin, self._yin = XYCurve._v(xData), XYCurve._v(yData)
self.mannequin, self.p0, self.paramNames = mannequin, 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.match(self.mannequin, 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.mannequin(xf, *self.fitResult.values); self.xColumn, self.yColumn = xf, yf
if showConfidenceInterval:
d = nsl_fit.confidenceBand(self.mannequin, 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").learn(); variety = "plain"
for magic, dec, nm in ProjectFile.MAGIC:
if blob.startswith(magic): blob, variety = dec(blob), nm; break
root = ET.fromstring(blob.decode("utf-8", "substitute"))
root = root if root.tag == "undertaking" else root.discover(".//undertaking")
if root is None: elevate ValueError("no undertaking aspect discovered")
prj = Project(os.path.basename(path), root.get("writer", ""))
prj.model = root.get("model", "?")
print(f" loaded .lml: compression={variety} model={prj.model} xmlVersion="
f"{root.get('xmlVersion','?')}")
mother and father = {c: p for p in root.iter() for c in p}
def sheet_of(n):
n = mother and father.get(n)
whereas n will not be None and n.tag != "spreadsheet": n = mother and father.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("title", "spreadsheet") if el will not be None else "sheet")
for c in cols: sp.addChild(ProjectFile._column(c))
prj.addChild(sp)
return prj
@staticmethod
def _column(el):
title = el.get("title") or subsequent(
(el.discover(t).get("title") for t in ("common", "remark")
if el.discover(t) will not be None and el.discover(t).get("title")), "Column")
rows = el.findall("row")
if rows:
uncooked = [r.text for r in sorted(rows, key=lambda r: int(r.get("index", 0)))]
else:
node = subsequent((el.discover(t) for t in ("values", "information", "double")
if el.discover(t) will not be None and el.discover(t).textual content), None)
uncooked = (node.textual content if node will not be None else el.textual content or "").cut up()
vals = []
for v in uncooked:
attempt: vals.append(float(v))
besides (TypeError, ValueError): vals.append(np.nan)
attempt: des = PlotDesignation(int(el.get("designation", 0)))
besides (ValueError, TypeError): des = PlotDesignation.NoDesignation
return Column(title, vals, designation=des)
@staticmethod
def save(undertaking, path, compression="gzip"):
root = ET.Element("undertaking", {
"model": undertaking.model, "xmlVersion": str(Project.XML_VERSION),
"fileName": os.path.basename(path), "writer": undertaking.writer,
"modificationTime": time.strftime("%Y-%m-%d %H:%M:%S")})
ET.SubElement(root, "remark").textual content = undertaking.remark
for sp in undertaking.spreadsheets():
e = ET.SubElement(root, "spreadsheet", {"title": sp.title()})
ET.SubElement(e, "common", {"rowCount": str(sp.rowCount()),
"columnCount": str(sp.columnCount())})
for col in sp.columns():
c = ET.SubElement(e, "column", {
"title": col.title(), "rows": str(col.rowCount()),
"designation": str(col.plotDesignation.worth), "mode": str(col.columnMode.worth)})
for i, v in enumerate(col.values()):
ET.SubElement(c, "row", {"index": str(i)}).textual content = repr(float(v))
xml = (b'<?xml model="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 assemble the visualization layer utilizing curves, histograms, Cartesian plots, worksheets, themes, and reusable analysis-curve objects. We join these plotting objects on to our numerical operations so we are able to recalculate processed curves and fitted fashions programmatically. We additionally implement LabPlot-style undertaking file loading and saving, together with compressed .lml codecs and spreadsheet reconstruction.
banner("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.)]
clear = 18. - .012 * (wl - 400)
for a, mu, s in TRUE: clear = clear + nsl_fit_model.gaussian(wl, a, mu, s)
counts = clear + 2.2 * np.sin(2*np.pi * wl / 3.7) + np.random.regular(0, 1.1, wl.dimension)
uncooked = os.path.be a part of(OUT, "spectrum.dat")
with open(uncooked, "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))
undertaking = Project("spectroscopy demo", "LabPlot Colab tutorial")
information = undertaking.addChild(Spreadsheet("information"))
AsciiFilter().learnDataFromFile(uncooked, information)
x, y = information.column("wavelength"), information.column("counts")
information.information()
banner("STEP 2 column statistics")
it = listing(y.statistics().objects())
for i in vary(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 perimeter; a band-reject notch removes it")
freq, amp = nsl_dft.remodel(x.values(), y.values(), "amplitude", "hann")
i0 = int(np.argmax(amp[5:])) + 5; f0 = freq[i0]
print(f" dominant element {f0:.4f} 1/nm -> interval {1/f0:.3f} nm (injected 3.700), "
f"amplitude {amp[i0]:.3f} (injected 2.200)")
yf = XYFourierFilterCurve("fringe eliminated", x, y, kind="bandreject", type="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 fact "
f"{np.std(y.values()-clean):.3f} -> {np.std(yf-clean):.3f}")
banner("STEP 5 easy + 2nd spinoff -> find peaks objectively")
easy = XYSmoothCurve("SG smoothed", x, yf, factors=41, order=3)
d2 = XYDifferentiationCurve("2nd spinoff", x, easy.yColumn, derivOrder=2, smoothPoints=61)
pk, pr = nsl_peak.discover(x.values(), -d2.yColumn, prominence=np.ptp(d2.yColumn)*.20, distance=25)
print(f" {len(pk)} peaks in -y'' | discovered " + ", ".be a part of(f"{v:7.2f}" for v in pr["positions"]) +
"n | fact " + ", ".be a part of(f"{t[1]:7.2f}" for t in TRUE))
banner("STEP 6 non-linear multi-peak match (Levenberg-Marquardt)")
NPEAK = 4
centres = np.type(pr["positions"][np.argsort(pr["heights"])[::-1][:NPEAK]])
def multi_gauss(xx, c0, c1, *p):
"""Linear baseline + NPEAK Gaussians -- the 'Custom' mannequin you'd kind in."""
out = c0 + c1 * xx
for i in vary(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 vary(NPEAK)], [])
lo = [-np.inf, -np.inf] + sum([[0., m - 12, .5] for m in centres], [])
hello = [np.inf, np.inf] + sum([[np.inf, m + 12, 40.] for m in centres], [])
match = XYFitCurve("match + 95% CI", x, yf, multi_gauss, p0, names, bounds=(lo, hello), lineWidth=2.)
match.recalculate()
match.fitResult.report("XYFitCurve :: 4 Gaussians + linear baseline")
pv = match.fitResult.values
print(f"n {'peak':<6}{'space':>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, information discount, Hilbert envelope")
base = pv[0] + pv[1] * x.values(); web = yf - base
tot = XYIntegrationCurve("cumulative", x, web, technique="simpson")
print(f" whole web sign (Simpson) {tot.outcome['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], web[m]):7.2f} (true {a:.0f})")
print(" peaks 2/3 overlap, so their numeric home windows double-count the shared space -- whichn"
" is precisely why you match a multi-peak mannequin as a substitute of integrating home windows by hand.")
pink = XYDataReductionCurve("lowered", x, easy.yColumn, tolerance=.4)
err = np.max(np.abs(np.interp(x.values(), pink.xColumn, pink.yColumn) - easy.yColumn))
env = XYHilbertTransformCurve("envelope", x, y.values() - easy.yColumn)
print(f" Douglas-Peucker tol=0.4: {pink.outcome['in']} -> {pink.outcome['out']} pts "
f"({pink.outcome['compression']*100:.1f}% dropped), max error {err:.4f}n"
f" Hilbert envelope of the eliminated fringe: imply {env.yColumn.imply():.3f} counts"
f" (injected amplitude 2.200)")
banner("STEP 8 residual diagnostics")
res = match.fitResult.residuals
ml = nsl_fit.distributionFitML(res, "norm")
dw = float(np.sum(np.diff(res)**2) / np.sum(res**2))
print(f" ML regular 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"{'constant with white Gaussian noise' if ml['KS_p']>.05 and 1.5<dw<2.5 else 'construction stays'}")
We generate a practical noisy spectroscopy dataset containing a sloping baseline, overlapping Gaussian peaks, periodic interference, and random noise. We use Fourier evaluation, band-reject filtering, smoothing, differentiation, and peak detection to isolate key spectral options earlier than becoming. We then carry out a constrained multi-Gaussian match, combine the recovered sign, cut back the info, calculate a Hilbert envelope, and statistically consider the match residuals.
banner("STEP 9 Worksheet -> CartesianPlots -> theme -> export")
ws = Worksheet("Spectroscopy evaluation", cols=3, figsize=(15, 8.5))
ws.setTheme("Dracula")
p1 = CartesianPlot("uncooked & match", "Raw spectrum + multi-Gaussian match", "wavelength (nm)", "counts")
p1.addLegend("higher proper").addChild(XYCurve("uncooked", x, y, lineWidth=.6, alpha=.45))
p1.addChild(XYCurve("fringe eliminated", x, yf, lineWidth=.9, alpha=.8)); p1.addChild(match)
p2 = CartesianPlot("parts", "Resolved parts", "wavelength (nm)", "counts")
p2.addLegend("higher proper").addChild(XYCurve("baseline", x, base, lineStyle="--", lineWidth=1.2))
for i in vary(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.remodel(x.values(), yf, "amplitude", "hann")
p3 = CartesianPlot("fft", "Amplitude spectrum (Hann)", "spatial frequency (1/nm)",
"amplitude", logY=True).addLegend("higher proper").setRange(x=(0, .6))
p3.addChild(XYCurve("uncooked", freq, np.most(amp, 1e-4), lineWidth=1.))
p3.addChild(XYCurve("filtered", f2, np.most(a2, 1e-4), lineWidth=1.))
p3.addTextLabel(f"fringen{f0:.3f} 1/nm", f0, amp[i0] * 1.6)
p4 = CartesianPlot("d2", "Second spinoff (peak detection)", "wavelength (nm)", "d2(counts)/dx2")
p4.addLegend("decrease proper").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 regular", "residual (counts)",
"chance density").addLegend("higher proper")
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.be a part of(OUT, "worksheet" + ext), f)
print(" exported worksheet.png / .pdf / .svg"); ws.present()
banner("STEP 10 undertaking tree + .lml round-trip")
r = undertaking.addChild(Spreadsheet("evaluation outcomes"))
r.appendColumn("wavelength", x.values(), PlotDesignation.X)
for n, v in (("filtered", yf), ("smoothed", easy.yColumn), ("baseline", base),
("match", multi_gauss(x.values(), *pv)),
("residuals", yf - multi_gauss(x.values(), *pv)), ("cumulative", tot.yColumn)):
r.appendColumn(n, v)
pp = undertaking.addChild(Spreadsheet("match parameters"))
pp.appendColumn("worth", pv)
pp.appendColumn("error", match.fitResult.errors, PlotDesignation.YError)
undertaking.addChild(ws)
print(undertaking.tree())
for c, ext in (("gzip", ".lml.gz"), ("xz", ".lml.xz"), ("none", ".lml")):
f = ProjectFile.save(undertaking, os.path.be a part of(OUT, "spectroscopy" + ext), c)
print(f" saved {os.path.basename(f):<24}{os.path.getsize(f)/1024:>8.1f} kB ({c})")
again = ProjectFile.load(os.path.be a part of(OUT, "spectroscopy.lml.gz"))
o, b = y.values(), again.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.toDataBody().to_csv(os.path.be a part of(OUT, "analysis_results.csv"), index=False)
We manage the spectroscopy outcomes right into a themed worksheet containing the uncooked spectrum, fitted parts, Fourier spectrum, detected peaks, and residual distribution. We export the whole visualization to PNG, PDF, and SVG codecs so we’ve reusable graphical outputs. We additionally retailer our processed measurements and fitted parameters contained in the undertaking, save them in a number of .lml codecs, and confirm that the undertaking information survives a whole spherical journey.
banner("STEP 11 batch: import -> filter -> match -> secondary match")
bd = os.path.be a part of(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.be a part of(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.regular(0, 1.1, wl.dimension)))
def analyse(path):
sp = Spreadsheet(os.path.basename(path)); AsciiFilter().learnDataFromFile(path, sp)
q = XYFitCurve("match", sp.column(0), sp.column(1), multi_gauss, p0, names, bounds=(lo, hello))
.recalculate(showConfidenceInterval=False).fitResult
return {"file": os.path.basename(path), "space": 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.DataBody([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)} recordsdata in {time.perf_counter()-t0:.2f} s")
arr = nsl_fit.match(lambda T, A, T0: A * np.exp(-(T-20)/T0), df.T_C.values, df.space.values,
[140., 140.], yerr=df.area_err.values, paramNames=["A", "T0"])
arr.report("Secondary match :: peak-3 space vs temperature")
print(f" injected quench fixed 140.0 -> recovered {arr.values[1]:.1f} +- {arr.errors[1]:.1f}")
ws2 = Worksheet("Batch outcomes", figsize=(6.5, 4.2)); ws2.setTheme("SolarizedDark")
q1 = CartesianPlot("space", "Peak space vs temperature", "temperature (C)", "fitted space").addLegend()
c = XYCurve("measured", df.T_C.values, df.space.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.be a part of(OUT, "batch_results.png")); ws2.present()
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 identical workflow on the actual pylabplot SDK")
print(textwrap.dedent("""
from pylabplot import * # each title under is equivalent
spreadsheet = Spreadsheet("information")
AsciiFilter().learnDataFromFile("spectrum.dat", spreadsheet)
worksheet = Worksheet("worksheet"); plotArea = CartesianPlot("plot space")
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("match"); matchData = fitCurve.matchData()
matchData.modelCategory = nsl_fit_model_peak; matchData.mannequinType = nsl_fit_model_gaussian
matchData.diploma = 4
XYFitCurve.initFitData(matchData); fitCurve.setFitData(matchData); fitCurve.recalculate()
worksheet.setTheme("Dracula")
worksheet.exportToFile("outcome.pdf", Worksheet.ExportFormat.PDF)
# pylabplot ships INSIDE a LabPlot set up, not on PyPI, and upstream nonetheless marks
# the SDK experimental -- no API/ABI stability assure but.
""").strip())
banner("accomplished")
for f in sorted(os.listdir(OUT)):
if os.path.isfile(q := os.path.be a part of(OUT, f)):
print(f" {f:<26}{os.path.getsize(q)/1024:>9.1f} kB")
if IN_COLAB: print(f"n from google.colab import recordsdata; recordsdata.obtain('{OUT}/worksheet.pdf')")
We lengthen the workflow from a single spectrum to a batch of temperature-dependent artificial measurements and routinely analyze each file. We extract fitted peak areas and facilities, carry out a secondary exponential match, visualize the temperature dependence, and measure the recovered spectral drift. We lastly join our emulated workflow to equal pylabplot SDK ideas and listing the generated tutorial outputs.
In conclusion, we constructed a whole scientific evaluation pipeline that mirrors a lot of LabPlot’s core ideas whereas letting us run the workflow straight in Python. We moved from structured information import and statistical inspection to sign processing, Fourier-domain filtering, peak detection, nonlinear multi-peak becoming, integration, residual evaluation, visualization, undertaking serialization, and automated batch processing. By combining these levels, we are able to remodel noisy experimental measurements into interpretable parameters, publication-ready plots, reusable undertaking outputs, and higher-level tendencies equivalent to temperature-dependent peak conduct. We additionally established a sensible bridge between the Python implementation and the actual pylabplot SDK, giving us a basis for transferring the identical workflow to a local LabPlot setting.
Check out the FULL CODES here. Also, be at liberty to comply with us on Twitter and don’t overlook to affix 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 associate with us for selling your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar and so on.? Connect with us
The put up Scientific Data Analysis with LabPlot in Python: Signal Processing, Spectral Peak Fitting, Visualization, and Batch Automation appeared first on MarkTechPost.
