FcuCalibration

FcuCalibration is the assembled, validated form of a Sirah FCU-style doubling-crystal tuning curve: a pure value type, hardware-free like LifConversion (its sibling in data/lif/), that maps a fundamental wavelength (nm) to and from a motor position (steps) under one of three interchangeable schemes. One of the static factories — physical(), polynomial(), or spline() — is the sole validating construction path; a default-constructed instance is the unconfigured, always-invalid case, since (unlike an empty LifConversion topology) there is no meaningful identity calibration for a doubling crystal. wavelengthToPos()/posToWavelength() delegate to the active scheme and are defined for any finite input regardless of isValid() — a malformed calibration evaluates to mathematically well-defined (if meaningless) numbers, and an evaluation that cannot be carried out for a structural reason (an unbracketed Physical root find, an out-of-domain Spline query) returns NaN rather than aborting. isValid()/errorString() report whether the calibration was assembled from well-formed input, not whether a particular evaluation succeeded.

SirahFcu::hwReadSettings() is the only current caller: it builds and caches a FcuCalibration from the driver’s registered calibrationScheme setting and the active scheme’s settings, and setPos()/readPos() delegate to it. See LIF Conversion Stage for the schemes from a user’s point of view — when to choose each, their user-visible settings, and the offline python/tools/ workflow (fcu_measure.py / fcu_fit.py) that fits the parameters or curves Blackchirp then imports or has typed in directly.

Schemes

Scheme::Physical evaluates a best-effort Type-I second-harmonic phase-match law for a BBO or KDP crystal (CrystalType) — the Eimerl (1987) and Zernike (1964) Sellmeier equations respectively, with a linear dispersion knob against an arbitrary-units temperature parameter — combined with the sine-bar drive’s mechanics and a crystal-face refraction term. phaseMatchAngleDeg() is exposed publicly so the phase-match law can be validated in isolation (for example, against the Sirah Autotracker service manual’s Table 6-1 cut angles) independent of the sine-bar geometry. The inverse direction (position → wavelength) is a bracketed 1-D root find over the crystal’s phase-matchable fundamental band, since the forward map has no closed-form inverse.

Scheme::Polynomial and Scheme::Spline both evaluate imported, already-fitted data rather than a physical law: Polynomial is Horner evaluation of separately-imported forward and inverse coefficient lists (ascending order, so neither direction needs a root find); Spline builds two monotone (gsl_interp_steffen) interpolating splines from an imported (wavelength, position) point table — one keyed by wavelength, one by position — since the mapping must be invertible in both directions and a single spline object is keyed by one axis only.

The physical model

The Physical scheme composes three pieces: crystal dispersion (Sellmeier equations), the Type-I phase-match condition, and the sine-bar drive geometry. The equations below are best-effort literature values, fit to the user’s own calibration data rather than matched bit-for-bit to any vendor curve.

Sellmeier equations. The ordinary and extraordinary refractive indices come from the crystal’s Sellmeier equation (wavelength \(\lambda\) in micrometres) with a linear thermo-optic term applied to \(n\) about a 293-unit reference:

\[n(\lambda, T) = \sqrt{n^2(\lambda)} + \frac{dn}{dT}\,(T - 293)\]

For BBO (Eimerl, 1987), with \(dn_o/dT = -16.6\times10^{-6}\) and \(dn_e/dT = -9.3\times10^{-6}\):

\[n_o^2 = 2.7405 + \frac{0.0184}{\lambda^2 - 0.0179} - 0.0155\,\lambda^2\]
\[n_e^2 = 2.3730 + \frac{0.0128}{\lambda^2 - 0.0156} - 0.0044\,\lambda^2\]

For KDP (Zernike, 1964), with \(dn_o/dT = -3.4\times10^{-5}\) and \(dn_e/dT = -2.4\times10^{-5}\):

\[n_o^2 = 2.259276 + \frac{0.01008956}{\lambda^2 - 0.012942625} + \frac{13.00522\,\lambda^2}{\lambda^2 - 400}\]
\[n_e^2 = 2.132668 + \frac{0.008637494}{\lambda^2 - 0.012281043} + \frac{3.2279924\,\lambda^2}{\lambda^2 - 400}\]

The temperature parameter is in arbitrary units — a dispersion fit knob rather than a controlled physical temperature — and enters only through the thermo-optic term above.

Phase-match condition. For Type-I second-harmonic generation in a negative uniaxial crystal, the phase-match angle \(\theta_{pm}\) (between the beam and the crystal optic axis) satisfies

\[\sin^2\theta_{pm} = \frac{n_o(\lambda)^{-2} - n_o(\lambda/2)^{-2}} {n_e(\lambda/2)^{-2} - n_o(\lambda/2)^{-2}}\]

with the indices evaluated at the fundamental \(\lambda\) and its second harmonic \(\lambda/2\). phaseMatchAngleDeg() returns this angle (clamped to \([0, 90]\) outside the phase-matchable band).

Sine-bar geometry. A lead screw drives a sine bar that rotates the crystal. With cut angle \(\theta_c\), linear offset \(L_0\), angle offset \(\alpha_0\), lever length \(\ell\), screw pitch \(s\), motor resolution \(r\), and sign \(\sigma = \pm 1\) from the invert flag, the forward map from fundamental wavelength to motor position \(p\) is

\[\begin{split}\alpha_\mathrm{int} &= \sigma\,(\theta_{pm} - \theta_c) \\ \alpha_\mathrm{ext} &= \arcsin\!\big(n_o(\lambda)\,\sin\alpha_\mathrm{int}\big) \\ x &= L_0 - \ell\,\sin(\alpha_0 - \alpha_\mathrm{ext}) \\ p &= \frac{r}{s}\,x\end{split}\]

The \(\alpha_\mathrm{int}\!\to\!\alpha_\mathrm{ext}\) step is Snell refraction at the crystal face (using the ordinary index at the fundamental), which makes the cut angle and angle offset independently identifiable rather than degenerate. The inverse direction (position to wavelength) has no closed form and is a bracketed 1-D root find over the crystal’s phase-matchable band.

API Reference

class FcuCalibration

Pure value type representing an assembled FCU tuning curve: the doubling crystal’s fundamental wavelength (nm) <-> motor position (steps) mapping, in either direction.

Hardware-free and copyable, like LifConversion (data/lif/lifconversion.h), its sibling in this directory: callers build a FcuCalibration from a settings snapshot (or, in tests, directly from the static factories below) and hand it to a driver without that driver touching the tuning math itself. One of physical(), polynomial(), or spline() is the only validating construction path; a default-constructed instance is the unconfigured, always-invalid case (there is no meaningful identity calibration the way an empty LifConversion topology is the identity conversion).

wavelengthToPos() / posToWavelength() are defined for any finite input regardless of isValid() — malformed coefficients evaluate to mathematically well-defined (if meaningless) numbers, and an evaluation that cannot be carried out for a structural reason (a Physical fundamental outside the crystal’s phase-matchable band, an unbracketed Physical root find, an out-of-domain Spline query) returns NaN rather than aborting or throwing. Polynomial has no such structural bound — imported coefficient lists carry no fit-domain metadata, so Horner evaluation extrapolates freely for any finite input. isValid() reports whether the calibration was assembled from well-formed input, not whether a particular evaluation succeeded.

Public Functions

FcuCalibration()

Construct the unconfigured, invalid calibration. Use one of the static factories to build a usable instance.

double wavelengthToPos(double lamNm) const

Fundamental wavelength (nm) -> motor position (steps), per the active scheme. See the class comment for the no-isValid() per-call convention.

double posToWavelength(double pos) const

Motor position (steps) -> fundamental wavelength (nm), per the active scheme. See the class comment for the no-isValid() per-call convention.

Physical inverts by a bracketed 1-D root find over the crystal’s phase-matchable fundamental band; Polynomial is Horner evaluation of the inverse coefficient list; Spline evaluates the position-keyed interpolating spline. Returns NaN if pos cannot be inverted (no bracket found, or outside the spline’s domain).

bool isValid() const

true iff this instance was assembled from well-formed input by one of the static factories.

QString errorString() const

Populated iff !isValid(), explaining why assembly failed.

Public Static Functions

static FcuCalibration physical(BC::FcuCal::CrystalType crystal, double cutAngleDeg, double temperature, double linearOffsetMm, double angleOffsetDeg, double screwPitchMm, double leverLengthMm, double motorResolution, bool invert)

Assemble the Physical scheme: a Type-I SHG phase-match law for crystal (see phaseMatchAngleDeg()) combined with the sine-bar drive mechanics and a crystal-face refraction term.

cutAngleDeg is the crystal’s optic-axis-to-face cut angle (Table 6-1 of the Sirah Autotracker service manual, per crystal/band). temperature is an arbitrary-units dispersion fit knob (see the Sellmeier comment in the .cpp), not a controlled physical temperature; it defaults to 293 in phaseMatchAngleDeg() but must be supplied explicitly here. linearOffsetMm, angleOffsetDeg, screwPitchMm, leverLengthMm, and motorResolution are the sine-bar drive’s mechanical constants. invert selects the phase-match relation’s ± branch. Invalid (non-finite parameters, or a zero screw pitch) yields isValid()==false with errorString() explaining why.

static FcuCalibration polynomial(std::vector<double> forwardCoeffs, std::vector<double> inverseCoeffs)

Assemble the Polynomial scheme from imported forward (wavelength (nm) -> position) and inverse (position -> wavelength (nm)) coefficient lists, ascending order (c0 + c1*x + c2*x^2 + …). Evaluated by Horner’s method. Invalid (either list empty) yields isValid()==false.

static FcuCalibration spline(std::vector<std::pair<double, double>> points)

Assemble the Spline scheme from an imported (wavelength (nm), position) point table. Builds two monotone (gsl_interp_steffen) interpolating splines internally — one keyed by wavelength for wavelengthToPos(), one keyed by position for posToWavelength() — since the mapping must be invertible in both directions.

points need not be pre-sorted. Invalid (fewer than GSL’s minimum point count for a Steffen spline, duplicate wavelengths, or a position sequence that is not strictly monotone in wavelength order and therefore not invertible) yields isValid()==false.

static double phaseMatchAngleDeg(BC::FcuCal::CrystalType crystal, double lamFundNm, double temperature = 293.0)

Type-I second-harmonic phase-match angle (degrees) for crystal at fundamental wavelength lamFundNm (nm) and dispersion-fit temperature (arbitrary units; see the Physical factory comment).

Exposed publicly so it can be validated in isolation against the Sirah Autotracker service manual’s Table 6-1 cut angles. The underlying sin^2(theta_pm) expression is clamped to [0,1] before the asin(), so this always returns a value in [0,90] even outside the crystal’s physically phase-matchable band (where it saturates at the boundary rather than reporting a domain error).

Private Functions

double physicalForward(double lamNm) const

Fundamental wavelength (nm) -> motor position (steps), Physical scheme. The building block posToWavelength()'s root find brackets.

double physicalInverse(double pos) const

Motor position (steps) -> fundamental wavelength (nm), Physical scheme, by bracketed bisection of physicalForward().

double splineEval(const std::shared_ptr<gsl_spline> &spline, double domainMin, double domainMax, double x) const

Evaluate spline at x, returning NaN if spline is null (an invalidly-assembled instance) or x falls outside [domainMin, domainMax]. GSL’s own domain check on gsl_spline_eval() routes through the global error handler, which aborts the process unless the application has disabled it (as main.cpp does); a value type usable from a hardware-free unit test cannot rely on that, so this guards the domain itself and never calls into GSL out of range.

Private Members

BC::FcuCal::Scheme d_scheme = {BC::FcuCal::Scheme::Physical}
bool d_valid = {false}
QString d_errorString
PhysicalParams d_physical
std::vector<double> d_forwardCoeffs

Polynomial: wavelength (nm) -> position.

std::vector<double> d_inverseCoeffs

Polynomial: position -> wavelength (nm).

std::shared_ptr<gsl_spline> ps_wavelengthToPosSpline

Spline: wavelength (nm) -> position, keyed by wavelength. shared_ptr (custom deleter gsl_spline_free) rather than unique_ptr so FcuCalibration stays a cheaply-copyable value type; gsl_spline itself is not copyable and evaluation is not a hot loop (once per acquisition point), so sharing the underlying spline across copies rather than deep-copying it is the simpler and sufficient choice.

std::shared_ptr<gsl_spline> ps_posToWavelengthSpline

Spline: position -> wavelength (nm), keyed by position.

double d_splineWavelengthMin = {0.0}
double d_splineWavelengthMax = {0.0}
double d_splinePosMin = {0.0}
double d_splinePosMax = {0.0}
struct PhysicalParams

Physical scheme parameters, named and grouped exactly as the forward-map formula (see the .cpp) consumes them.

Public Members

BC::FcuCal::CrystalType crystal = {BC::FcuCal::CrystalType::BBO}
double cutAngleDeg = {0.0}
double temperature = {293.0}
double linearOffsetMm = {0.0}
double angleOffsetDeg = {0.0}
double screwPitchMm = {1.0}
double leverLengthMm = {0.0}
double motorResolution = {1.0}
bool invert = {false}
enum class BC::FcuCal::Scheme

Which tuning-curve model a FcuCalibration was assembled from.

Values:

enumerator Physical

Best-effort Type-I SHG phase-match + sine-bar mechanics.

enumerator Polynomial

Imported forward/inverse coefficient lists (Horner evaluation).

enumerator Spline

Imported (wavelength, position) point table (GSL monotone splines).

enum class BC::FcuCal::CrystalType

Doubling-crystal species supported by the Physical scheme; picks the Sellmeier equations used for the phase-match calculation.

Values:

enumerator BBO

Beta barium borate (Eimerl 1987 Sellmeier).

enumerator KDP

Potassium dihydrogen phosphate (Zernike 1964 Sellmeier).