Skip to content

jbrea/NestedStructInitialiser.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 

Repository files navigation

NestedStructInitialiser.jl

Lifecycle Build Status codecov.io

NestedStructInitialiser exports two functions: parameters helps to explore nested structures and initialiser creates efficient constructors for nested structures.

julia> using NestedStructInitialiser, StaticArrays, Unitful
julia> struct A{N}
        b
        c
        t::NTuple{N, Float64}
    end
julia> struct B{T}
        m::T
        n::Float64
        o::Int
    end
julia> struct C{N}
        x::SVector{N, Float64}
    end

julia> p = parameters(A)
A Parameters
Fixed
 none
Free
 b: Any (indeterminable dimensionality)
 c: Any (indeterminable dimensionality)
 t: Tuple{Vararg{Float64,N}} where N (indeterminable dimensionality)

julia> p = parameters(A{2}, b = B{typeof(1.0u"s")}, c = C{2}, o = 42, n = () -> rand())
A{2} Parameters
Fixed
 b = B{Quantity{Float64,𝐓,Unitful.FreeUnits{(s,),𝐓,nothing}}}
 n = #9
 o = 42
 c = C{2}
Free
 m: Quantity{Float64,𝐓,Unitful.FreeUnits{(s,),𝐓,nothing}}
 x: SArray{Tuple{2},Float64,1,2}
 t: Tuple{Float64,Float64}

julia> constructor = initialiser(p)

julia> constructor([1, 2, 3, 4, 5])
A{2}(B{Quantity{Float64,𝐓,Unitful.FreeUnits{(s,),𝐓,nothing}}}(1.0 s, 0.5556739449879704, 42), C{2}([2.0, 3.0]), (4.0, 5.0))

Motivation

When we simulate complex phenomena, we have usually multiple options to approximate certain aspects. For example, in simulations of trajectories of tennis balls we can approximate the ball as a point mass, a rigid ball or an elastic ball, and we have multiple choices for how the ball interacts with the air and the court. Each approximation depends on some parameters, like the mass of the ball, its radius or some drag coefficients. Some of these parameters are well known or easy to measure; others we may want to determine indirectly by comparing simulations with measurements. For example, the mass and radius of a ball are easy to measure, but we may want to find drag coefficients by comparing simulated trajectories with measured trajectories. To achieve this we can run an optimisation method that runs the simulator with different values for the free parameters until simulations and measurements match.

Nested structures are convenient to express choices (see example below). This package provides the function parameters to inspect the parameters of nested structures and the function initialiser to get an efficient constructor for nested structures that can be used in optimisation. More specifically, we can tell the initialiser all the known parameters and it returns a function that takes as input a vector with length equal to the number of free parameters and outputs the nested structure. The number of free parameters is currently determined in the following way: for fields of subtype Number: 1, for fields of subtype NTuple{N}: N and for fields of subtype SArray{S}: prod(S).

Example

Here are some structures that we may want to define in a tennis simulator.

using StaticArrays, Unitful

struct Simulator{A,B,C}
    air::A
    ball::B
    court::C
end

# Air
struct Vacuum end
struct SimpleDrag
    ρ::Float64
end
struct WithMagnusEffect
    ρ::Float64
end

# Balls
struct PointMass{U}
    mass::U
end
struct RigidBall{S}
    surface::S
    radius::Float64
    mass::Float64
end
struct ElasticBall{S,E}
    surface::S
    elasticity::E
    radius::Float64
    mass::Float64
end

# Surfaces
struct SimpleSurface end
struct FeltSurface
    drag_coefficient::Float64
end

# Elasticity
struct LinearIsotropic
    k::Float64
end
struct StiffnessTensor
    C::SMatrix{6, 6, Float64, 36}
end

# Court
struct ClayCourt{N}
    some_parameters::NTuple{N, Float64}
end
struct GrassCourt
    grass_length::Float64
end

Let us inspect the parameters of this simulator with the function parameters.

julia> using NestedStructInitialiser

julia> parameters(Simulator)
Simulator Parameters
Fixed
 none
Free
 air: Any (indeterminable dimensionality)
 ball: Any (indeterminable dimensionality)
 court: Any (indeterminable dimensionality)

Let us now fix some of the free parameters (in a not so meaningful, but hopefully instructive way). Note that we define the units in which the mass of the ball is measured and the dimensionality of some_parameters in the respective type parameters.

julia> p = parameters(Simulator, air = SimpleDrag, ball = PointMass{typeof(1.0u"kg")}, ρ = () -> rand(), court = ClayCourt{3})
Simulator Parameters
Fixed
 air = SimpleDrag
 ρ = #9
 ball = PointMass{Quantity{Float64,𝐌,Unitful.FreeUnits{(kg,),𝐌,nothing}}}
 court = ClayCourt{3}
Free
 mass: Quantity{Float64,𝐌,Unitful.FreeUnits{(kg,),𝐌,nothing}}
 some_parameters: Tuple{Float64,Float64,Float64}

The "fixed" value for ρ is a function without arguments, which is evaluated every time the constructor is called. This can be useful, when you want to initialise a state variable with an array, e.g. () -> zeros(10).

Let us now use the initialiser function to get a constructor that takes a 4-dimensional vector as input and returns the initialised nested structure.

julia> c = initialiser(p)
Number of Free Parameters: 4
#35 (generic function with 1 method)

julia> c([1, 2, 3, 4])
Simulator{SimpleDrag,PointMass{Quantity{Float64,𝐌,Unitful.FreeUnits{(kg,),𝐌,nothing}}},ClayCourt{3}}(SimpleDrag(0.5521639102892784), PointMass{Quantity{Float64,𝐌,Unitful.FreeUnits{(kg,),𝐌,nothing}}}(1.0 kg), ClayCourt{3}((2.0, 3.0, 4.0)))

We could have also called the initialiser directly with the known parameters to get the same function as above.

julia> c = initialiser(Simulator, air = SimpleDrag, ball = PointMass{typeof(1.0u"kg")}, ρ = 2., court = ClayCourt{3})
Number of Free Parameters: 4
#37 (generic function with 1 method)

This initialiser can now be used in optimisation. For the sake of demonstration, let us define a dummy method to simulate and compute the loss between simulation and measured data.

simulate(s::Simulator{SimpleDrag,<:PointMass,<:ClayCourt}) = sum(s.court.some_parameters) - s.air.ρ
loss(s, data) = (simulate(s) - data)^2
const data = 1.2
loss(x) = loss(c(x), data)
loss(rand(4))

We can evaluate this loss function for any value of the four free parameters. Therefore we can use it in an optimisation method. And if gradients are needed, one can use for example Zygote.

using Zygote
gradient(loss, rand(4))

Alternative, but related approaches

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages