AccessibleModels.jl: Automatic UI and Model Fitting for Arbitrary Objects

Hi all! :waving_hand:

I’m excited to share AccessibleModels.jl – the easiest way to fit/optimize models with parameters being arbitrary Julia objects and create quick UIs for those objects.

I highlighted AccessibleModels.jl in my Accessors talk at this year JuliaCon – see the relevant part here:

Motivation and Design :bullseye:

When fitting models, we often have parameters naturally represented as complex Julia objects (structs, nested data, etc), but optimization/sampling packages expect flat parameter vectors. This leads to lots of boilerplate code for parameter extraction and reconstruction.

The same applies to interactive UIs: it’s often natural to represent multiple relevant parameters as a single object, but UI libraries tend to work with individual independent controls or vectors.

AccessibleModels.jl uses Accessors.jl to automatically handle parameter management, letting you optimize/sample any Julia object directly. The exact same model and parameter definitions can be used for both model fitting and quick UIs!

Key features:

  • Universal: Works with basically any Julia struct, flat or nested :bullseye:
  • Wide ecosystem support: Works with Optimization.jl, Pigeons.jl (MCMC), and more :globe_with_meridians:
  • Zero boilerplate: Just define your model - no parameter extraction/reconstruction code :sparkles:
  • Bonus :wrapped_gift:: Can create instant interactive UIs for any AccessibleModels model using Makie!

Usage :light_bulb:

Define your model object as an arbitrary struct (no dependency on AccessibleModels required at this stage):

julia> struct ExpFunction{A,B}
           scale::A
           shift::B
       end

julia> struct SumFunction{T}
           comps::T
       end

julia> (m::ExpFunction)(x) = m.scale * exp(-(x - m.shift)^2)

julia> (m::SumFunction)(x) = sum(c -> c(x), m.comps)

Create an interactive Makie UI for adjusting model parameters:

julia> using AccessibleModels, IntervalSets
julia> using GLMakie

julia> mod0 = SumFunction((
           ExpFunction(1., 1.),
           ExpFunction(2., 2.),
       ))

julia> amodel = AccessibleModel(mod0, (
           (@o _.comps[∗].shift) => 0..10,
           (@o _.comps[∗].scale) => 0..4,
       ))

julia> obj, = SliderGrid(fig[1,1], amodel)

julia> lines(fig[1,2], 0..10, @lift x -> $obj(x))

Use the same AccessibleModel for optimization by adding a loss function:

# Generate example data using a "true" model
julia> data = [(x=x, y=true_model(x) + 0.2 * randn()) for x in 0:0.5:10]
21-element Vector{@NamedTuple{x::Float64, y::Float64}}:
 (x = 0.0, y = 0.158)
 (x = 0.5, y = -0.172)
 (x = 1.0, y = -0.138)
 <...>

julia> loglike(m::SumFunction, data) = sum(r -> logpdf(Normal(m(r.x), 0.3), r.y), data)

# The only change: add the log-likelihood function
julia> amodel = AccessibleModel(Base.Fix2(loglike, data), mod0, (
           (@o _.comps[∗].shift) => 0..10,
           (@o _.comps[∗].scale) => 0..4,
       ))

julia> using Optimization, OptimizationMetaheuristics

julia> op = OptimizationProblem(amodel)

julia> sol = solve(op, ECA(), amodel)

# Get the fitted model:
julia> getobj(sol)
SumFunction((
    ExpFunction(1.983, 3.098),
    ExpFunction(1.574, 7.013)))

The same model definition works for MCMC sampling. This is especially convenient with MonteCarloMeasurements to hold the results: :bar_chart:

julia> using Pigeons

julia> pt = pigeons(target=amodel, record=[traces; round_trip; record_default()])
julia> using MonteCarloMeasurements

julia> mcmc_fitted = samples(Particles, pt)
SumFunction((ExpFunction(1.5 ± 0.5, 5.35 ± 2.0), ExpFunction(1.6 ± 0.6, 4.66 ± 2.0)))

julia> lines(0..10, x -> mcmc_fitted(x))
julia> band!(0..10, x -> mcmc_fitted(x))

More examples and explanations in the docs.

More :books:

This package is a thin layer of plumbing that builds heavily on the Accessors.jl and AccessorsExtra.jl functionality. See the docs and code for more details.

Related works:

  • AccessibleOptimization.jl: same concept, but Optimization.jl-only; effectively deprecated, all functionality is available in AccessibleModels.jl with a more unified design.
  • PlutoTables.jl: same concept, but for Pluto notebook UIs. Currently, AccessibleModels.jl supports Makie UIs, Pluto backend can be added in the future.

Would love to hear your thoughts and feedback! :thought_balloon:
AccessibleModels.jl is pending registration in General. :package:

16 Likes

This is looking great! I look forward to try it later this week.

Accessors really seems to have no limits! :slight_smile:

Question, what are the semantics of @lift, exactly?

True! Somebody asked me after my talk, like “where do you typically use Accessors?” – and my answer was “basically everywhere” :grinning_face:
(btw added the link to my JuliaCon’25 Accessors.jl talk where I highlight AccessibleModels.jl as well)

Oh, that’s from Makie.jl / Observables.jl, completely independent from Accessors!
Basically, we return an Observable from SliderGrid(...), and this Observable can be used with all the Makie functionality to make dynamic plots.

Oh, right, @lift is part of Makie. Would actually be nice to have it upstreamed to Observables. Or even have a monadic-lift macro like that in some central place?

I wonder why Observables don’t support broadcasting? Then @lift would just be @. (without $-escapes), basically. :slight_smile:

Observables supports map, but I kinda like that Makie uses and recommends lift/@lift instead… These operations are just so different in practice! When I see lift in the code, it’s clear that dynamic updates are happening there – not just map/broadcast over a collection.

1 Like

Still - if any Makie/Observables dev is reading this, @lift could be upstreamed to Observables, right?

1 Like

I guess, although I’m personally not a big fan of the macro … One reason is, that it’s always a good idea to think twice before creating an observable, so I don’t mind more verbosity… On the other hand, I don’t find it easy to see what’s going on with most @lift expressions :wink: