# Fine-Structure Analyzer ```{toctree} :hidden: :maxdepth: 2 ``` ## 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: ```text 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: ```text 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 ```bash git clone https://gitlab.com/plesty/hub/analyzers/plesty-fss.git cd plesty-fss uv sync ``` ## Running the analyzer ```python 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. ```python 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]"`). ```python 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: ```python 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.