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 now registrered in General. :package:

25 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:

I’ve been playing the whole week with AccessibleModels.jl, and I’m completely convinced: thanks a lot for this package! The ability to pop an interactive window at almost no cost is really nice for adjusting priors.

2 Likes

Happy that it worked out for you!
I have a bunch of further improvements in mind, but not sure about the timeline – they are nice, but not really pressing. For UI specifically, sometimes I miss other widgets, like a checkbox for booleans (or even a text input for strings!). Let me know if something else would be nice to have :slight_smile:

I’m kinda ok with the way it is now, of course I add stuff to the UI when I need it (like a button to start the fit, some ways to select the fit range…) which are note provided automatically, but Makie is simple enough to use that it’s not a big deal at all.

A few months ago I added a small feature to AccessibleModels.jl that ended up being really useful for interactive SliderGrids — especially once you have lots of parameters. It makes these UIs much more usable in practice.

Introducing state for persistence

SliderGrid now accepts an optional state argument, which stores the current values of all controls:

state = Dict()
SliderGrid(fig[1,1], amodel; state)

The key idea is persistence + synchronization:

  • when the UI is created, it initializes sliders from state (if values are present)
  • when you move sliders, it updates state accordingly

(state can be any AbstractDict/AbstractDictionary store)

Why it’s great in notebooks

This is especially nice in notebooks:

  • define state in one cell
  • create the figure + UI controls in another
  • iterate on visuals/layout/setup and re-run the UI cell

Your slider values survive re-execution, instead of being reset each time.

Also useful in scripts: disk-backed state

And it works well in scripts too: you can use a disk-backed dictionary as state. I use Dictionary from SQLCollections.jl(SQLite-backed), which makes a convenient persistent store and works directly as AccessibleModels.jl state.

So when you rerun the script (or tweak + rerun), slider positions don’t reset.


Now I don’t worry about UIs with many parameters to control (when they are needed) :slight_smile:

2 Likes

That does sound quite useful. Will you release a new version with SliderGrid soon?

I think the answer to that is “4 days ago” :slight_smile: optimize SliderGrid for performance by blocking layout updates during… · JuliaAPlavin/AccessibleModels.jl@0000000 · GitHub

Not sure if I understand the question… SliderGrid has been available in AccessibleModels.jl from the beginning. And the state argument was added about a month ago, definitely available in the released version!

Do you have any issues accessing it?

Sorry for the confusion. I saw that the latest release was in October and incorrectly assumed SliderGrid was added after. I’ll give it a try!

Hmm, where do you see this? The date is correct on JuliaHub, Feb 2026:

Ok. I see the problem. The posted releases on github are out of sync :thinking:.

Yes, the information relevant for Julia package manager and the General registry is shown on the JuliaHub website. Github just shows whatever tags happen to exist in the repo, and calls them “releases” :slight_smile:

1 Like