Model Fitting
Parametric models
| Function | Description |
|---|---|
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 needs — fit_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.
| Function | Description |
|---|---|
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.5m = 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
| Function | Description |
|---|---|
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.FlatModel — Type
Top-level compiled model.
OITOOLS.dict_to_model — Function
dict_to_model(model_dict, list_free_params; nB_workspace=100) -> FlatModelCompile 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)OITOOLS.parse_model — Function
Backward-compatible alias for dict_to_model.
OITOOLS.model_to_vis — Function
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.
OITOOLS.eval_model — Function
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
OITOOLS.eval_model_grad — Function
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.
OITOOLS.display_model — Function
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.
OITOOLS.default_bounds — Function
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 coverageEverything 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.
OITOOLS.DEFAULT_MAX_SIZE_MAS — Constant
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.
OITOOLS.max_angular_scale — Function
max_angular_scale(data) -> Float64Largest 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.
OITOOLS.ModelConstraint — Type
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 looseGlobals — 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.
OITOOLS.parse_constraints — Function
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.
OITOOLS.check_constraints — Function
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.
OITOOLS.DEFAULT_CONSTRAINT_TOL — Constant
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.
OITOOLS.read_model_file — Function
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.
OITOOLS.write_model_file — Function
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, ...).
OITOOLS.fit_model — Function
fit_model(model_dict, list_free_params, data; kwargs...) -> FitResultFit 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 optimizedata::OIdata— interferometric data
Keywords
lb,ub—Dict{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 penaltiesconstraints— relations between parameters that bounds cannot express, asModelConstraints or(lhs, op, rhs[, tol])tuples. Handed to NLopt as real nonlinear constraints, so they hold at the optimum; amethodthat cannot take them is wrapped in:AUGLAGrather than replaced. The reportedchi2is the data χ², unaffectedmethod— NLopt algorithm (default:LD_LBFGS; use:LN_NELDERMEADfor gradient-free)maxeval— maximum function evaluations (default 2000)ftol_rel,xtol_rel— convergence tolerancesvonmises— use von Mises statistic for T3phinB_workspace— Hankel workspace size (default: nuv from data)verb— print per-evaluation chi2 breakdown
fit_model(model::FlatModel, x0, data; kwargs...) -> FitResultFit 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).
OITOOLS.fit_model_lsqfit — Function
fit_model_lsqfit(model_dict, list_free_params, data; kwargs...) -> LsqFitResultFit 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 dictionarylist_free_params::Vector{String}— names of the free parameters to optimizedata::OIdata— interferometric data
Keywords
lb,ub—Dict{String,Float64}of lower/upper bounds per parameterweights— observable weights [V2, T3amp, T3phi, visamp, visphi, flux, diffphase]vonmises— use von Mises statistic for T3phinB_workspace— Hankel workspace sizemaxIter— maximum iterations (default 200)constraints— relations between parameters that bounds cannot express, asModelConstraints 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, meaningcovarandstderroraccount for them. The reportedchi2excludes them, so it stays comparable with what every other fitter reportsverb— print per-evaluation chi2 breakdown
fit_model_lsqfit(model::FlatModel, x0, data; kwargs...) -> LsqFitResultLevenberg-Marquardt fit on a pre-compiled FlatModel starting from x0. Bounds lb and ub can be Dicts or Vectors.
OITOOLS.fit_model_nested — Function
fit_model_nested(model_dict, list_free_params, data; kwargs...) -> NestedResult
fit_model_nested(model::FlatModel, data; kwargs...) -> NestedResultFit 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
| keyword | default | meaning |
|---|---|---|
lb, ub | — | Dict{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] |
vonmises | false | use 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_workspace | from the data | Hankel workspace size |
verb | true | print progress |
cornerplot | true | draw the corner plot when a plotting backend is loaded |
Keywords, :nautilus
| keyword | default | meaning |
|---|---|---|
n_live | 500 | live points in the exploration phase |
n_eff | 2000 | effective sample size to reach before stopping |
f_live | 0.01 | fraction of evidence left in the live set at which exploration ends |
n_networks | 4 | networks in the bounding ensemble; more is more robust and slower |
threaded | nthreads() > 1 | evaluate the likelihood across threads |
seed | nothing | fix for a reproducible run |
Keywords, :ultranest
| keyword | default | meaning |
|---|---|---|
min_num_live_points | 400 | minimum live points |
cluster_num_live_points | 100 | live points per cluster |
num_bootstraps | 30 | bootstraps for the evidence error |
use_stepsampler | false | use RegionSliceSampler |
nsteps | 400 | steps per slice, with use_stepsampler |
frac_remain | 0.001 | termination fraction |
log_interval | 100 | logging 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 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.
OITOOLS.fit_model_ultranest — Function
fit_model_ultranest(args...; kwargs...) -> NestedResultfit_model_nested pinned to the UltraNest backend. Needs using PythonCall and the Python ultranest package (declared in CondaPkg.toml).
OITOOLS.nested_backend — Function
nested_backend() -> SymbolWhich nested sampler fit_model_nested will use: :nautilus, :ultranest, or :none when no extension is loaded.
OITOOLS.set_nested_backend! — Function
set_nested_backend!(b::Symbol) -> SymbolChoose 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.
OITOOLS.chi2_map — Function
chi2_map(model_dict, list_free_params, data, p1, p2; kwargs...) -> Chi2MapEvaluate χ² 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| keyword | default | meaning |
|---|---|---|
n1, n2 | 60 | grid points along each axis |
range1, range2 | from lb/ub | (lo, hi) for each axis, overriding the bounds |
lb, ub | Dict() | bounds; used as the default axis extent |
weights | [1,1,1,0,0,0,0] | as everywhere else in model fitting — seven long |
vonmises | false | passed through to chi2_flat |
nB_workspace | 100 | passed 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.
OITOOLS.delta_chi2_levels — Constant
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.
OITOOLS.model_warnings — Function
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.
OITOOLS.model_to_obs — Function
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.
OITOOLS.model_to_residuals — Function
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.
OITOOLS.model_to_chi2 — Function
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.
OITOOLS.model_to_chi2_fg — Function
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.
OITOOLS.model_to_image — Function
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 fromdict_to_modelx::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$WLin spectrum expressions
Returns
img — nx × nx Matrix{Float64}
OITOOLS.model_to_sed — Function
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 fromdict_to_modelx::AbstractVector— current free-parameter valueswl_grid— wavelength array in METRES, the unit$WLcarries in the resolver anduv_lamcarries in the data. A grid in microns evaluates every$WLexpression 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
OITOOLS.model_to_flux — Function
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.
OITOOLS.bootstrap_fit — Function
bootstrap_fit(model_dict, list_free_params, data; kwargs...) -> BootstrapResultEstimate 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 dictionarylist_free_params::Vector{String}— names of the free parametersdata::OIdata— interferometric data
Keywords
nboot— number of replicates (default 200; use ≥1000 for stable percentiles)mode—:replacement(default),:halfsampleor:weights; seeblock_countsandblock_weights.:pmoiredreproduces PMOIRED's scheme and is biased low by about √2 — comparison onlygranularity—:config(default),:epochor:point; seedata_blocksfitter—:lsqfit(default, Levenberg-Marquardt with analytic Jacobian) or:nloptlb,ub,weights,vonmises,nB_workspace— passed through to the fittermethod,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 (defaultnothing; PMOIRED's plots default to 4.5)chi2r_max— reject replicates whose reduced χ² exceeds this valueseed— RNG seed; replicateiusesXoshiro(seed + i)so results are reproducible regardless of thread schedulingthreaded— run replicates on all available Julia threads (defaulttrue)verb— progress reporting (defaulttrue)
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)OITOOLS.bootstrap_driver — Function
bootstrap_driver(fitfun, x_opt, list_free_params; kwargs...) -> BootstrapResultRun 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 ininfo(see below).x_opt— best fit to the unresampled data, recorded in the result.list_free_params— parameter names;lengthsets the expected width ofx.
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).stateisnothingwhenworker_initis not given.nworkers— number of replicate-level workers;0(default) meansmin(nthreads(), nboot). Set it to 1 whenfitfunthreads internally, so the two levels do not oversubscribe.info— optionalAbstractVectorof lengthnboot; when given andfitfunreturns 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— replicateiusesXoshiro(seed + i), so results do not depend on thread schedulingthreaded— use several workers (defaulttrue; ignored ifnworkersis set)verb— progress reportingmode,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]OITOOLS.data_blocks — Function
data_blocks(data::OIdata; granularity=:config, mjd_digits=5) -> DataBlocksPartition 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|andV²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).
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.)
OITOOLS.resample_blocks — Function
resample_blocks(data, blocks; mode=:replacement, rng=Random.default_rng()) -> OIdata
resample_blocks(data; granularity=:config, mode=:replacement, ...) -> OIdataBootstrap 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.
OITOOLS.block_counts — Function
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) — drawnblocksblocks 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 underdemos/bootstrap_validationthis 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.
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.
OITOOLS.apply_block_counts — Function
apply_block_counts(data, blocks, counts) -> OIdataBuild the OIdata in which each block of blocks appears counts[i] times.
OITOOLS.block_weights — Function
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.
OITOOLS.apply_block_weights — Function
apply_block_weights(data, blocks, w) -> OIdataMultiplier 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.
OITOOLS.perturb_data — Function
perturb_data(data::OIdata; rng=Random.default_rng()) -> OIdataReturn 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.
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.
OITOOLS.resample_data — Function
resample_data(data::OIdata)Deprecated alias for perturb_data.
Despite the name, this function never resampled anything: it adds Gaussian noise drawn from the error bars. For a genuine bootstrap (resampling which observations are used) see bootstrap_fit and resample_blocks.
OITOOLS.BootstrapResult — Type
BootstrapResultOutcome of bootstrap_fit.
Fields
samples—nboot × nparmatrix of per-replicate best-fit parameterslist_free_params— parameter names, matching the columns ofsamplesx_opt— best fit to the full datasetmedian,sigma,sigma_minus,sigma_plus— median and 16/84 percentile half-widths of the (masked) bootstrap distributioncovar,correlation— parameter covariance and correlation matriceschi2r— reduced χ² of each replicatemask— replicates kept after sigma/χ² clippingnblocks,mode,granularity— resampling setupnfailed— replicates whose fit threw an error
OITOOLS.NestedResult — Type
NestedResultResult of fit_model_nested, whichever sampler produced it.
| field | type | meaning |
|---|---|---|
x_opt | Vector{Float64} | maximum-likelihood parameter values |
list_free_params | Vector{String} | parameter names, same order as x_opt |
chi2, chi2r, ndof | Float64, Float64, Int | chi² at x_opt, reduced chi², degrees of freedom |
logz, logzerr | Float64 | Bayesian log-evidence and its uncertainty |
posterior | Matrix{Float64} | (n_samples, n_params) equally weighted posterior samples |
result | Any | the sampler's own result object, passed through unconverted |
model | FlatModel | the compiled model that was fitted |
backend | Symbol | which 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.
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.
OITOOLS.UltraNestResult — Type
UltraNestResultAlias for NestedResult, so code written against the UltraNest-only API keeps working.
OITOOLS.FitResult — Type
FitResultWhat every point-estimate fitter returns: fit_model, fit_model_lsqfit and the grid search.
| field | meaning |
|---|---|
x_opt | best-fit values, in list_free_params order |
list_free_params | the names, so x_opt[i] is always identifiable |
chi2, chi2r, ndof | raw weighted χ², χ²/ndof, and the number of DATA POINTS |
n_evals | χ² evaluations spent |
ret | the optimiser's return code |
model | the 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.
OITOOLS.Chi2Map — Type
Chi2MapA χ² surface over two free parameters, with everything needed to draw it and to report the best point on it.
| field | meaning |
|---|---|
p1, p2 | the two parameter names, as they appear in list_free_params |
v1, v2 | the grid values along each axis |
chi2 | (length(v1), length(v2)) matrix of raw weighted χ² |
ndof | data points, so chi2 ./ ndof is the reduced map |
x_opt | the FULL free-parameter vector at the grid minimum |
list_free_params | names for x_opt, in fit-vector order |
model | the 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.
OITOOLS.DataBlocks — Type
DataBlocksPartition 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.
| Type | Description |
|---|---|
FitResult | Result from fit_model (fields: x_opt, chi2r, model, ...) |
LsqFitResult | Result from fit_model_lsqfit (adds stderror, covar, converged) |
NestedResult | Result from fit_model_nested (adds logz, logzerr, posterior, result, backend) |
UltraNestResult | Alias for NestedResult |
BootstrapResult | Result 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 |
DataBlocks | Partition of an OIdata into resampling blocks |
ModelConstraint | One 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.
| Function | Model |
|---|---|
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 |