Fine-Structure Analyzer
Overview
FineStructure measures the exciton fine-structure splitting (FSS) of every line in a
polarization-resolved PL sweep: one spectrum per rotator angle, the angle recorded as a
frame coordinate.
The two components of a fine-structure doublet are linearly polarized at right angles. While they are closer together than the linewidth, the spectrometer shows them as one line. Behind a rotating analyser the weight of the two components changes with the angle, so the fitted position of that line oscillates between them, and the splitting is the peak-to-peak amplitude of the oscillation:
centre(θ) = mean + A·cos(k·(θ − axis)) FSS = 2·A
k is 4 for a half-wave plate (rotator="hwp", 90° period) and 2 for a rotating polarizer
(rotator="polarizer", 180° period).
A quarter-wave plate in front of a fixed polarizer (rotator="qwp") also repeats every
90°, but shows the doublet at half the contrast. A component linearly polarized at φ passes
it with weight ½[1 + ½·cos 2φ + ½·cos(4θ − 2φ)], so the line’s position swings by only
half the splitting, around a mean shifted by a constant the fit absorbs:
qwp: centre(θ) = mean + A·cos(4·(θ − axis)) FSS = 2·A / ½ = 4·A
The analysis has two stages:
plesty.lib.analyzer.SpectrumFitfits every line in every frame, in angle order so each frame’s fit seeds the next.plesty.fss.fit_sinefits each line’s centre against the angle, a weighted linear least-squares solve that needs no starting values.
The splitting is reported in µeV, ΔE = hc·Δλ/λ² at the mean position, and in the
spectrum’s wavelength unit. Both come with a one-sigma error that propagates the full
covariance of the sine fit.
When the method applies
The oscillation of the fitted position measures a doublet only while its two components are
closer together than the linewidth and appear as one line. The position of such a line
cannot move by more than its linewidth, so a line whose fss_wavelength reaches its fwhm
is marked fss_valid = 0 and named in a warning. Its fit followed a different line from
frame to frame — typically the other component of a doublet whose components are further
apart than the linewidth, or a neighbouring line inside the fit window — and its fss is not
a splitting.
A doublet whose components are further apart than the linewidth shows as two separate lines
at fixed positions whose intensities alternate with the rotator angle, with maxima 45° of
half-wave-plate angle apart. Its splitting is the distance between the two lines, which this
analyzer does not report yet. Fit neighbouring lines together (the SpectrumFit default
overlap) rather than one at a time: a single-line fit whose window reaches a neighbour can
lock onto it.
Installation
git clone https://gitlab.com/plesty/hub/analyzers/plesty-fss.git
cd plesty-fss
uv sync
Running the analyzer
from plesty.lib.data import SpectrumData
from plesty.fss import FineStructure
spectrum = SpectrumData.from_run(run, coordinates=("hwp_deg",))
outputs = FineStructure(
angle="hwp_deg",
rotator="hwp",
fit_options={"peaks": [775.0], "window": 0.3, "center_tolerance": 0.05},
)(spectrum=spectrum)
fit_options takes any SpectrumFit keyword. Without order, frames are fitted in angle
order.
Outputs
Output |
Type |
Content |
|---|---|---|
|
|
one entry per line, below |
|
|
the line fits, |
|
array |
fitted position at each fitted frame’s angle, to plot against |
FineStructureData arrays, each of shape (peaks,):
Array |
Unit |
Meaning |
|---|---|---|
|
µeV |
fine-structure splitting |
|
wavelength unit |
the splitting as a wavelength difference |
|
deg |
rotator angle of the longer-wavelength line |
|
wavelength unit |
mean line position over the sweep |
|
— |
coefficient of determination of the sweep fit |
|
— |
sweep points the fit used |
|
wavelength unit |
median fitted linewidth over the sweep |
|
— |
1 when |
Reporting an analysis
FineStructure.report(outputs) returns a plesty.lib.report.Report of three blocks: a
sentence naming the lines that split and by how much, a table with one row per line (the
splitting as µeV and pm under one heading, the polarization axis, the linewidth), and the
sweep drawn as the live view draws it. A line whose position moves by at least its
linewidth is marked as not a splitting; a splitting below three standard errors has no axis
to quote. Only fine_structure and fit are read, so a report can be rebuilt from the two
saved outputs.
report = analyzer.report(outputs)
report.save(analysis_dir / "QD-002.report.json") # beside the outputs
html, latex = report.to_html(), report.to_latex()
Plotting an analysis
plesty.fss.plot.plot_fine_structure(fine_structure, fit) draws one column per line: the
line’s fitted position as a photon-energy shift in µeV from its mean position against the
rotator angle, with error bars, the fitted sine and the splitting and axis (top), and the
residuals of that fit in units of each point’s error with guides at ±2 (bottom). A line with
fss_valid = 0 is drawn the same way and labelled as not a splitting. It needs the plot
extra (uv add "plesty-fss[plot]").
from plesty.fss.plot import figure_png, plot_fine_structure
figure = plot_fine_structure(outputs["fine_structure"], outputs["fit"], lines=[0, 2])
figure.savefig("fine_structure.png", dpi=150)
png = figure_png(figure) # bytes, e.g. for plesty.lib.data.save_result
Watching a sweep
plesty.fss.monitor has two views for an experiment’s viewer (plesty.lib.monitor.Viz).
Each line has the same name and colour in both — line 0 is the shortest wavelength:
View |
Shows |
|---|---|
|
the newest spectrum, with a marker on every line |
|
each line’s position as an energy shift against the rotator angle, and each line’s result under the plot in the line’s colour |
Both fill while the sweep is measured. A LineTracker finds the lines the way the analysis
does, with the same fit options — given peaks, or found in the spectra so far with
prominence — and fits each spectrum as it arrives. After the last angle, the experiment’s
analysis record replaces the points with the analysis: the fits of the whole sweep, the
fitted sines and the splittings.
An experiment adds them to its Viz by name, saying which of its record fields hold the
sweep. Nothing else is configured:
from plesty.lib.monitor import Viz
from plesty.fss.monitor import FineStructureView, LineSpectrumView, LineTracking
viz = Viz("Polarization PL", experiment="pol_pl", reads=read_spectrum)
lines = LineTracking(angle="hwp_deg", label="qd")
viz.add_plot(LineSpectrumView(lines, weight=3.0))
viz.add_plot(FineStructureView(lines, angle_label="HWP angle (deg)", weight=3.0))
read_spectrum is the experiment’s own: a record names the file the spectrometer wrote, and
it turns that path into the wavelength and counts columns the views read. LineTracking
takes its fit options from the run’s frozen configuration — under analysis.fit_options,
where an experiment that runs FineStructure keeps them — so the lines followed during the
sweep are the lines reported after it. Pass options= to say them outright.
Find lines in the median (average: median): early in a sweep the mean covers few spectra,
and a cosmic ray in one of them passes as a line. One LineTracking serves both views, so
each spectrum is fitted once. When a line is found or lost mid-sweep, the positions already
fitted stay with their line and the numbering follows the wavelength order. A run whose
configuration gives neither peaks nor prominence is not tracked at all: the spectrum
still draws, without markers.
metadata records the rotator, the angle coordinate, its period_deg, and the
rotator’s contrast (½ for a quarter-wave plate, else 1), by which the measured
oscillation was divided to give fss.
FineStructureData.evaluate(angles) rebuilds the fitted positions at any angles, for a
smooth curve.
Further fine-structure quantities are added as fields of fine_structure with a
schema_version bump, never as new outputs.
API reference
The API Reference pages in the navigation are generated automatically from
the package sources by plesty docs — you do not edit them by hand.