Parameter Set Constraints
Opened this issue · 3 comments
When defining a ParameterSet
object with ranges corresponding to each of its variables (i.e. the arg_ranges
member), currently there isn't any way to constrain the parameter space using the relationships between the variables.
There should be logic to support these kinds of inter-variable relationship constraints. For example, take a simple strategy using two moving averages, one with a short lookback period and one with a long lookback period. Suppose our rule is to go long when the short MA crosses over the long MA, and we want to simulate this strategy over a bunch of pairs of lookback windows. There should be logic to constrain the simulation such that the short MA's lookback must be less than the long MA's lookback.
What your are looking for looks like a CSP solver
With Python, there is
- python-constraint
https://github.com/python-constraint/python-constraint
https://labix.org/python-constraint - Numberjack http://numberjack.ucc.ie/
I don't know if there is project like this written in Julia.
I also wonder if https://github.com/JuliaOpt/JuMP.jl have such feature (or more generally https://github.com/JuliaOpt )
See http://nbviewer.jupyter.org/github/JuliaOpt/juliaopt-notebooks/tree/master/notebooks/ for some sample notebooks
python-constraint is "only" 1500 lines of code
A simpler approach with Channel
and for
loops
with Tuple
for parameters
parameter_generator() = Channel(ctype=Tuple{Int,Int}) do c
for short in 1:10
for long in 1:10
if short < long
push!(c, (short, long))
end
end
end
end
for (i, param) in enumerate(parameter_generator())
short, long = param
println("i=$i short=$short long=$long")
end
or with Dict
for parameters
parameter_generator() = Channel(ctype=Dict{String,Int}) do c
for short in 1:10
for long in 1:10
if short < long
push!(c, Dict("short"=>short, "long"=>long))
end
end
end
end
for (i, param) in enumerate(parameter_generator())
short = param["short"]
long = param["long"]
println("i=$i short=$short long=$long")
end
it displays:
i=1 short=1 long=2
i=2 short=1 long=3
i=3 short=1 long=4
i=4 short=1 long=5
i=5 short=1 long=6
i=6 short=1 long=7
i=7 short=1 long=8
i=8 short=1 long=9
i=9 short=1 long=10
i=10 short=2 long=3
i=11 short=2 long=4
i=12 short=2 long=5
i=13 short=2 long=6
i=14 short=2 long=7
i=15 short=2 long=8
i=16 short=2 long=9
i=17 short=2 long=10
i=18 short=3 long=4
i=19 short=3 long=5
i=20 short=3 long=6
i=21 short=3 long=7
i=22 short=3 long=8
i=23 short=3 long=9
i=24 short=3 long=10
i=25 short=4 long=5
i=26 short=4 long=6
i=27 short=4 long=7
i=28 short=4 long=8
i=29 short=4 long=9
i=30 short=4 long=10
i=31 short=5 long=6
i=32 short=5 long=7
i=33 short=5 long=8
i=34 short=5 long=9
i=35 short=5 long=10
i=36 short=6 long=7
i=37 short=6 long=8
i=38 short=6 long=9
i=39 short=6 long=10
i=40 short=7 long=8
i=41 short=7 long=9
i=42 short=7 long=10
i=43 short=8 long=9
i=44 short=8 long=10
i=45 short=9 long=10
Using NamedTuples could also be considered
Code updated with NamedTuples (from https://github.com/JuliaData/NamedTuples.jl )
mutable struct ParameterSet
arg_names::Vector{Symbol}
arg_defaults::Vector
arg_ranges::Vector
arg_types::Vector{<:Type}
n_args::Int
constraints::Vector{Function}
function ParameterSet(arg_names::Vector{Symbol},
arg_defaults::Vector,
arg_ranges::Vector=[x:x for x in arg_defaults],
constraints=Vector{Function}())
@assert length(arg_names) == length(arg_defaults) == length(arg_ranges)
@assert eltype.(arg_defaults) == eltype.(arg_ranges)
arg_types::Vector{<:Type} = eltype.(arg_defaults)
return new(arg_names, arg_defaults, arg_ranges, arg_types, length(arg_names), constraints)
end
end
import Base: length
length(ps::ParameterSet) = mapreduce(length, *, 1, ps.arg_ranges) # inspired by IterTools.jl length(p::Product)
abstract type ParameterIteration end
struct CartesianProductIteration <: ParameterIteration
end
using IterTools
using NamedTuples: make_tuple
function get_iterator(ps::ParameterSet, method::CartesianProductIteration)
itr = IterTools.product(ps.arg_ranges...)
itr = map(t->make_tuple(ps.arg_names)(t...), itr)
#itr = map(t->NamedTuple{Tuple(ps.arg_names)}(t...), itr) # see https://github.com/JuliaData/NamedTuples.jl/issues/53
for constraint in ps.constraints
itr = Iterators.filter(constraint, itr)
end
itr
end
using Base.Test
# write your own tests here
@testset "Main tests" begin
arg_names = [:real1, :real2]
arg_defaults = [0.5, 0.05]
arg_ranges = [0.01:0.01:0.99, 0.01:0.01:0.99]
#constraints = [
# p -> p[1] + p[2] < 1.0,
# p -> p[1] + p[2] > 0.5
#]
constraints = [
p -> p.real1 + p.real2 < 1.0,
p -> p.real1 + p.real2 > 0.5
]
ps = ParameterSet(arg_names, arg_defaults, arg_ranges, constraints)
@test length(ps) == 9801 # 99*99
itr_method = CartesianProductIteration()
#itr_method = RandomIteration()
itr = get_iterator(ps, itr_method)
#for p in itr
# println(p)
#end
v = collect(itr)
#println(v)
@test length(v) == 3626
end
@testset "Other test" begin
arg_names = [:short, :long]
arg_defaults = [1, 1]
arg_ranges = [1:10, 1:10]
#constraints = [
# p -> p[1] < p[2]
#]
constraints = [
p -> p.short < p.long
]
ps = ParameterSet(arg_names, arg_defaults, arg_ranges, constraints)
@test length(ps) == 100 # 10*10
itr_method = CartesianProductIteration()
#itr_method = RandomIteration()
itr = get_iterator(ps, itr_method)
v = collect(itr)
@test length(v) == 45
end