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:

  1. plesty.lib.analyzer.SpectrumFit fits every line in every frame, in angle order so each frame’s fit seeds the next.

  2. plesty.fss.fit_sine fits 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

fine_structure

FineStructureData (kind fine_structure)

one entry per line, below

fit

SpectrumFitData

the line fits, (frames, peaks)

curve

array (frames, peaks)

fitted position at each fitted frame’s angle, to plot against fit["center"]

FineStructureData arrays, each of shape (peaks,):

Array

Unit

Meaning

fss, fss_err

µeV

fine-structure splitting

fss_wavelength, fss_wavelength_err

wavelength unit

the splitting as a wavelength difference

axis_deg, axis_deg_err

deg

rotator angle of the longer-wavelength line

mean_center, mean_center_err

wavelength unit

mean line position over the sweep

r_squared

—

coefficient of determination of the sweep fit

n_points

—

sweep points the fit used

fwhm

wavelength unit

median fitted linewidth over the sweep

fss_valid

—

1 when fss_wavelength < fwhm, so fss is a fine-structure splitting; else 0

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

LineSpectrumView

the newest spectrum, with a marker on every line

FineStructureView

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.