API Reference
Everything documented here is part of the API, (except for things that are explicitly declared experimental), including the names that are not exported but do have docstrings. In Julia versions that support the public
keyword, those unexported but public names are marked explicitly.
Design Problems
Specifying and Composing Design Problems
Kirstine.AbstractDesignProblem
— TypeKirstine.DesignProblem
— TypeDesignProblem <: Kirstine.AbstractDesignProblem
A DesignProblem
has 7 components:
- a
DesignCriterion
, - a
DesignRegion
, - a
Model
, - a
CovariateParameterization
, - some
PriorKnowledge
, - a
Transformation
, - and a
NormalApproximation
.
See also solve
, and the mathematical background.
Kirstine.DesignProblem
— MethodDesignProblem(<keyword arguments>)
Construct a design problem with some sensible defaults.
Arguments
criterion::Kirstine.DesignCriterion
region::Kirstine.DesignRegion
model::Kirstine.Model
covariate_parameterization::Kirstine.CovariateParameterization
prior_knowledge::Kirstine.PriorKnowledge
transformation::Kirstine.Transformation = Identity()
normal_approximation::Kirstine.NormalApproximation = FisherMatrix()
Kirstine.DesignProblem
— MethodDesignProblem(prototype::DesignProblem; <keyword arguments>)
Construct a design problem with defaults from prototype
.
All explicitly given keyword arguments will be part of the new design problem. For every missing keyword argument, the corresponding field from prototype
is copied instead. Note that the result does not share memory with prototype
.
This function is useful for constructing variations of a given problem without having to re-type a lot of code or having to write helper functions.
Examples
Consider some design problem
dp = DesignProblem(; criterion = DCriterion(), other_keyword_arguments...)
This constructs a similar problem where only the criterion differs:
DesignProblem(dp; criterion = ACriterion())
Kirstine.criterion
— Functioncriterion(dp::DesignProblem)
Get the DesignCriterion
of dp
.
Kirstine.region
— Methodregion(dp::DesignProblem)
Get the DesignRegion
of dp
.
Kirstine.model
— Functionmodel(dp::DesignProblem)
Get the Model
of dp
.
Kirstine.covariate_parameterization
— Functioncovariate_parameterization(dp::DesignProblem)
Get the CovariateParameterization
of dp
.
Kirstine.prior_knowledge
— Functionprior_knowledge(dp::DesignProblem)
Get the PriorKnowledge
of dp
.
Kirstine.transformation
— Functiontransformation(dp::DesignProblem)
Get the Transformation
of dp
.
Kirstine.normal_approximation
— Functionnormal_approximation(dp::DesignProblem)
Get the NormalApproximation
of dp
.
Several AbstractDesignProblems
can be combined to form a composite design problem, where a weighted sum of the component objectives is maximized. Composite design problems can be nested within each other.
Kirstine.CompositeDesignProblem
— TypeCompositeDesignProblem <: Kirstine.AbstractDesignProblem
A weighted construct of other abstract design problems.
See the mathematical background.
Kirstine.CompositeDesignProblem
— MethodCompositeDesignProblem(problems, weights)
Construct a composite design problem from a vector of AbstractDesignProblem
s and corresponding non-negative weights.
All problems
must have the same DesignRegion
.
CompositeDesignProblem
s can be nested.
See also problems
, weights(::CompositeDesignProblem)
, region(::CompositeDesignProblem)
.
Kirstine.problems
— Methodproblems(cdp::CompositeDesignProblem)
Get the vector of component design problems.
Kirstine.weights
— Methodweights(cdp::CompositeDesignProblem)
Get the weights associated with the component design problems.
Kirstine.region
— Methodregion(cdp::CompositeDesignProblems)
Get the common DesignRegion
of the component design problems.
Solving Design Problems
Different high-level strategies are available for trying to solve any AbstractDesignProblem
.
Kirstine.solve
— Functionsolve(
adp::Kirstine.AbstractDesignProblem,
strategy::Kirstine.ProblemSolvingStrategy;
trace_state = false,
sargs...,
)
Return a tuple (d, r)
.
d
: The bestDesignMeasure
found. As post-processing,simplify
andsort_points
are called. Their respective keyword arguments can be mixed insargs
, sincesolve
will take care to pass them along correctly.r
: A subtype ofProblemSolvingResult
that is specific to the strategy used. Iftrace_state=true
, this object contains additional debugging information. The unsimplified version ofd
can be accessed assolution(r)
.
See also DirectMaximization
, Exchange
.
Kirstine.ProblemSolvingStrategy
— TypeKirstine.ProblemSolvingStrategy
Supertype for algorithms for solving an AbstractDesignProblem
.
Kirstine.ProblemSolvingResult
— TypeKirstine.ProblemSolvingResult
Supertype for results of a ProblemSolvingStrategy
.
Kirstine.DirectMaximization
— TypeDirectMaximization <: Kirstine.ProblemSolvingStrategy
Find an optimal design by directly maximizing a criterion's objective function.
Kirstine.DirectMaximization
— MethodDirectMaximization(;
optimizer::Kirstine.Optimizer,
prototype::DesignMeasure,
fixedweights = [],
fixedpoints = [],
)
Initialize the optimizer
with a single prototype
and attempt to directly maximize the objective function.
For every index in fixedweights
, the corresponding weight of prototype
is held constant during optimization. The indices in fixedpoints
do the same for the design points of the prototype
. These additional constraints can be used to speed up computation in cases where some weights or design points are know analytically.
For more details on how the prototype
is used, see the specific Optimizer
s.
The return value of solve
for this strategy is a DirectMaximizationResult
.
Kirstine.DirectMaximizationResult
— TypeKirstine.DirectMaximizationResult <: Kirstine.ProblemSolvingResult
Wraps the OptimizationResult
from direct maximization.
See also solution(::DirectMaximizationResult)
, optimization_result
.
Kirstine.solution
— Methodsolution(dmr::Kirstine.DirectMaximizationResult)
Return the best candidate found.
Kirstine.total_runtime
— Methodtotal_runtime(dmr::Kirstine.DirectMaximizationResult)
Get the total time spent during optimization.
Kirstine.optimization_result
— Functionoptimization_result(dmr::Kirstine.DirectMaximizationResult)
Get the full OptimizationResult
.
Kirstine.Exchange
— TypeExchange <: Kirstine.ProblemSolvingStrategy
Find an optimal design by ascending the Gateaux derivative of the objective function.
This is a variant of the basic idea in [YBT13].
Kirstine.Exchange
— MethodExchange(;
optimizer_direction::ZerothOrderOptimizer,
optimizer_weight::Kirstine.Optimizer,
steps::Integer,
candidate::DesignMeasure,
simplify_args = Dict{Symbol,Any}(),
)
Improve the initial candidate
design by repeating the following loop steps
times:
simplify
the currentcandidate
, passing alongsimplify_args
.- Find the direction (one-point design)
d
in which thegateauxderivative
at the currentcandidate
is highest. This usesoptimizer_direction
, initialized with one-point prototypes at thepoints
of the currentcandidate
. See the low-levelOptimizer
s for algorithm-specific details. - Append the single point of
d
to the currentcandidate
. - Recompute optimal weights for the current candidate. This is implemented as a call to
solve
with theDirectMaximization
strategy where all of the points of the currentcandidate
are kept fixed.
Note that this strategy can produce intermediate designs with a lower value of the objective function than the candidate
you started with.
The return value of solve
for this strategy is an ExchangeResult
.
Kirstine.ExchangeResult
— TypeKirstine.ExchangeResult <: Kirstine.ProblemSolvingResult
Wraps the OptimizationResult
s from the individual steps.
See also solution(::ExchangeResult)
, optimization_results_direction
, optimization_results_weight
.
Kirstine.solution
— Methodsolution(er::Kirstine.ExchangeResult)
Return the best candidate found.
Kirstine.total_runtime
— Methodtotal_runtime(er::Kirstine.ExchangeResult)
Get the total time spent during optimization.
Kirstine.total_runtime_direction
— Methodtotal_runtime_direction(er::Kirstine.ExchangeResult)
Get the total time spent during the direction steps.
Kirstine.total_runtime_weight
— Methodtotal_runtime_weight(er::Kirstine.ExchangeResult)
Get the total time spent during the re-weighting steps.
Kirstine.optimization_results_direction
— Functionoptimization_results_direction(er::Kirstine.ExchangeResult)
Get the vector of OptimizationResult
s from the direction steps.
Kirstine.optimization_results_weight
— Functionoptimization_results_weight(er::Kirstine.ExchangeResult)
Get the full OptimizationResult
s from the re-weighting steps.
Particle-based optimizers are operating on a lower level, and are used as part of a Kirstine.ProblemSolvingStrategy
.
Kirstine.Optimizer
— TypeKirstine.Optimizer
Supertype for particle-based optimization algorithms.
See also Pso
, GridSearch
, FrankWolfe
.
Kirstine.ZerothOrderOptimizer
— TypeKirstine.ZerothOrderOptimizer <: Kirstine.Optimizer
Supertype for optimization algorithms that only use the objective function.
Kirstine.FirstOrderOptimizer
— TypeKirstine.FirstOrderOptimizer <: Kirstine.Optimizer
Supertype for optimization algorithms that use the objective function and its gradient.
Kirstine.OptimizationResult
— TypeKirstine.OptimizationResult
Wrapper for results of particle-based optimization.
Kirstine.OptimizationResult
fields
Field | Description |
---|---|
maximizer | final maximizer |
maximum | final objective value |
trace_x | vector of current maximizer in each iteration |
trace_fx | vector of current objective value in each iteration |
trace_gx | vector of current gradient in each iteration |
trace_state | vector of internal optimizer state in each iteration |
n_eval_f | total number of objective evaluations |
n_eval_g | total number of gradient evaluations |
seconds_elapsed | total runtime |
Note that for a ZerothOrderOptimizer
trace_gx
will be empty and n_eval_g == 0
.
See also solve
.
Kirstine.Pso
— TypePso <: ZerothOrderOptimizer
A constricted particle swarm optimizer.
There is a large number of published PSO variants, this one was taken from two [CK02] publications [ES00].
Initialization
The swarm is initialized from a vector of prototypical AbstractPoint
s, which are supplied by the caller via a non-exported interface. Construction from the vector of prototypes
proceeds as follows:
The first
length(prototypes)
particles are a deep copy of theprototypes
vector.Each of the remaining particles is a deep copy of
prototypes[1]
which is subsequently randomized. Randomization is subject to constraints, if any are given.
Kirstine.Pso
— MethodPso(; iterations, swarmsize, c1 = 2.05, c2 = 2.05)
Set up parameters for a particle swarm optimizer.
c1
controls movement towards a particle's personal best, while c2
controls movement towards the global best. In this implementation, a particle's neighborhood is the whole swarm.
Kirstine.GridSearch
— TypeKirstine.GridSearch <: ZerothOrderOptimizer
A grid search optimizer.
This optimizer is experimental. It is not to be considered part of the API yet, hence it is not exported and may be subject to breaking changes even in minor or patch version updates.
This optimizer is particularly useful for the direction-finding step in Exchange
, but can also be used for DirectMaximization
as long as all weights of the candidate are fixed, and there are only few non-fixed design points. Note that for DirectMaximization
, the total number of grid points is exponential both in the dimension of the design region and in the number of non-fixed design points.
(Strictly speaking this optimizer is not particle-based, but it uses the same internal infrastructure.)
Initialization
Similar to optimization with Pso
, the optimizer state is initialized internally with a vector of prototype points. The first prototype determines the structure of each grid point that is constructed from the Kirstine.GridSpec
. This set of points is then merged with the vector of prototypes and used for maximization.
Kirstine.GridSearch
— MethodKirstine.GridSearch(; gridspec::Kirstine.GridSpec)
Set up parameters for a grid search.
The GridSpec
is used to construct a grid over the problem's DesignRegion
, so the dimensions of both must be equal.
Examples
julia> Kirstine.GridSearch(; gridspec = Kirstine.ProductGrid(11, 11))
Kirstine.GridSearch{Kirstine.ProductGrid{2}}(Kirstine.ProductGrid{2}((11, 11)))
Kirstine.GridSpec
— TypeKirstine.ProductGrid
— TypeKirstine.ProductGrid <: Kirstine.GridSpec
A Cartesian product with the given number of grid points in each dimension.
Examples
A 2-dimensional grid with 5×11 points:
julia> Kirstine.ProductGrid(5, 11)
Kirstine.ProductGrid{2}((5, 11))
Kirstine.FrankWolfe
— TypeFrankWolfe <: FirstOrderOptimizer
A Frank-Wolfe optimizer.
This optimizer is experimental. It is not to be considered part of the API yet, hence it is not exported and may be subject to breaking changes even in minor or patch version updates.
It is currently intended to be used for the re-weighting step of Exchange
. For DirectMaximization
it can currently only be used when all design points are fixed.
Two variants are implemented: a basic one that always moves towards the best vertex for the linearized problem (see Algorithm 4 in [P23]), and a version with pairwise steps (see Algorithm 2 in [LJ15]) that is better at finding an optimum on the boundary.
Initialization
Frank-Wolfe can only be initialized with a single prototype. This prototype will be used as the starting value.
Kirstine.FrankWolfe
— MethodFrankWolfe(; iterations::Integer, stepsize::StepSizeStrategy, pairwise::Bool)
Set up parameters for the Frank-Wolfe algorithm.
Kirstine.StepSizeStrategy
— TypeStepSizeStrategy
Supertype for Frank-Wolfe step-size strategies.
Kirstine.OpenLoop
— TypeOpenLoop <: StepSizeStrategy
OpenLoop(; shift = 2)
Frank-Wolfe step-size strategy $γ(t) = l / (t + l)$ for $t=0,1,2,…$ with shift $l∈ℕ, l≥1$.
With OpenLoop
, the algorithm will always land on a vertex of the feasible set at $t=1$. For a probability simplex, these vertices are the one-point designs. As a consequence, OpenLoop
is not suitable for the re-weighting step of Exchange
for design problems where one-point designs are singular.
Kirstine.ConstNumIter
— TypeConstNumIter <: StepSizeStrategy
Frank-Wolfe step-size strategy $γ(t) = 1 / \sqrt{T}$ for a run with $T$ iterations $t=0,…,T-1$.
Kirstine.Adaptive
— TypeAdaptive <: StepSizeStrategy
Adaptive(; eta = 0.9, tau = 2.0, initial_smoothness_estimate = Inf, smoothness_threshold = 1e10)
Adaptive Frank-Wolfe step-size strategy that uses a smoothness property of the objective function.
Since we typically do not know the smoothness constant of the objective function $f$, i.e. a real number $L$ such that $⟨∇f(y) - ∇f(x), y - x⟩ ≤ L\lVert y-x\rVert^2$ for all $x$ and $y$ in the convex domain of $f$, the constant is dynamically approximated via multiplicative search with parameters eta ≤ 1
and tau > 1
. When initial_smoothness_estimate = Inf
, the initial estimate is computed from a local approximation at the starting value.
If at any point during the search the smoothness estimate exceeds smoothness_threshold
, this is interpreted as the divergence of the multiplicative search, and the current (non-optimal) step size is returned.
This strategy is an implementation of Algorithm 5 from [P23], and the defaults of the parameters are taken from the adaptive line search in FrankWolfe.jl.
Design Criteria
Kirstine.DesignCriterion
— TypeKirstine.DesignCriterion
Supertype for optimal experimental design criteria.
See also DCriterion
, ACriterion
.
Kirstine.DCriterion
— TypeDCriterion <: Kirstine.DesignCriterion
Criterion for D-optimal experimental design.
Log-determinant of the normalized information matrix.
See also the mathematical background.
Kirstine.ACriterion
— TypeACriterion <: Kirstine.DesignCriterion
Criterion for A-optimal experimental design.
Trace of the inverted normalized information matrix.
See also the mathematical background.
Kirstine.objective
— Functionobjective(d::DesignMeasure, adp::Kirstine.AbstractDesignProblem)
Evaluate the objective function.
See also the mathematical background.
Kirstine.gateauxderivative
— Functiongateauxderivative(
at::DesignMeasure,
directions::AbstractArray{DesignMeasure},
dp::DesignProblem,
)
The directions are allowed to have different numbers of points.
See also the mathematical background.
Kirstine.efficiency
— Functionefficiency(d1::DesignMeasure, d2::DesignMeasure, dp::DesignProblem)
efficiency(d1::DesignMeasure, d2::DesignMeasure, dp1::DesignProblem, dp2::DesignProblem)
Relative D-efficiency of d1
to d2
.
Note that for the method with two design problems, the dimensions of the transformed parameters must be identical. All other aspects are allowed to be different.
See also the mathematical background.
This always computes D-efficiency, regardless of the criterion used in dp
.
Kirstine.shannon_information
— Functionshannon_information(d::DesignMeasure, dp::DesignProblem, n::Integer)
Compute the approximate expected posterior Shannon information for an experiment with n
units of observation under design d
and design problem dp
.
See also the mathematical background.
d
is notapportion
ed.- Shannon information is motivated by D-optimal design, so this function ignores the type of
criterion(dp)
.
Design Regions
Kirstine.DesignRegion
— TypeKirstine.DesignRegion{N}
Supertype for design regions. A design region is a compact subset of $\Reals^N$.
The design points of a DesignMeasure
are taken from this set.
See also DesignInterval
, dimnames
.
Kirstine.DesignInterval
— TypeDesignInterval{N} <: Kirstine.DesignRegion{N}
A (hyper)rectangular subset of $\Reals^N$.
See also lowerbound
, upperbound
, dimnames
.
Kirstine.DesignInterval
— MethodDesignInterval(names, lowerbounds, upperbounds)
Construct a design interval.
Examples
julia> DesignInterval([:dose, :time], [0, 0], [300, 20])
DesignInterval(:dose => (0.0, 300.0), :time => (0.0, 20.0))
Kirstine.DesignInterval
— MethodDesignInterval(name_bounds::Pair...)
Construct a design interval from name => (lb, ub)
pairs for individual dimensions.
Note that the order of the arguments matters and that it should match the implementation of map_to_covariate!
for your Model
and Covariate
subtypes.
Examples
julia> DesignInterval(:dose => (0, 300), :time => (0, 20))
DesignInterval(:dose => (0.0, 300.0), :time => (0.0, 20.0))
Kirstine.dimension
— Methoddimension(dr::Kirstine.DesignRegion{N})
Return the dimension N
of the design region.
Kirstine.dimnames
— Functiondimnames(dr::Kirstine.DesignRegion)
Return the names of the design region's dimensions.
Kirstine.upperbound
— Functionupperbound(dr::DesignInterval)
Return the vector of upper bounds.
Kirstine.lowerbound
— Functionlowerbound(dr::DesignInterval)
Return the vector of lower bounds.
Nonlinear Regression Models
Regression models are implemented by first subtyping Kirstine.Model
, Kirstine.Covariate
, Kirstine.CovariateParameterization
, and Kirstine.Parameter
, and then defining several helper methods on them. See the tutorial for a hands-on example.
Supertypes
Kirstine.Model
— TypeKirstine.Model
Supertype for statistical models.
Kirstine.NonlinearRegression
— TypeKirstine.NonlinearRegression
Supertype for user-defined nonlinear regression models.
Kirstine.Covariate
— TypeKirstine.Covariate
Supertype for user-defined model covariates.
Implementation
Subtypes of Kirstine.Covariate
should be mutable, or have at least a mutable field. This is necessary for being able to modify them in map_to_covariate!
.
Kirstine.CovariateParameterization
— TypeKirstine.CovariateParameterization
Supertype for user-defined mappings from design points to model covariates.
See also implied_covariates
.
Kirstine.Parameter
— TypeKirstine.Parameter
Supertype for Model
parameters.
Implementing a Nonlinear Regression Model
Suppose the following subtypes have been defined:
M <: Kirstine.NonlinearRegression
C <: Kirstine.Covariate
Cp <: Kirstine.CovariateParameterization
P <: Kirstine.Parameter
Then methods need to be added for the following package-internal functions:
Kirstine.allocate_covariate
— FunctionKirstine.allocate_covariate(m::M) -> c::C
Construct a single covariate for the model.
Implementation
For user types M <: Kirstine.Model
and a corresponding C <: Kirstine.Covariate
, this should construct a single instance c
of C
with some (model-specific) sensible initial value.
See also the mathematical background.
Kirstine.jacobianmatrix!
— FunctionKirstine.jacobianmatrix!(jm::AbstractMatrix{<:Real}, m::M, c::C, p::P) -> jm
Compute Jacobian matrix of the mean function at p
.
Implementation
For user types M <: Kirstine.NonlinearRegression
, C <: Kirstine.Covariate
, and P <: Kirstine.Parameter
, this should fill in the elements of the pre-allocated Jacobian matrix jm
, and finally return jm
.
Note that you are free how you map the partial derivatives to the columns of jm
. The only thing that is required is to use the same order in any DeltaMethod
that you want to implement.
See also the mathematical background.
Kirstine.map_to_covariate!
— FunctionKirstine.map_to_covariate!(c::C, dp::AbstractVector{<:Real}, m::M, cp::Cp) -> c
Map a design point to a model covariate.
Implementation
For user types C <: Kirstine.Covariate
, M <: Kirstine.Model
, and Cp <: Kirstine.CovariateParameterization
this should set the fields of the single covariate c
according to the single design point dp
. Finally, this method should return c
.
See also implied_covariates
and the mathematical background.
Kirstine.update_model_vcov!
— FunctionKirstine.update_model_vcov!(Sigma::Matrix, m::M, c::C)
Compute variance-covariance matrix of the nonlinear regression model for one unit of observation.
Implementation
For user types C <: Kirstine.Covariate
and M <: Kirstine.NonlinearRegression
, this should fill in the elements of Sigma
for a unit of observation with covariate c
. The size
of the matrix is (unit_length(m), unit_length(m))
.
See also the mathematical background.
Kirstine.unit_length
— FunctionKirstine.unit_length(m::M) -> Integer
Return the length of one unit of observation.
Implementation
For a user type M <: Kirstine.Model
this should return the length $\DimUnit$ of one unit of observation $\Unit$.
See also the mathematical background.
Kirstine.dimension
— FunctionKirstine.dimension(p::P) -> Integer
Return dimension of the parameter space.
Implementation
For a user type P <: Kirstine.Parameter
, this should return the dimension of the parameter space.
See also the mathematical background.
Some of these methods only require M <: Kirstine.Model
and not M <: Kirstine.NonlinearRegression
. This is intentional. These methods can in principle also be implemented for M <: Ma
where abstract type Ma <: Kirstine.Model end
is also user-defined.
Helpers
To reduce boilerplate code in the common cases of a one-dimensional unit of observation, and for a vector parameter without any additional structure, the following helper macros can be used:
Kirstine.@simple_model
— Macro@simple_model name covariate_field_names...
Generate code for defining a NonlinearRegression
model, a corresponding Covariate
, and helper functions for a 1-dimensional unit of observation with constant measurement variance.
This macro will define the two types $(name)Model
and $(name)Covariate
, hence name
should start with a capital letter. The model constructor will have the measurement standard deviation sigma
as a mandatory argument.
Examples
The call
@simple_model Emax dose
is equivalent to manually spelling out
@kwdef struct EmaxModel <: Kirstine.NonlinearRegression
sigma::Float64
function EmaxModel(sigma::Real)
if sigma <= 0
throw(ArgumentError("sigma must be positive"))
end
return new(sigma)
end
end
mutable struct EmaxCovariate <: Kirstine.Covariate
dose::Float64
end
Kirstine.unit_length(m::EmaxModel) = 1
function Kirstine.update_model_vcov!(Sigma::Matrix{Float64}, m::EmaxModel, c::EmaxCovariate)
Sigma[1, 1] = m.sigma^2
return Sigma
end
Kirstine.allocate_covariate(m::EmaxModel) = EmaxCovariate(0)
Note that this works both when you are using Kirstine
and when you are import
ing it, even if you do something like
import Kirstine as Smith
Further note that the @simple_model
macro can be used together with, or independently of @simple_parameter
.
Kirstine.@simple_parameter
— Macro@simple_parameter name field_names...
Generate code for defining a subtype of Parameter
with the given name and fields, and define its dimension as length(field_names)
.
This macro will define the type $(name)Parameter
, hence name
should start with a capital letter. The type will have a keyword constructor. All fields will have the type Float64
.
Examples
The call
@simple_parameter Emax e0 emax ec50
is equivalent to
@kwdef struct EmaxParameter <: Kirstine.Parameter
e0::Float64
emax::Float64
ec50::Float64
end
Kirstine.dimension(p::EmaxParameter) = 2
An EmaxParameter
object with e0=1
, emax=2
, and ec50=4
can then be constructed by
EmaxParameter(; e0 = 1, emax = 2, ec50 = 4)
Note that this works both when you are using Kirstine
and when you are import
ing it, even if you do something like
import Kirstine as Smith
Further note that the @simple_parameter
macro can be used together with, or independently of @simple_model
.
In problems where the design variables coincide with the model covariates, the following Kirstine.CovariateParameterization
can be used:
Kirstine.JustCopy
— TypeJustCopy <: Kirstine.CovariateParameterization
A convenience type to use when design variables and model covariates are identical.
Kirstine.JustCopy
— MethodJustCopy(names::Symbol...)
Construct a CovariateParameterization
that copies the elements of a design point to the fields of a Covariate
with the given names
.
This parameterization can be used with any Covariate
that has only Float64
fields. Note that the order of the names
should match the the order of dimnames(region(prob))
for the DesignProblem
in which JustCopy
is used. No new method for map_to_covariate!
needs to be implemented for user types since the package supplies a generic one that works with any Model
and any Covariate
.
See also the tutorial for another usage example.
Examples
When we have
julia> @simple_model Foo a b
julia> @simple_parameter Foo θ
we can define
julia> cp = JustCopy(:a, :b)
JustCopy(:a, :b)
and then solve
julia> dp = DesignProblem(;
region = DesignInterval(:a => (0, 1), :b => (-2, 5)),
covariate_parameterization = cp,
criterion = DCriterion(),
model = FooModel(; sigma = 1),
prior_knowledge = PriorSample([FooParameter(; θ = 42)]),
);
whithout first having to add a method to map_to_covariate!
for the FooModel
and FooCovariate
types.
Note however that the equivalent manual version
julia> struct CopyBoth <: Kirstine.CovariateParameterization end
julia> function Kirstine.map_to_covariate!(c::FooCovariate, dp, m::FooModel, cp::CopyBoth)
c.a = dp[1]
c.b = dp[2]
return c
end
would be slightly more efficient since it knows the types and field names at compile time.
Also note that since the design points are assigned in the order of the names given, cp = JustCopy(:b, :a)
would be equivalent to setting c.b = dp[1]
and c.a = dp[2]
.
Prior Knowledge
Kirstine.PriorKnowledge
— TypeKirstine.PriorKnowledge{T<:Kirstine.Parameter}
Supertype for representing prior knowledge of the model Parameter
.
See also PriorSample
.
Kirstine.PriorSample
— TypePriorSample{T} <: Kirstine.PriorKnowledge{T}
A sample from a prior distribution, or a discrete prior distribution with finite support.
Kirstine.PriorSample
— MethodPriorSample(p::AbstractVector{<:Kirstine.Parameter} [, weights::AbstractVector{<:Real}])
Construct a weighted prior sample on the given parameter draws.
If no weights
are given, a uniform distribution on the elements of p
is assumed.
See also parameters
, weights(::PriorSample)
.
Base.:==
— Method==(pk1::PriorSample, pk2::PriorSample)
Test prior samples for equality.
Two prior samples are considered equal iff
- they have the same subtype of
Parameter
, - and their parameter draws are equal,
- and their weights are equal,
- and the order of the draws is equal.
Note that this is stricter than when comparing prior samples as mathematical sets, where the order of the draws does not matter.
Kirstine.parameters
— Methodparameters(pk::PriorSample)
Return a reference to the parameters of the PriorSample
.
See also weights(::DesignMeasure)
.
Kirstine.weights
— MethodTransformations
Kirstine.Transformation
— TypeKirstine.Transformation
Supertype of posterior transformations.
See also Identity
, DeltaMethod
, and the mathematical background.
Kirstine.Identity
— TypeIdentity <: Kirstine.Transformation
The Transformation
that maps a parameter to itself.
Kirstine.DeltaMethod
— TypeDeltaMethod <: Kirstine.Transformation
DeltaMethod(jacobian_matrix::Function)
A nonlinear Transformation
of the model parameter.
The delta method uses the Jacobian matrix $\TotalDiff\Transformation$ to map the asymptotic multivariate normal distribution of $\Parameter$ to the asymptotic multivariate normal distribution of $\Transformation(\Parameter)$.
To construct a DeltaMethod
object, jacobian_matrix
must be a function that maps a Parameter
p
to the Jacobian matrix of $\Transformation$ evaluated at p
.
Note that the order of the columns must match the order that you used when implementing the jacobianmatrix!
for your model.
The Dimension of the Transformed Parameter
Several places in the package need to know the dimension of $\Transformation(\Parameter)$. By default, this will be computed by evaluating $\TotalDiff\Transformation(\Parameter)$ for one $\Parameter$ from the prior sample, and then counting the number of rows of this matrix. However, this may be undesirable when $\TotalDiff\Transformation$ is expensive to compute (e.g., when it requires numerical integration). To prevent this default behavior, you can implement a method for the internal function transformed_parameter_dimension
that dispatches on the types of your jacobian_matrix
function and your model parameter.
For example, suppose you have an 8-dimensional MyParameter <: Parameter
and an expensive transformation to something 5-dimensional. Then you would write
function expensive_dt(p::MyParameter)
# some expensive computations that return a matrix with size (5, 8)
end
function Kirstine.transformed_parameter_dimension(dt::typeof(expensive_dt), p::MyParameter)
return 5
end
Examples
Suppose p
has the fields a
and b
, and $\Transformation(\Parameter) = (ab, b/a)'$. Then the Jacobian matrix of $\Transformation$ is
\[\TotalDiff\Transformation(\Parameter) = \begin{bmatrix} b & a \\ -b/a^2 & 1/a \\ \end{bmatrix}.\]
In Julia this is equivalent to
jm1(p) = [p.b p.a; -p.b/p.a^2 1/p.a]
DeltaMethod(jm1)
Note that for a scalar quantity, e.g. $\Transformation(\Parameter) = \sqrt{ab}$, the Jacobian matrix is a row vector.
jm2(p) = [b a] ./ (2 * sqrt(p.a * p.b))
DeltaMethod(jm2)
Normal Approximations
Kirstine.NormalApproximation
— TypeKirstine.NormalApproximation
Supertype for different possible normal approximations to the posterior distribution.
Kirstine.FisherMatrix
— TypeFisherMatrix
Normal approximation based on the maximum-likelihood approach.
The information matrix is obtained as the average of the Fisher information matrix with respect to the design measure. Singular information matrices can occur.
See also the mathematical background.
Design Measures
Kirstine.DesignMeasure
— TypeDesignMeasure
A probability measure with finite support representing a continuous experimental design.
The support points of a design measure are called design points. In Julia, a design point is simply a Vector{Float64}
.
Special kinds of design measures can be constructed with one_point_design
, uniform_design
, equidistant_design
, random_design
.
See also weights(::DesignMeasure)
, points
, apportion
, plotting API.
Base.:==
— Method==(d1::DesignMeasure, d2::DesignMeasure)
Test design measures for equality.
Two design measures are considered equal iff
- they have the same number of design points,
- and their design points and weights are equal,
- and their design points are in the same order.
Note that this is stricter than when comparing measures as mathematical functions, where the order of the design points in the representation does not matter.
Kirstine.DesignMeasure
— MethodDesignMeasure(
points::AbstractMatrix{<:Real},
weights::AbstractVector{<:Real}
)
Construct a design measure with design points from the columns of points
.
This is the only design measure constructor where the result does share memory with points
and weights
.
Kirstine.DesignMeasure
— MethodDesignMeasure(
points::AbstractVector{<:AbstractVector{<:Real}},
weights::AbstractVector{<:Real},
)
Construct a design measure from a vector of design points.
The result does not share memory with points
.
Examples
julia> DesignMeasure([[1, 2], [3, 4], [5, 6]], [0.5, 0.2, 0.3])
DesignMeasure(
[1.0, 2.0] => 0.5,
[3.0, 4.0] => 0.2,
[5.0, 6.0] => 0.3,
)
Kirstine.DesignMeasure
— MethodDesignMeasure(dp_w::Pair...)
Construct a design measure from designpoint => weight
pairs.
Examples
julia> DesignMeasure([1] => 0.2, [42] => 0.3, [9] => 0.5)
DesignMeasure(
[1.0] => 0.2,
[42.0] => 0.3,
[9.0] => 0.5,
)
Kirstine.one_point_design
— Functionone_point_design(designpoint::AbstractVector{<:Real})
Construct a one-point DesignMeasure
.
The result does not share memory with designpoint
.
Examples
julia> one_point_design([42])
DesignMeasure(
[42.0] => 1.0,
)
Kirstine.uniform_design
— Functionuniform_design(designpoints::AbstractVector{<:AbstractVector{<:Real}})
Construct a DesignMeasure
with equal weights on the given designpoints
.
The result does not share memory with designpoints
.
Examples
julia> uniform_design([[1], [2], [3], [4]])
DesignMeasure(
[1.0] => 0.25,
[2.0] => 0.25,
[3.0] => 0.25,
[4.0] => 0.25,
)
Kirstine.equidistant_design
— Functionequidistant_design(dr::DesignInterval{1}, K::Integer)
Construct a DesignMeasure
with an equally-spaced grid of K
design points and uniform weights on the given 1-dimensional design interval.
Examples
julia> equidistant_design(DesignInterval(:z => (0, 1)), 5)
DesignMeasure(
[0.0] => 0.2,
[0.25] => 0.2,
[0.5] => 0.2,
[0.75] => 0.2,
[1.0] => 0.2,
)
Kirstine.random_design
— Functionrandom_design(dr::Kirstine.DesignRegion, K::Integer)
Construct a random DesignMeasure
.
The design points are drawn independently from the uniform distribution on the design region, and the weights drawn from the uniform distribution on a simplex.
Kirstine.points
— Functionpoints(d::DesignMeasure)
Return an iterator over the design points of d
.
See also weights(::DesignMeasure)
, numpoints
.
Kirstine.weights
— Methodweights(d::DesignMeasure)
Return a reference to the weights of the design measure.
Kirstine.numpoints
— Functionnumpoints(d::DesignMeasure)
Return the number of design points of d
.
See also points
, weights(::DesignMeasure)
.
Kirstine.sort_points
— Functionsort_points(d::DesignMeasure; rev = false, dimorder = 1:length(first(points(d))))
Return a representation of d
where the design points are sorted lexicographically.
By default, the result is sorted by the first dimension of the design points, and ties are resolved by the second, further ties by the third, and so on. This can be changed by setting dimorder
to a different permutation.
See also sort_weights
.
Examples
julia> sort_points(uniform_design([[3, 40], [2, 10], [1, 20], [2, 30]]))
DesignMeasure(
[1.0, 20.0] => 0.25,
[2.0, 10.0] => 0.25,
[2.0, 30.0] => 0.25,
[3.0, 40.0] => 0.25,
)
julia> sort_points(uniform_design([[3, 40], [2, 10], [1, 20], [2, 30]]); dimorder = [2, 1])
DesignMeasure(
[2.0, 10.0] => 0.25,
[1.0, 20.0] => 0.25,
[2.0, 30.0] => 0.25,
[3.0, 40.0] => 0.25,
)
Kirstine.sort_weights
— Functionsort_weights(d::DesignMeasure; rev::Bool = false)
Return a representation of d
where the design points are sorted by their corresponding weights.
See also sort_points
.
Examples
julia> sort_weights(DesignMeasure([[1], [2], [3]], [0.5, 0.2, 0.3]))
DesignMeasure(
[2.0] => 0.2,
[3.0] => 0.3,
[1.0] => 0.5,
)
Kirstine.mixture
— Functionmixture(alpha, d1::DesignMeasure, d2::DesignMeasure)
Return the convex combination $α d_1 + (1-α) d_2$.
The result is not simplified, hence its design points might not be unique.
Kirstine.apportion
— Functionapportion(weights::AbstractVector{<:Real}, n::Integer)
Find integers a
with sum(a) == n
such that a ./ n
best approximates weights
.
This is the efficient design apportionment procedure from Pukelsheim[P06], p. 309.
apportion(d::DesignMeasure, n)
Apportion the weights of d
.
Kirstine.simplify
— Methodsimplify(d::DesignMeasure, dp::DesignProblem; maxweight = 0, maxdist = 0, uargs...)
A wrapper that calls simplify_unique
, simplify_merge
(with the design region of dp
), and simplify_drop
in that order.
Kirstine.simplify
— Methodsimplify(d::DesignMeasure, cdp::CompositeDesignProblem; maxweight = 0, maxdist = 0, uargs...)
A wrapper that calls simplify_merge
, and simplify_drop
in that order.
Note that uargs
are ignored since there is no obvious generalization of simplify_unique
for general composite design problems. When uargs
is not empty, a warning is printed.
Kirstine.simplify_drop
— Functionsimplify_drop(d::DesignMeasure, maxweight::Real)
Construct a new DesignMeasure
where all design points with weights smaller than or equal to maxweight
are removed.
The vector of remaining weights is re-normalized.
Kirstine.simplify_unique
— Functionsimplify_unique(d::DesignMeasure, dr::Kirstine.DesignRegion, m::M, cp::C; uargs...)
Construct a new DesignMeasure
that corresponds uniquely to its implied normalized information matrix.
The package default is a catch-all with the abstract types M = Model
and C = CovariateParameterization
, which simply returns a copy of d
.
When called via simplify
, user-model specific keyword arguments will be passed in uargs
.
Implementation
Users can specialize this method for their concrete subtypes M <: Kirstine.Model
and C <: Kirstine.CovariateParameterization
. It is intended for cases where the mapping from design measure to normalized information matrix is not one-to-one. This depends on the model and covariate parameterization used. In such a case, simplify_unique
should be implemented to select a canonical version of the design.
User-defined versions must have type annotations on all arguments to resolve method ambiguity.
Examples
For several worked examples, see the dose-time-response vignette.
Kirstine.simplify_merge
— Functionsimplify_merge(d::DesignMeasure, dr::Kirstine.DesignRegion, maxdist::Real)
Merge designpoints with a normalized distance smaller or equal to maxdist
.
Special case of simplify_merge
with f(z) = (z .- lb) ./ (ub .- lb)
, where ub
and lb
define the bounding box of dr
, and distfun(x, y) = norm(x - y)
.
In other words, the design points of d
are scaled into the N
-dimensional unit cube before measuring their pairwise distances. Note that the largest possible distance between points is sqrt(N)
.
Examples
After scaling into the unit square, the distance between the first two points less than 0.3
, hence they are merged.
julia> d = DesignMeasure([[2.25, 2], [3.5, 2.5], [10, 3]], [0.3, 0.1, 0.6]);
julia> simplify_merge(d, DesignInterval(:a => (1, 11), :b => (2, 4)), 0.3)
DesignMeasure(
[2.5624999999999996, 2.125] => 0.4,
[10.0, 3.0] => 0.6,
)
simplify_merge(d::DesignMeasure, f::Function, distfun::Function, maxdist::Real)
Merge design points with distfun(f(dp1), f(dp2))
smaller or equal to maxdist
.
The following two steps are repeated until all points are more than maxdist
apart:
- All pairwise distances are calculated.
- The two points closest to each other are averaged according to their relative weights.
Examples
The first example uses the 2-norm for measuring distances between the points directly, and merges the first two points since they are 1/sqrt(2)
apart. The second example uses the 1-norm instead, where the first two points have distance 1
, so nothing happens.
julia> d = DesignMeasure([[1, 2], [1.5, 2.5], [3, 3]], [0.1, 0.4, 0.5]);
julia> simplify_merge(d, identity, (x, y) -> sqrt(sum((x .- y) .^ 2)), 0.8)
DesignMeasure(
[1.4000000000000001, 2.4] => 0.5,
[3.0, 3.0] => 0.5,
)
julia> simplify_merge(d, identity, (x, y) -> sum(abs.(x - y)), 0.8)
DesignMeasure(
[1.0, 2.0] => 0.1,
[1.5, 2.5] => 0.4,
[3.0, 3.0] => 0.5,
)
The third example has two pairs of points with the same distance to each other. When simplified with a design region, either none or both are merged. However, when mapped onto the graph of the exp()
function, only the second pair will be merged since it is spread much wider than the first. Such a behavior might be desired with a more complicated expected response function in place of exp()
.
julia> d = uniform_design([[0.2], [0.3], [4.9], [5.0]]);
julia> simplify_merge(d, DesignInterval(:a => (0, 10)), 0.01)
DesignMeasure(
[0.25] => 0.5,
[4.95] => 0.5,
)
julia> simplify_merge(d, z -> [z[1], exp(z[1])], (x, y) -> sqrt(sum((x .- y) .^ 2)), 1)
DesignMeasure(
[0.25] => 0.5,
[4.9] => 0.25,
[5.0] => 0.25,
)
Kirstine.informationmatrix
— FunctionKirstine.informationmatrix(
d::DesignMeasure,
m::Model,
cp::Kirstine.CovariateParameterization,
p::Kirstine.Parameter,
na::Kirstine.NormalApproximation,
)
Compute the normalized information matrix for a single Parameter
value p
.
This function is useful for debugging.
See also apply_transformation
and the mathematical background.
Kirstine.apply_transformation
— FunctionKirstine.apply_transformation(nim::AbstractMatrix{<:Real}, p::Kirstine.Parameter, trafo::Kirstine.Transformation)
Compute the information matrix for the transformed Parameter
.
This function is useful for debugging.
See also informationmatrix
and the mathematical background.
Kirstine.implied_covariates
— Functionimplied_covariates(d::DesignMeasure, m::M, cp::Cp)
Return covariates that are implied by d
for instances of user types M <: Kirstine.Model
and Cp <: Kirstine.CovariateParameterization
.
This function is useful for debugging.
See also map_to_covariate!
.
Plotting
Kirstine.jl provides plot recipes for several (combinations of) types, instances of which can simply be plot()
ted. For Gateaux derivatives and expected functions there are separate methods.
plot(d::DesignMeasure)
plot(d::DesignMeasure, dr::Kirstine.DesignRegion)
plot(r::Kirstine.OptimizationResult)
plot(rs::AbstractVector{<:OptimizationResult})
plot(r::Kirstine.DirectMaximizationResult)
plot(r::Kirstine.ExchangeResult)
The method for plotting DesignMeasure
s has several options.
- When a
DesignRegion
is given,xlims
(and for 2-dimensional design points alsoylims
) is set to the boundaries of the region. - The
label_formatter
keyword argument accepts a function that maps a triple(k, pt, w)
of an index, design point, and weight to a string. By default, the weight is indicated in percentage points rounded to 3 significant digits. Note that due to rounding the weights do not necessarily add up to 100%. To achieve this, consider plotting anapportion
ed version of your design measure like this:DesignMeasure(points(d), apportion(weights(d), 1000) ./ 1000)
- The
maxmarkersize
keyword argument indicates themarkersize
(radius in pixels) of a design point with weight 1. Other points are scaled down according to thesqrt
of their weight. It will be overridden by an explicitly givenmarkersize
. The default ismaxmarkersize=10
. - The
minmarkersize
keyword argument gives the absolute lower bound for themarkersize
of a point. This prevents points with very little weight from becoming illegible. It will be overridden by an explicitly givenmarkersize
. The default isminmarkersize=2
.
In all cases, standard plotting attributes will be passed through.
The next plots illustrate these options:
using Kirstine, Plots
dr = DesignInterval(:a => (0, 1), :b => (-2.5, 3))
d = DesignMeasure(
[0.5, -1] => 0.1,
[0.2, 2] => 0.2,
[0.9, -2] => 0.2,
[0.1, -0.1] => 0.05,
[0.3, 0.4] => 0.45,
)
p1 = plot(d, title = "without region")
p2 = plot(d, dr, title = "with region")
p3 = plot(
d,
dr,
markercolor = :orange,
markershape = [:star :diamond :hexagon :x :+],
grid = nothing,
legend = :outerright,
title = "standard attributes",
)
pa = plot(p1, p2, p3)
p5 = plot(d, dr; maxmarkersize = 20, title = "maxmarkersize 20")
p6 = plot(d, dr; minmarkersize = 4, title = "minmarkersize 4")
p7 = plot(d, dr; markersize = 5, title = "markersize 5")
p8 = plot(d, dr; label_formatter = (k, p, w) -> "$k: a = $(p[1])", title = "custom labels")
pb = plot(p5, p6, p7, p8)
Since the design points are plotted in the given order, large points may be plotted over small ones. Unfortunately, the z-order of points can not be set independently in Plots.jl. Possible workarounds are sorting the points by decreasing weight before plotting, or choosing an unfilled marker shape.
d = DesignMeasure([0 1 1.01 2], [0.1, 0.1, 0.7, 0.1])
plot(d) # point at 1 not visible
plot(sort_weights(d; rev = true)) # point at 1 visible, but legend re-ordered
plot(d; markershape = :+) # all visible, but colors harder to distinguish
Kirstine.plot_gateauxderivative
— Functionplot_gateauxderivative(d::DesignMeasure, adp::Kirstine.AbstractDesignProblem; <keyword arguments>)
Plot the gateauxderivative
in directions from a grid on region(adp)
, overlaid with the design points of d
.
For 2-dimensional design regions, the default color gradient for the heat map is :diverging_bwr_55_98_c37_n256
, which is perceptually uniform from blue via white to red. Custom gradients can be set via the fillcolor
keyword argument. The standard plotting keyword arguments (markershape
, markercolor
, markersize
, linecolor
, linestyle
, clims
, ...) are supported. By default, markersize
indicates the design weights.
Additional Keyword Arguments
n_grid::Union{Integer, Tuple{Integer}, Tuple{Integer, Integer}}
: number of points in the grid. Must match the dimension of the design region. Defaults to101
for one dimension, and to(51, 51)
for two.overlay_points::Bool
: draw alsopoints(d)
on top of the graph. Defaults totrue
.- Keyword arguments for plotting
DesignMeasure
s:label_formatter
,minmarkersize
,maxmarkersize
. See the API reference for what they do.
Currently only implemented for 1- and 2-dimensional DesignRegion
s.
Kirstine.plot_expected_function
— Functionplot_expected_function(
f,
g,
h,
xgrid::AbstractVector{<:Real},
d::DesignMeasure,
m::Kirstine.Model,
cp::Kirstine.CovariateParameterization,
pk::PriorSample;
<keyword arguments>
)
Plot a graph of the pointwise expected value of a function and design points on that graph.
For user types C<:Kirstine.Covariate
and p<:Kirstine.Parameter
, the functions f
, g
, and h
should have the following signatures:
f(x::Real, c::C, p::P)::Real
. This function is for plotting a graph.g(c::C)::AbstractVector{<:Real}
. This function is for plotting points, and should return the x coordinates corresponding toc
.h(c::C, p::P)::AbstractVector{<:Real}
. This function is for plotting points, and should return the y coordinates corresponding toc
.
For each value of the covariate c
implied by the design measure d
, the following things will be plotted:
A graph of the average of
f
with respect topk
as a function ofx
evaluated at the elements ofxgrid
. If differentc
result in identical graphs, only one will be plotted.Points at
g(c)
andh(c, p)
averaged with respect topk
. By default, themarkersize
attribute is used to indicate the weight of the design point.
Additional Keyword Arguments
bands::Bool=false
: plot point-wise credible bands. Currently only implemented for uniformweights(d)
. Color and opacity can be controlled with the standard keyword argumentsfillcolor
andfillalpha
.probability::Real=0.8
: probability to use for the credible band.- Keyword arguments for plotting
DesignMeasure
s:label_formatter
,minmarkersize
,maxmarkersize
. See the API reference for what they do.
Examples
For usage examples see the tutorial and dose-time-response vignettes.
- YBT13Min Yang, Stefanie Biedermann, and Elina Tang (2013). On optimal designs for nonlinear models: a general and efficient algorithm. Journal of the American Statistical Association, 108(504), 1411–1420. doi:10.1080/01621459.2013.806268
- CK02Maurice Clerc and James Kennedy (2002). The particle swarm - explosion, stability, and convergence in a multidimensional complex space. IEEE Transactions on Evolutionary Computation, 6(1), 58–73. doi:10.1109/4235.985692
- ES00Russell C. Eberhardt and Yuhui Shi (2000). Comparing inertia weights and constriction factors in particle swarm optimization. Proceedings of the 2000 Congress on Evolutionary Computation. doi: 10.1109/CEC.2000.870279
- LJ15Simon Lacoste-Julien and Martin Jaggi (2015). On the Global Linear Convergence of Frank-Wolfe Optimization Variants. arXiv:1511.05932
- P23Sebastian Pokutta (2023). The Frank-Wolfe algorithm: a short introduction. Jahresbericht der Deutschen Mathematiker-Vereinigung, 126(1), 3–35. doi:10.1365/s13291-023-00275-x
- P06Friedrich Pukelsheim (2006). Optimal design of experiments. Wiley. doi:10.1137/1.9780898719109