Model Fitting

Parametric models

FunctionDescription
dict_to_model(model_dict, list_free_params)Compile a flat parameter dict into a FlatModel
model_to_vis(model, x, uv)Evaluate complex visibilities for a model (alias: eval_model)
eval_model_grad(model, x, uv)Evaluate visibilities + Jacobian
display_model(model_dict, list_free_params)Pretty-print model parameters
fit_model(model, x0, data)Fit a FlatModel via NLopt
fit_model_lsqfit(model, x0, data)Fit a FlatModel via Levenberg-Marquardt
fit_model_nested(model, data; lb, ub)Fit by nested sampling — posterior and log-evidence; needs finite bounds
fit_model_ultranest(model, data; lb, ub)fit_model_nested pinned to the UltraNest backend
nested_backend(), set_nested_backend!(b)Which nested sampler is in force: :nestedsamplers or :ultranest

Which package each fitter needs is tabulated under What each optimiser needsfit_model, fit_model_lsqfit, chi2_map and bootstrap_fit need nothing beyond OITOOLS; fit_model_nested needs a sampler. | chi2_map(model_dict, free, data, p1, p2) | Grid-search two free parameters and return the χ² surface; needs no gradient and no starting guess | | model_to_obs(model, x, data) | Compute observables (V², T3amp, T3phi) from a model | | model_to_residuals(model, x, data) | Compute normalised residuals (model - data) / error | | model_to_chi2(model, x, data) | Compute weighted chi² (alias: chi2_flat) | | model_to_chi2_fg(model, x, data) | Compute chi² + gradient (alias: chi2_flat_fg) | | model_to_image(model, x; nx, pixsize) | Synthesize a model image via inverse FFT | | model_to_sed(model, x, wl_grid) | Compute spectral energy distribution | | model_to_flux(model, x; wl) | Total flux at zero baseline: real(V(0,0)) |

Bounds, constraints and model files

lb/ub describe a box, one parameter at a time. A relation between parameters — a ring's outer diameter exceeding its inner one, two flux fractions summing to one — is not a box, and needs ModelConstraint.

FunctionDescription
default_bounds(model_dict, free; data, max_size)Suggested lb/ub per free parameter; pass data and angular sizes are capped at 2 λ/B_min from the actual uv coverage
max_angular_scale(data)The largest angular scale the shortest baseline senses, in mas
ModelConstraint(lhs, op, rhs; tol)A relation between parameters, with op one of <, <=, >, >=, =
parse_constraints(specs)Accept constraints as ModelConstraints, (lhs, op, rhs[, tol]) tuples (the PMOIRED layout) or dicts
check_constraints(constraints, model_dict)Which constraints the starting model already satisfies
read_model_file(path)Read a TOML model file into (; model, free, lb, ub, constraints, priors, name)
write_model_file(path, model_dict; free, lb, ub, constraints, priors)Write all of that back out

fit_model hands constraints to NLopt as real nonlinear constraints, so they hold at the optimum rather than being encouraged there; an algorithm that cannot take them (including the default :LD_LBFGS) is wrapped in :AUGLAG rather than replaced. fit_model_lsqfit and fit_model_nested have no such machinery and use a one-sided quadratic penalty on the normalised violation, matching PMOIRED's prior list — soft, and so able to lose to a steep χ². The distinction is worth knowing before choosing a fitter for a constrained model.

A model dict alone does not describe a fit: the free list, the bounds, the constraints and the priors all change the answer and none of them lived in a file before. A TOML model file carries all five.

free = ["star,ud", "disk,pa"]

[model]
"star,ud"   = 6.5
"disk,fwhm" = "$star,ud * 3"

[bounds]
"star,ud" = [0.0, 20.0]

[[constraints]]
param = "disk,diamout"
op    = ">"
value = "disk,diamin"
tol   = 0.001

[[priors]]
expr   = "star,ud"
target = 6.0
sigma  = 0.5
m = read_model_file("binary.toml")
res = fit_model(m.model, m.free, data; m.lb, m.ub, m.constraints, m.priors)

free is a top-level key rather than a member of [model] so that [model] mirrors the model dict exactly — a model may hold a bare global key, and one named free would otherwise be eaten by the free-parameter list.

Uncertainty estimation by resampling

FunctionDescription
bootstrap_fit(model_dict, list_free_params, data)Nonparametric block bootstrap: refit replicates in which blocks of data are resampled
bootstrap_driver(fitfun, x_opt, list_free_params)The model-agnostic replicate loop and statistics behind bootstrap_fit, for callers with their own fitter or resampling unit
data_blocks(data; granularity)Partition data into resampling blocks (:config, :epoch, :point)
resample_blocks(data, blocks; mode)One bootstrap replicate (:replacement, :halfsample, :weights; :pmoired reproduces PMOIRED's scheme and is biased low by √2)
block_counts(nblocks, mode)Block multiplicities drawn by a resampling scheme
block_weights(nblocks)Continuous block weights for the multiplier (Bayesian) bootstrap
apply_block_weights(data, blocks, w)Build the weighted replicate (error bars scaled by 1/√w)
apply_block_counts(data, blocks, counts)Build the replicate for given block multiplicities
perturb_data(data)Add Gaussian noise drawn from the error bars — a simulation utility, not an uncertainty estimator (was resample_data)

bootstrap_fit resamples which observations are used, in blocks of (MJD, telescope configuration), and therefore responds to correlated calibration errors and to mis-stated error bars — neither of which the analytic covariance of fit_model_lsqfit can see. See the model-fitting examples page, and demos/bootstrap_validation for the calibration test behind that statement.

OITOOLS.dict_to_modelFunction
dict_to_model(model_dict, list_free_params; nB_workspace=100) -> FlatModel

Compile a flat parameter dict into a FlatModel ready for evaluation.

Arguments

modeldict : Dict{String,Any} with "component,param" keys. Values may be Float64 (fixed or free) or String (expression). Special string key "component,profile" triggers the Hankel pathway. listfreeparams : Vector{String} of keys to optimize (must be numeric in modeldict). nB_workspace : Pre-allocated baseline grid size for HankelWorkspace buffers. Should be ≥ the number of baselines you will evaluate at.

Returns

A FlatModel whose evalmodel / evalmodel_grad methods can be called in a loop with zero Dict allocation.

Example

model_dict = Dict(
    "star,ud"      => 0.8,
    "star,f"       => 0.6,
    "ring,profile" => "exp(-(\$R / \$scale)^2 / 2)",
    "ring,scale"   => 1.5,
    "ring,udout"   => 12.0,
    "ring,f"       => "1 - \$star,f",
)
list_free_params = ["star,ud", "star,f", "ring,scale"]
model = dict_to_model(model_dict, list_free_params)
source
OITOOLS.model_to_visFunction
model_to_vis(model, x, uv; wl=nothing, mjd=nothing, n=0)

Alias for eval_model. Compute complex visibilities from a parametric model, for consistency with the model_to_obs / model_to_chi2 family.

source
OITOOLS.eval_modelFunction
eval_model(model, x, uv; wl=nothing, mjd=nothing, n=0) -> Vector{Complex{T}}

Evaluate the model visibility at the given parameter vector x and baselines uv.

Arguments

model : FlatModel from dicttomodel x : current free-parameter values, length = length(model.listfreeparams) uv : 2×N matrix of (u,v) spatial frequencies in cycles/rad wl : wavelength per UV point (metres), length N — enables $WL in expressions mjd : MJD per UV point, length N — enables $MJD in expressions n : Bessel order for Hankel components (default 0)

Returns

V : complex visibility vector, length N

source
OITOOLS.eval_model_gradFunction
eval_model_grad(model, x, uv; wl=nothing, mjd=nothing, n=0) -> (V, J)

Evaluate model visibility and its Jacobian w.r.t. x.

Returns

V : complex visibility vector (length nB) J : Jacobian, Matrix{Complex{T}} of shape (nB, length(x)); T follows x and uv

Uses ForwardDiff for the resolver and analytic components; for Hankel components uses the cached-K chain rule via hankelvisfwd! + pullback. This is more efficient than naive ForwardDiff through the full Hankel loop when nB is large, since Bessel evaluations are shared across parameters.

source
OITOOLS.display_modelFunction
display_model(model_dict, list_free_params; lb, ub)

Print a human-readable summary of the model setup, grouped by component. Shows each parameter's value (or expression), whether it is free or fixed, and its bounds. Warns about common mistakes (value out of bounds, missing bounds, flux fractions that don't sum to 1).

Call this before fit_model / fit_model_ultranest to verify your setup.

source
OITOOLS.default_boundsFunction
default_bounds(model_dict, list_free_params; data = nothing, max_size = nothing) -> (lb, ub)

Suggest lower and upper bounds for each free parameter, as two Dict{String,Float64} ready to pass to any fitter's lb/ub keyword.

Bounds are chosen from the parameter's suffix — the part after the first comma, so "star,ud" is looked up as "ud". Azimuthal-mode keys ("c,az amp1", "c,az projang1") are recognised by prefix. Anything unrecognised, including bare global names, gets (-Inf, Inf); callers that need finite bounds (nested sampling) should check for that.

Angular sizes and offsets are the only bounds worth tuning, and they are the ones the data can tune. Pass data and the ceiling becomes 2 λ/B_min from the actual uv coverage — the array cannot constrain a source larger than the scale its shortest baseline senses. Without data the ceiling is DEFAULT_MAX_SIZE_MAS; max_size overrides both.

lb, ub = default_bounds(model_dict, free)              # 100 mas ceiling
lb, ub = default_bounds(model_dict, free; data = data) # sized to this uv coverage

Everything else is physical rather than instrumental — flux fractions in [0,1], inclination in [0,90], position angles in [-180,180] — and does not depend on the data.

See also display_model, which validates values against whatever bounds you end up with, and max_angular_scale.

source
OITOOLS.DEFAULT_MAX_SIZE_MASConstant

Default upper bound on an angular size, in mas, when no data is supplied.

Long-baseline interferometry senses scales between roughly λ/2Bmax and λ/Bmin. Measured on three real datasets, that is 0.5–1.2 mas at the fine end and 18–41 mas at the coarse end: a source much larger than λ/Bmin is fully resolved out and its size is not constrained at all. 100 mas therefore leaves generous headroom over anything an array can measure, while staying a real bound. Pass data to `defaultbounds` to size this from the actual uv coverage instead.

source
OITOOLS.max_angular_scaleFunction
max_angular_scale(data) -> Float64

Largest angular scale the uv coverage is sensitive to, in mas: λ/B_min, from the shortest baseline present. A source larger than this is fully resolved and its size is unconstrained, which makes it the natural ceiling for a size bound.

source
OITOOLS.ModelConstraintType
ModelConstraint(lhs, op, rhs; tol = DEFAULT_CONSTRAINT_TOL)

A relation between model parameters, enforced during fitting.

lhs and rhs are parameter names ("disk,diamout"), expressions over parameter names ("2 * star,ud"), or — for rhs — a plain number. op is one of "<", "<=", ">", ">=", "=", given as a string or a symbol. tol is how much violation counts as none; see DEFAULT_CONSTRAINT_TOL.

fit_model hands these to NLopt as real nonlinear constraints, so they hold at the optimum. fit_model_lsqfit and fit_model_ultranest have no such machinery and apply a one-sided quadratic penalty instead, which is soft: a steep enough χ² can overrule it, and tol then sets the stiffness rather than a feasibility tolerance.

ModelConstraint("disk,diamout", ">", "disk,diamin")   # a ring must have an outside
ModelConstraint("star,f", "+", 0.0)                   # ── invalid: `+` is not a relation
ModelConstraint("star,f + disk,f", "=", 1.0)          # fluxes sum to one
ModelConstraint("star,ud", "<", 4.0; tol = 0.01)      # deliberately loose

Globals — model keys with no comma — must be written with a \$, as "\$PA", exactly as in a model expression. A bare name with no comma and no arithmetic is taken to be one and gets its \$ added.

See also fit_model and read_model_file.

source
OITOOLS.parse_constraintsFunction
parse_constraints(specs) -> Vector{ModelConstraint}

Accept constraints in any of the forms a caller is likely to have them in.

A ModelConstraint passes through. A tuple is read positionally as (lhs, op, rhs) or (lhs, op, rhs, tol) — the PMOIRED prior layout, so a converted PMOIRED model needs no reshaping. A Dict or NamedTuple is read by the keys param/lhs, op, value/rhs and tol, which is the shape a TOML file parses to.

source
OITOOLS.check_constraintsFunction
check_constraints(constraints, model_dict; verb = true) -> Vector{Bool}

Report which constraints the model satisfies as it stands, without fitting anything.

Useful before a long run: a starting model that violates a constraint is not an error — the penalty will push it back — but it is worth knowing about, because it may equally mean the constraint was written backwards.

source
OITOOLS.DEFAULT_CONSTRAINT_TOLConstant

Default tolerance for a constraint, in the units of the quantity being constrained.

It means two related things, one per enforcement mechanism. To NLopt it is the feasibility tolerance: how far outside the constraint a point may sit and still count as satisfied. To the penalty used by the least-squares and nested-sampling fitters it is the stiffness: a violation of tol costs one unit of penalty.

1e-3 is a thousandth of a milliarcsecond on an angular size, and a tenth of a percent on a flux fraction — below anything interferometry resolves, in both readings.

source
OITOOLS.read_model_fileFunction
read_model_file(path) -> (; model, free, lb, ub, constraints, priors, name)

Read a TOML model file into the arguments a fitter takes.

The result destructures straight into any of the fitters:

m = read_model_file("binary.toml")
res = fit_model(m.model, m.free, data; m.lb, m.ub, m.constraints, m.priors)

Every section is optional. A file with only [model] reads back as a model with no free parameters, which is what model_to_image and model_to_sed want. Bounds absent from the file are not filled in from default_bounds — an absent bound means ±Inf, and silently substituting a guess would make two fits from the same file differ by which version of the defaults was in the package.

See also write_model_file, ModelConstraint and default_bounds.

source
OITOOLS.write_model_fileFunction
write_model_file(path, model_dict; free, lb, ub, constraints, priors, name)

Write a model and its fit settings to a TOML file readable by read_model_file.

write_model_file("binary.toml", model; free, lb, ub,
                 constraints = [ModelConstraint("disk,diamout", ">", "disk,diamin")])

Keys are sorted, so the same model always produces the same bytes and two files can be diffed. Bounds that are infinite on both sides are omitted rather than written as [-inf, +inf]: they constrain nothing, and every fitter already treats a missing bound that way.

A FitResult is not what gets written — the fitted values are, if the caller passes them in model_dict. Writing the result of a fit back out is therefore write_model_file(path, merge(model, Dict(zip(res.list_free_params, res.x_opt))); free = res.list_free_params, ...).

source
OITOOLS.fit_modelFunction
fit_model(model_dict, list_free_params, data; kwargs...) -> FitResult

Fit a parametric model to interferometric data using NLopt.

Arguments

  • model_dict::Dict{String} — flat parameter dictionary (values are numbers or expression strings)
  • list_free_params::Vector{String} — names of the free parameters to optimize
  • data::OIdata — interferometric data

Keywords

  • lb, ubDict{String,Float64} of lower/upper bounds per parameter (default: ±Inf)
  • weights — observable weights [V2, T3amp, T3phi, visamp, visphi, flux, diffphase]
  • priors — Vector of (expr_str, target, sigma) tuples for Gaussian penalties
  • constraints — relations between parameters that bounds cannot express, as ModelConstraints or (lhs, op, rhs[, tol]) tuples. Handed to NLopt as real nonlinear constraints, so they hold at the optimum; a method that cannot take them is wrapped in :AUGLAG rather than replaced. The reported chi2 is the data χ², unaffected
  • method — NLopt algorithm (default :LD_LBFGS; use :LN_NELDERMEAD for gradient-free)
  • maxeval — maximum function evaluations (default 2000)
  • ftol_rel, xtol_rel — convergence tolerances
  • vonmises — use von Mises statistic for T3phi
  • nB_workspace — Hankel workspace size (default: nuv from data)
  • verb — print per-evaluation chi2 breakdown
source
fit_model(model::FlatModel, x0, data; kwargs...) -> FitResult

Fit a pre-compiled FlatModel starting from parameter vector x0.

This method skips the dict_to_model step, which is useful when fitting the same model multiple times (e.g. bootstrap, grid search).

Bounds lb and ub can be Dict{String,Float64} (keyed by parameter name) or Vector{Float64} (ordered to match model.list_free_params).

source
OITOOLS.fit_model_lsqfitFunction
fit_model_lsqfit(model_dict, list_free_params, data; kwargs...) -> LsqFitResult

Fit a parametric model to interferometric data using Levenberg-Marquardt (LsqFit.jl). Returns parameter covariance and 1σ uncertainties.

Uses the analytic Jacobian from residuals_flat_jac (Wirtinger chain rule through the complex visibility Jacobian), so no finite-difference overhead.

Arguments

  • model_dict::Dict{String} — flat parameter dictionary
  • list_free_params::Vector{String} — names of the free parameters to optimize
  • data::OIdata — interferometric data

Keywords

  • lb, ubDict{String,Float64} of lower/upper bounds per parameter
  • weights — observable weights [V2, T3amp, T3phi, visamp, visphi, flux, diffphase]
  • vonmises — use von Mises statistic for T3phi
  • nB_workspace — Hankel workspace size
  • maxIter — maximum iterations (default 200)
  • constraints — relations between parameters that bounds cannot express, as ModelConstraints or (lhs, op, rhs[, tol]) tuples. They enter as extra rows of the residual vector, which is exactly where Levenberg-Marquardt can act on them — and so also enter the Jacobian, meaning covar and stderror account for them. The reported chi2 excludes them, so it stays comparable with what every other fitter reports
  • verb — print per-evaluation chi2 breakdown
source
fit_model_lsqfit(model::FlatModel, x0, data; kwargs...) -> LsqFitResult

Levenberg-Marquardt fit on a pre-compiled FlatModel starting from x0. Bounds lb and ub can be Dicts or Vectors.

source
OITOOLS.fit_model_nestedFunction
fit_model_nested(model_dict, list_free_params, data; kwargs...) -> NestedResult
fit_model_nested(model::FlatModel, data; kwargs...)             -> NestedResult

Fit a parametric model by Bayesian nested sampling, returning the posterior and the evidence as well as a best-fit point.

Unlike the gradient fitters this needs no starting guess — it samples the whole prior volume — but it does need finite bounds on every free parameter, since those bounds are the prior.

using OITOOLS, Nautilus
r = fit_model_nested(model_dict, ["star,ud"], data;
                     lb = Dict("star,ud" => 0.5), ub = Dict("star,ud" => 10.0))
r.logz, r.logzerr        # the evidence, for comparing models
r.posterior              # equally weighted samples, (n_samples, n_params)

backend selects the sampler and defaults to nested_backend(); see set_nested_backend! to change it for a session.

Keywords, both backends

keyworddefaultmeaning
lb, ubDict{String,Float64} bounds, required for every free parameter: they are the prior
weights[1,1,1,0,0,0,0]observable weights [V2, T3amp, T3phi, visamp, visphi, flux, diffphase]
vonmisesfalseuse the von Mises statistic for T3phi
constraints[]ModelConstraints, or (lhs, op, rhs[, tol]) tuples. They enter the log-likelihood as -penalty/2, so the posterior and logz are both the constrained ones
nB_workspacefrom the dataHankel workspace size
verbtrueprint progress
cornerplottruedraw the corner plot when a plotting backend is loaded

Keywords, :nautilus

keyworddefaultmeaning
n_live500live points in the exploration phase
n_eff2000effective sample size to reach before stopping
f_live0.01fraction of evidence left in the live set at which exploration ends
n_networks4networks in the bounding ensemble; more is more robust and slower
threadednthreads() > 1evaluate the likelihood across threads
seednothingfix for a reproducible run

Keywords, :ultranest

keyworddefaultmeaning
min_num_live_points400minimum live points
cluster_num_live_points100live points per cluster
num_bootstraps30bootstraps for the evidence error
use_stepsamplerfalseuse RegionSliceSampler
nsteps400steps per slice, with use_stepsampler
frac_remain0.001termination fraction
log_interval100logging interval

Evidence from two different samplers is worth more than evidence from one: logz should agree within logzerr, and NestedResult.backend records which produced any given number.

Nautilus is importance nested sampling

:nautilus draws from neural-network-boosted importance shells rather than by rejection inside a bounding ellipsoid, which buys it far more effective samples per likelihood call. Measured on the α Cen A and α Cen B power-law fits, its logzerr is 0.019 against UltraNest's 0.18–0.31 on the same data, while x_opt agrees to four decimals — so the two can be cross-checked against each other and the Julia one gives the tighter evidence.

threaded defaults to Threads.nthreads() > 1; seed makes a run reproducible.

source
OITOOLS.set_nested_backend!Function
set_nested_backend!(b::Symbol) -> Symbol

Choose the nested sampler: :nautilus or :ultranest.

The backend's package must already be loaded, and this says so immediately rather than letting a long run end in a MethodError.

source
OITOOLS.chi2_mapFunction
chi2_map(model_dict, list_free_params, data, p1, p2; kwargs...) -> Chi2Map

Evaluate χ² on a grid over the two free parameters p1 and p2, holding every other free parameter at its starting value.

m = read_model_file("LD_power.toml")
map = chi2_map(m.model, m.free, data, "star,ldpow", "star,alpha"; m.lb, m.ub, weights = [1.0,0,0])
res = FitResult(map)          # the best grid point, as any other fitter would report it
keyworddefaultmeaning
n1, n260grid points along each axis
range1, range2from lb/ub(lo, hi) for each axis, overriding the bounds
lb, ubDict()bounds; used as the default axis extent
weights[1,1,1,0,0,0,0]as everywhere else in model fitting — seven long
vonmisesfalsepassed through to chi2_flat
nB_workspace100passed through to parse_model

An axis with no finite bound and no explicit range is an error rather than a guess: a grid needs an extent, and inventing one would quietly decide the answer.

p1 and p2 must both be free. Mapping a fixed parameter would be a perfectly reasonable thing to want, but x[i] ↔ list_free_params[i] is what makes the result a FitResult at all, so free it first.

source
OITOOLS.delta_chi2_levelsConstant
delta_chi2_levels

Δχ² for the 68.3, 95.4 and 99.7% confidence regions of two jointly estimated parameters: 2.30, 6.17, 11.8.

These are the two-parameter values, not the one-parameter 1, 4, 9. Contouring a two-parameter map at 1σ = 1 draws a region about a third too small, which is the usual way a χ² map is misread.

source
OITOOLS.model_warningsFunction
model_warnings(model_dict, list_free_params; lb, ub) -> Vector{String}

Everything wrong with a model that can be seen without fitting it, one sentence each.

Four checks, and every one of them describes a setup that runs happily and answers the wrong question: a starting value outside its own bounds, bounds that exclude everything, and flux fractions that do not sum to one. The last is the quiet one – a second component added with f = 1 doubles the model's flux, and the only symptom is a χ² in the millions.

Shared with display_model, which prints these to a terminal, so a GUI and a script cannot disagree about what is wrong with the same model.

Flux fractions given as expressions are not checked: their values depend on the resolver, so summing the literals would be summing the wrong things.

source
OITOOLS.model_to_obsFunction
model_to_obs(model, x, data) -> (v2, t3amp, t3phi, visamp, visphi)

Evaluate a FlatModel at parameter vector x and return the model observables matching the data structure.

Returns a NamedTuple with fields v2, t3amp, t3phi (degrees), visamp, visphi (degrees). Empty vectors are returned for observable types not present in data.

source
OITOOLS.model_to_residualsFunction
model_to_residuals(model, x, data)

Compute normalised residuals (model - data) / error for each observable type. Returns a NamedTuple with fields v2, t3amp, t3phi, visamp, visphi. Phase residuals (t3phi, visphi) are wrapped to [-180, 180] before dividing by error.

source
OITOOLS.model_to_chi2Function
model_to_chi2(model, x, data; weights=[1,1,1,0,0,0,0], verb=false, vonmises=false)

Alias for chi2_flat. Compute the weighted chi-squared of a parametric model against data, for consistency with the model_to_obs / model_to_vis family.

source
OITOOLS.model_to_chi2_fgFunction
model_to_chi2_fg(model, x, data; weights=[1,1,1,0,0,0,0], verb=false, vonmises=false)

Alias for chi2_flat_fg. Compute chi-squared and its gradient w.r.t. x, for consistency with the model_to_obs / model_to_vis family.

source
OITOOLS.model_to_imageFunction
model_to_image(model, x; nx=256, pixsize=0.1, oversample=1, normalize=true, wl=nothing)
    -> Matrix{Float64}

Synthesise an image from a FlatModel at parameter vector x.

The image is computed by evaluating the model visibility on the 2-D FFT frequency grid and applying an inverse real FFT (irfft), which exploits the Hermitian symmetry V(-u,-v) = conj(V(u,v)) to halve the number of visibility evaluations.

Arguments

  • model::FlatModel — compiled model from dict_to_model
  • x::AbstractVector — current free-parameter values

Keywords

  • nx — image size in pixels (default 256)
  • pixsize — pixel size in mas (default 0.1)
  • oversample — oversampling factor (default 1)
  • normalize — normalize image to unit sum (default true)
  • wl — wavelength in microns (scalar); enables $WL in spectrum expressions

Returns

imgnx × nx Matrix{Float64}

source
OITOOLS.model_to_sedFunction
model_to_sed(model, x, wl_grid) -> (total, components)

Evaluate the spectral energy distribution of a FlatModel by computing V(u=0, v=0) at each wavelength — the zero-baseline visibility equals the total flux.

Per-component fluxes are obtained by resolving the f / spectrum parameters at each wavelength.

Arguments

  • model::FlatModel — compiled model from dict_to_model
  • x::AbstractVector — current free-parameter values
  • wl_grid — wavelength array in METRES, the unit $WL carries in the resolver and uv_lam carries in the data. A grid in microns evaluates every $WL expression at a wavelength a million times too large and returns fluxes that are wrong without erroring.

Returns

  • total::Vector{Float64} — total flux at each wavelength (= V(0,0))
  • components::Dict{String,Vector{Float64}} — per-component flux
source
OITOOLS.model_to_fluxFunction
model_to_flux(model, x; wl=nothing)

Total flux of the model at zero baseline: real(V(0,0)).

For polychromatic models, pass wl as a scalar or 1-element vector. See also model_to_sed for the full spectral energy distribution.

source
OITOOLS.bootstrap_fitFunction
bootstrap_fit(model_dict, list_free_params, data; kwargs...) -> BootstrapResult

Estimate parameter uncertainties by nonparametric block bootstrap: refit the model to nboot replicates in which the blocks of data (by default one per MJD and telescope configuration) are resampled, and summarise the scatter of the best-fit parameters.

Unlike the analytic covariance of fit_model_lsqfit, this does not assume the quoted error bars are correct or uncorrelated: correlated calibration errors and mis-stated error bars show up as extra scatter between replicates. It does assume the blocks are numerous and exchangeable — with a single snapshot (a handful of blocks) the estimate is unreliable, and perturb_data based Monte Carlo or the analytic errors are the better tool.

Arguments

  • model_dict::Dict{String} — flat parameter dictionary
  • list_free_params::Vector{String} — names of the free parameters
  • data::OIdata — interferometric data

Keywords

  • nboot — number of replicates (default 200; use ≥1000 for stable percentiles)
  • mode:replacement (default), :halfsample or :weights; see block_counts and block_weights. :pmoired reproduces PMOIRED's scheme and is biased low by about √2 — comparison only
  • granularity:config (default), :epoch or :point; see data_blocks
  • fitter:lsqfit (default, Levenberg-Marquardt with analytic Jacobian) or :nlopt
  • lb, ub, weights, vonmises, nB_workspace — passed through to the fitter
  • method, maxeval — NLopt settings (fitter=:nlopt)
  • maxIter — LsqFit iterations (fitter=:lsqfit)
  • start_scatter — perturb each replicate's starting point by this many σ of the full-data fit (default 0; PMOIRED uses the equivalent of 1). Non-zero values mix optimiser scatter into the uncertainty and are meant for comparison studies only.
  • sigma_clipping — reject replicates beyond this many σ from the median (default nothing; PMOIRED's plots default to 4.5)
  • chi2r_max — reject replicates whose reduced χ² exceeds this value
  • seed — RNG seed; replicate i uses Xoshiro(seed + i) so results are reproducible regardless of thread scheduling
  • threaded — run replicates on all available Julia threads (default true)
  • verb — progress reporting (default true)

Example

data  = readoifits("data/AlphaCenA.oifits")[1,1]
model = Dict{String,Any}("star,ldlin" => 8.0, "star,u" => 0.3, "star,f" => 1.0)
boot  = bootstrap_fit(model, ["star,ldlin", "star,u"], data;
                      lb=Dict("star,ldlin"=>5.0, "star,u"=>0.0),
                      ub=Dict("star,ldlin"=>12.0, "star,u"=>1.0),
                      weights=[1.0, 0.0, 0.0], nboot=500)
source
OITOOLS.bootstrap_driverFunction
bootstrap_driver(fitfun, x_opt, list_free_params; kwargs...) -> BootstrapResult

Run nboot bootstrap replicates of an arbitrary fit and summarise the scatter of the resulting parameters.

This is the model-agnostic half of bootstrap_fit: it owns the replicate loop, the seeding, the threading, the failure handling, the clipping and the statistics, and knows nothing about what is being fitted or how the data are resampled. Packages with their own estimators (or their own notion of a resampling unit) can reuse the statistics by calling this directly.

Arguments

  • fitfun(state, rng) -> (x, chi2r) — performs one replicate: it resamples the data and fits them, returning the best-fit parameters and the reduced χ². The driver never resamples; that is deliberate, so that callers are free to resample structures OITOOLS knows nothing about. A third element may be returned, (x, chi2r, extra), which is stored in info (see below).
  • x_opt — best fit to the unresampled data, recorded in the result.
  • list_free_params — parameter names; length sets the expected width of x.

Keywords

  • nboot — number of replicates (default 200)
  • worker_init(w::Int) -> state — build per-worker state, called once per worker and serially, before any task is spawned (compiling or allocating concurrently is exactly what this exists to avoid). state is nothing when worker_init is not given.
  • nworkers — number of replicate-level workers; 0 (default) means min(nthreads(), nboot). Set it to 1 when fitfun threads internally, so the two levels do not oversubscribe.
  • info — optional AbstractVector of length nboot; when given and fitfun returns a third element, info[i] receives it. Written at the replicate's own index, never in completion order.
  • sigma_clipping, chi2r_max — reject replicates beyond this many σ from the median, or above this reduced χ²
  • seed — replicate i uses Xoshiro(seed + i), so results do not depend on thread scheduling
  • threaded — use several workers (default true; ignored if nworkers is set)
  • verb — progress reporting
  • mode, granularity, nblocks — metadata only, copied into the result

Example

# a fit with no data at all: recover the scatter of a known Gaussian
x_true = [1.0, 2.0]
r = bootstrap_driver((state, rng) -> (x_true .+ 0.1 .* randn(rng, 2), 1.0),
                     x_true, ["a", "b"]; nboot = 500, seed = 1, verb = false)
r.sigma   # ≈ [0.1, 0.1]
source
OITOOLS.data_blocksFunction
data_blocks(data::OIdata; granularity=:config, mjd_digits=5) -> DataBlocks

Partition data into resampling blocks.

Granularity

  • :config (default) — one block per (MJD, telescope configuration), where the configuration is the baseline for V²/VIS, the triangle for T3 and the telescope for FLUX. All wavelength channels of a block stay together, and observables sharing a configuration at the same MJD (e.g. |V| and on the same baseline) stay together too. This is PMOIRED's "spectral vector".
  • :epoch — one block per MJD: an entire observation, all baselines and all observables, is kept or dropped as a unit. Coarser, more conservative; needs many epochs to be usable.
  • :point — one block per data point. The classical i.i.d. bootstrap; it destroys the spectral and per-configuration correlation structure and will therefore underestimate uncertainties on real data. Provided for comparison studies.

mjd_digits sets the rounding used to group MJDs into a common epoch (default 5 decimals ≈ 0.86 s, matching PMOIRED).

MJD precision

Blocking needs the MJDs at full precision: near MJD 55000 the representable Float32 values are 3.9e-3 d = 5.6 minutes apart, so exposures closer than that would collapse onto one value and their blocks would merge. readoifits therefore stores v2_mjd, t3_mjd, vis_mjd and flux_mjd as Float64 whatever T is — the block structure is identical at T=Float32 and T=Float64. (uv_mjd follows T, so it is coarser than these — see the $MJD note in the modelling docs — but block structure is built from the per-table MJDs above, not from uv_mjd, so bootstrapping is unaffected.)

source
OITOOLS.resample_blocksFunction
resample_blocks(data, blocks; mode=:replacement, rng=Random.default_rng()) -> OIdata
resample_blocks(data; granularity=:config, mode=:replacement, ...) -> OIdata

Bootstrap replicate of data: the observable values and their error bars are left untouched, and the blocks of blocks are drawn according to mode (see block_counts). A block drawn twice contributes its data twice; a block not drawn is absent.

The uv table is carried over unchanged — duplicated data points simply share the same uv row — so the model evaluation cost per replicate is unchanged.

source
OITOOLS.block_countsFunction
block_counts(nblocks, mode; rng) -> Vector{Int}

Multiplicity drawn for each block under the given resampling mode. Exposed mainly for testing and for the calibration study of the different schemes.

Modes

  • :replacement (default) — draw nblocks blocks with replacement. This is the textbook nonparametric bootstrap; multiplicities are ≈ Poisson(1), i.e. mean 1 and variance 1.
  • :halfsample — keep a random half of the blocks, each once. Balanced repeated replication: correctly calibrated (the finite-population correction cancels the factor 2 from halving the data), and cheaper since each replicate fit uses half the data. In the validation study under demos/bootstrap_validation this was the best-calibrated scheme whenever blocks were numerous (ratio 0.94-1.06); with very few blocks it turns conservative (1.24-1.56), since each replicate then fits very little data.
  • :pmoired — PMOIRED's scheme: build two independent copies of the dataset, drop a random half of the blocks in each, and fit the union. Multiplicities are 0/1/2 with probabilities 1/4, 1/2, 1/4, so their variance is 1/2 rather than 1.
`:pmoired` is biased low by √2 — for comparison only

Because its block multiplicities have variance 1/2 instead of 1, this scheme returns uncertainties that are too small by roughly a factor √2. Measured against simulated truth (demos/bootstrap_validation) it gives 0.62–0.79 of the true parameter scatter in every regime tested, with a 1σ coverage of ~0.50 instead of 0.683. Use it to reproduce or cross-check a PMOIRED result, never to quote an error bar: use :replacement (default) or :halfsample for that.

resample_blocks additionally accepts mode=:weights, the multiplier (Bayesian) bootstrap, which draws continuous weights rather than integer multiplicities — see block_weights.

source
OITOOLS.apply_block_countsFunction
apply_block_counts(data, blocks, counts) -> OIdata

Build the OIdata in which each block of blocks appears counts[i] times.

source
OITOOLS.block_weightsFunction
block_weights(nblocks; rng) -> Vector{Float64}

Random weights for the multiplier ("Bayesian") bootstrap: nblocks draws from Dirichlet(1, …, 1) rescaled to mean 1, i.e. mean 1 and variance 1 − 1/nblocks, the same first two moments as the multiplicity of a draw with replacement.

Unlike block_counts, these are continuous: no block is ever dropped entirely and none is duplicated, so a replicate costs exactly one dataset instead of one-and-a-bit. See apply_block_weights.

source
OITOOLS.apply_block_weightsFunction
apply_block_weights(data, blocks, w) -> OIdata

Multiplier bootstrap replicate: instead of duplicating and dropping blocks, give block i the weight w[i] in the χ² by dividing its error bars by sqrt(w[i]).

For a weighted least-squares fit this is the standard multiplier (or Bayesian) bootstrap: with mean(w) = 1 and var(w) = 1 it has the same asymptotic behaviour as resampling blocks with replacement, at the cost of one dataset per replicate and with no discrete jumps as blocks enter and leave.

The reduced χ² of such a replicate is not meaningful; the parameter scatter is.

source
OITOOLS.perturb_dataFunction
perturb_data(data::OIdata; rng=Random.default_rng()) -> OIdata

Return a copy of data with independent Gaussian noise added to every observable, each draw scaled by that point's error bar. uv coordinates, flags and the number of data points are unchanged.

Useful for building noisy realisations of a dataset — simulation, end-to-end tests, sensitivity checks.

Not an uncertainty estimator

Refitting perturb_data replicates is a parametric Monte Carlo, not a bootstrap: it assumes the quoted error bars are correct and uncorrelated, so it can only ever reproduce the analytic covariance of the fit. In the validation study under demos/bootstrap_validation it was never better than fit_model_lsqfit's covariance and, when calibration systematics were present, it understated the true scatter by a factor 11 to 25. Use bootstrap_fit for uncertainties.

source
OITOOLS.BootstrapResultType
BootstrapResult

Outcome of bootstrap_fit.

Fields

  • samplesnboot × npar matrix of per-replicate best-fit parameters
  • list_free_params — parameter names, matching the columns of samples
  • x_opt — best fit to the full dataset
  • median, sigma, sigma_minus, sigma_plus — median and 16/84 percentile half-widths of the (masked) bootstrap distribution
  • covar, correlation — parameter covariance and correlation matrices
  • chi2r — reduced χ² of each replicate
  • mask — replicates kept after sigma/χ² clipping
  • nblocks, mode, granularity — resampling setup
  • nfailed — replicates whose fit threw an error
source
OITOOLS.NestedResultType
NestedResult

Result of fit_model_nested, whichever sampler produced it.

fieldtypemeaning
x_optVector{Float64}maximum-likelihood parameter values
list_free_paramsVector{String}parameter names, same order as x_opt
chi2, chi2r, ndofFloat64, Float64, Intchi² at x_opt, reduced chi², degrees of freedom
logz, logzerrFloat64Bayesian log-evidence and its uncertainty
posteriorMatrix{Float64}(n_samples, n_params) equally weighted posterior samples
resultAnythe sampler's own result object, passed through unconverted
modelFlatModelthe compiled model that was fitted
backendSymbolwhich sampler produced this — :nautilus or :ultranest

backend is not decoration. Two independent nested samplers agreeing on logz is evidence; one of them quoted without saying which is not, because the estimator, the bounding strategy and the stopping rule all differ between them.

`result` is backend-specific

Under :ultranest it is a PythonCall.Py wrapping UltraNest's result dict — index it with result["key"] and convert explicitly with pyconvert(T, …). Under :nautilus it is the sampler state Nautilus returned. Every other field is a concrete Julia type and means the same thing either way; prefer those where they suffice.

source
OITOOLS.FitResultType
FitResult

What every point-estimate fitter returns: fit_model, fit_model_lsqfit and the grid search.

fieldmeaning
x_optbest-fit values, in list_free_params order
list_free_paramsthe names, so x_opt[i] is always identifiable
chi2, chi2r, ndofraw weighted χ², χ²/ndof, and the number of DATA POINTS
n_evalsχ² evaluations spent
retthe optimiser's return code
modelthe compiled FlatModel the fit ran against

ndof counts data points and is not reduced by the number of free parameters, so it is not a degrees-of-freedom in the model-comparison sense; compute AIC or BIC from chi2 and the parameter count rather than reading them off chi2r.

See also LsqFitResult, UltraNestResult and Chi2Map.

source
OITOOLS.Chi2MapType
Chi2Map

A χ² surface over two free parameters, with everything needed to draw it and to report the best point on it.

fieldmeaning
p1, p2the two parameter names, as they appear in list_free_params
v1, v2the grid values along each axis
chi2(length(v1), length(v2)) matrix of raw weighted χ²
ndofdata points, so chi2 ./ ndof is the reduced map
x_optthe FULL free-parameter vector at the grid minimum
list_free_paramsnames for x_opt, in fit-vector order
modelthe compiled model the map was computed with

Parameters not on the two axes are held at their starting values throughout: a map is a slice through the χ² surface, not a profile likelihood, and reading it as one overstates how well the two plotted parameters are determined.

See also chi2_map and delta_chi2_levels.

source
OITOOLS.DataBlocksType
DataBlocks

Partition of an OIdata into resampling units. Each block holds the row indices it owns in each observable table (OI_VIS, OI_VIS2, OI_T3, OI_FLUX). Built by data_blocks.

source
TypeDescription
FitResultResult from fit_model (fields: x_opt, chi2r, model, ...)
LsqFitResultResult from fit_model_lsqfit (adds stderror, covar, converged)
NestedResultResult from fit_model_nested (adds logz, logzerr, posterior, result, backend)
UltraNestResultAlias for NestedResult
BootstrapResultResult from bootstrap_fit (adds samples, median, sigma_minus, sigma_plus, covar)
Chi2Mapχ² surface over two free parameters from chi2_map; FitResult(map) gives its best grid point
DataBlocksPartition of an OIdata into resampling blocks
ModelConstraintOne relation between model parameters, enforced during fitting

Visibility functions

Analytic visibility functions for standard source geometries. All take baseline spatial frequency arguments and return complex visibilities.

FunctionModel
visibility_ud(b, diam)Uniform disk
visibility_ldlin(b, diam, u)Linear limb-darkened disk
visibility_ldquad(b, diam, u, w)Quadratic limb-darkened disk
visibility_ldquad_alt(b, diam, u, w)Quadratic LD disk (alternative convention)
visibility_ldpow(b, diam, alpha)Power-law limb-darkened disk
visibility_ldsquareroot(b, diam, u, w)Square-root limb-darkened disk
visibility_annulus(b, din, dout)Uniform annulus
visibility_ellipse_uniform(u, v, a, b, pa)Uniform ellipse
visibility_ellipse_quad(u, v, a, b, pa, c1, c2)Quadratic LD ellipse
visibility_thin_ring(b, diam)Infinitely thin ring
visibility_Gaussian_ring(b, diam, fwhm)Gaussian ring
visibility_Gaussian_ring_az(...)Gaussian ring with azimuthal modulation
visibility_Lorentzian_ring(b, diam, fwhm)Lorentzian ring
visibility_GaussianLorentzian_ring_az(...)Gaussian-Lorentzian ring with azimuthal modulation