Skip to content

Commit 3f788de

Browse files
authored
Solve compatibility with Julia 0.6 (#341, #330, #293)
* Initial support for infix ~ (#173). * seperate model wrapper call in sample.jl test * update ForwardDiff.Dual signature * sample.jl test passed * fix some tests for Julia 0.6 * remove typealias for 0.6 * make some functions 0.6-ish * make abstract 0.6-ish * change some decpreated functions * Update .travis.yml * Update appveyor.yml * Update appveyor.yml * fix test * Deprecations on package loading fixed * fix deprecations * implement callbacks for inner function * fix model type bug * Fix type * update Dual in benchmark * update Dual constructor * Bump up required Julia version to 0.6 * Disable depreciated warning messages for `consume/produce`. * Remove duplicate definition of produce. * fix floor, tanh, abs, log * fix logpdf warning and bug * fix vec assume init * Travis: Allow `Benchmarking` test to fail.
1 parent 7a19ab0 commit 3f788de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+434
-316
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ addons:
88
- g++-5
99
language: julia
1010
julia:
11-
- 0.5
11+
- 0.6
1212
os:
1313
- linux
1414
- osx
@@ -36,6 +36,7 @@ matrix:
3636
allow_failures:
3737
- env: GROUP=Test
3838
os: osx
39+
- env: GROUP=Bench
3940
- env: GROUP=LDA
4041
- env: GROUP=MOC
4142
- env: GROUP=SV

REQUIRE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
julia 0.5
1+
julia 0.6
22

33
Stan
44
Distributions 0.11.0

appveyor.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
environment:
22
matrix:
3-
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.5/julia-0.5.0-win64.exe"
3+
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6.0-win64.exe"
44
MINGW_DIR: mingw64
55
# MINGW_URL: https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/5.3.0/threads-win32/seh/x86_64-5.3.0-release-win32-seh-rt_v4-rev0.7z/download
66
MINGW_URL: http://mlg.eng.cam.ac.uk/hong/x86_64-5.3.0-release-win32-seh-rt_v4-rev0.7z
77
MINGW_ARCHIVE: x86_64-5.3.0-release-win32-seh-rt_v4-rev0.7z
8-
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.5/julia-0.5.0-win32.exe"
8+
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6.0-win32.exe"
99
MINGW_DIR: mingw32
1010
# MINGW_URL: https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/5.3.0/threads-win32/dwarf/i686-5.3.0-release-win32-dwarf-rt_v4-rev0.7z/download
1111
MINGW_URL: http://mlg.eng.cam.ac.uk/hong/i686-5.3.0-release-win32-dwarf-rt_v4-rev0.7z

benchmarks/optimization.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ optRes *= "realpart(): \n"
290290

291291
using ForwardDiff: Dual
292292

293-
ds = [Dual{10,Float64}(rand()) for i = 1:1000]
293+
ds = [Dual{Void,Float64,10}(rand()) for i = 1:1000]
294294

295295
t_map = @elapsed for i = 1:1000 map(d -> d.value, ds) end
296296
t_list = @elapsed for i = 1:1000 Float64[ds[i].value for i = 1:length(ds)] end
@@ -304,7 +304,7 @@ optRes *= "Constructing Dual numbers: \n"
304304

305305
dps = zeros(44); dps[11] = 1;
306306

307-
t_dualnumbers = @elapsed for _ = 1:(44*2000*5) ForwardDiff.Dual(1.1, dps...) end
307+
t_dualnumbers = @elapsed for _ = 1:(44*2000*5) ForwardDiff.Dual{Void, Float64, 44}(1.1, dps) end
308308

309309
optRes *= "44*2000*5 times: $t_dualnumbers\n"
310310

example-models/nips-2017/gmm.model.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ p = [ 0.2, 0.2, 0.2, 0.2, 0.2]
2828
# μ = [ 0, 1, 2, 3.5, 4.25] + 0.5 * collect(0:4)
2929
μ = [ 0, 1, 2, 3.5, 4.25] + 2.5 * collect(0:4)
3030
s = [-0.5, -1.5, -0.75, -2, -0.5]
31-
σ = exp(s)
31+
σ = exp.(s)

example-models/nips-2017/sv.model.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
μ ~ Cauchy(0, 10)
66
h = tzeros(Real, T)
77
h[1] ~ Normal(μ, σ / sqrt(1 - ϕ^2))
8-
y[1] ~ Normal(0, exp(h[1] / 2))
8+
y[1] ~ Normal(0, exp.(h[1] / 2))
99
for t = 2:T
1010
h[t] ~ Normal+ ϕ * (h[t-1] - μ) , σ)
11-
y[t] ~ Normal(0, exp(h[t] / 2))
11+
y[t] ~ Normal(0, exp.(h[t] / 2))
1212
end
1313
end

example-models/nips-2017/sv.sim.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ for t in 2:T
1616
end
1717
y = Vector{Float64}(T);
1818
for t in 1:T
19-
y[t] = rand(Normal(0, exp(h[t] / 2)));
19+
y[t] = rand(Normal(0, exp.(h[t] / 2)));
2020
end
2121

2222

example-models/nuts-paper/lr_helper.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ readlrdata() = begin
77
open("lr_nuts.data") do f
88
while !eof(f)
99
raw_line = readline(f)
10-
data_str = filter(str -> length(str) > 0, split(raw_line, r"[ ]+")[1:end-1])
10+
data_str = Iterators.filter(str -> length(str) > 0, split(raw_line, r"[ ]+")[1:end-1])
1111
data = map(str -> parse(str), data_str)
1212
x = cat(1, x, data[1:end-1]')
1313
y = cat(1, y, data[end] - 1) # turn {1, 2} to {0, 1}

example-models/nuts-paper/sv_nuts.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ y = readsvdata()
1414
s[1] ~ Exponential(1/100)
1515
for i = 2:N
1616
s[i] ~ Normal(log(s[i-1]), τ)
17-
s[i] = exp(s[i])
17+
s[i] = exp.(s[i])
1818
dy = log(y[i] / y[i-1]) / s[i]
1919
dy ~ TDist(ν)
2020
end

example-models/sgld-paper/lr_helper.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ readlrdata() = begin
88
open("a9a2000.data") do f
99
while !eof(f)
1010
raw_line = readline(f)
11-
data_str = filter(str -> length(str) > 0, split(raw_line, r"[ ]+")[1:end-1])
11+
data_str = Iterators.filter(str -> length(str) > 0, split(raw_line, r"[ ]+")[1:end-1])
1212
data = map(str -> parse(str), data_str)
1313
x_tmp = zeros(Int32, d)
1414
x_tmp[data[2:end]] = 1

example-models/stan-models/normal-mixture.model.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ using ForwardDiff: Dual
88

99
theta ~ Uniform(0, 1)
1010

11-
mu = tzeros(Dual, 2)
11+
mu = Vector{Real}(2) # mu is sampled by HMC
1212
for i = 1:2
1313
mu[i] ~ Normal(0, 10)
1414
end
@@ -25,6 +25,6 @@ using ForwardDiff: Dual
2525
# logtheta_p = map(yᵢ -> [log(theta) + logpdf(Normal(mu[1], 1.0), yᵢ), log(1 - theta) + logpdf(Normal(mu[2], 1.0), yᵢ)], y)
2626
# map!(logtheta_pᵢ -> logtheta_pᵢ - logsumexp(logtheta_pᵢ), logtheta_p) # normalization
2727
# for i = 1:N
28-
# k[i] ~ Categorical(exp(logtheta_p[i]))
28+
# k[i] ~ Categorical(exp.(logtheta_p[i]))
2929
# end
3030
end

src/Turing.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ global const CACHERANGES = 0b01
5555
# Sampler abstraction #
5656
#######################
5757

58-
abstract InferenceAlgorithm
59-
abstract Hamiltonian <: InferenceAlgorithm
58+
abstract type InferenceAlgorithm end
59+
abstract type Hamiltonian <: InferenceAlgorithm end
6060

6161
doc"""
6262
Sampler{T}

src/core/ad.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ gradient(vi::VarInfo, model::Function, spl::Union{Void, Sampler}) = begin
5555
vals = getval(vi, vns[i])
5656
if vns[i] in vn_chunk # for each variable to compute gradient in this round
5757
for i = 1:l
58-
vi[range[i]] = ForwardDiff.Dual{CHUNKSIZE, Float64}(realpart(vals[i]), SEEDS[dim_count])
58+
vi[range[i]] = ForwardDiff.Dual{Void, Float64, CHUNKSIZE}(realpart(vals[i]), SEEDS[dim_count])
5959
dim_count += 1 # count
6060
end
6161
else # for other varilables (no gradient in this round)
6262
for i = 1:l
63-
vi[range[i]] = ForwardDiff.Dual{CHUNKSIZE, Float64}(realpart(vals[i]))
63+
vi[range[i]] = ForwardDiff.Dual{Void, Float64, CHUNKSIZE}(realpart(vals[i]))
6464
end
6565
end
6666
end
@@ -83,7 +83,7 @@ gradient(vi::VarInfo, model::Function, spl::Union{Void, Sampler}) = begin
8383
end
8484

8585
verifygrad(grad::Vector{Float64}) = begin
86-
if any(isnan(grad)) || any(isinf(grad))
86+
if any(isnan.(grad)) || any(isinf.(grad))
8787
dwarn(0, "Numerical error has been found in gradients.")
8888
dwarn(1, "grad = $(grad)")
8989
false

src/core/compiler.jl

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ macro VarName(ex::Union{Expr, Symbol})
77
# return: (:x,[1,2],6,45,3)
88
s = string(gensym())
99
if isa(ex, Symbol)
10-
_ = string(ex)
11-
return :(Symbol($_), Symbol($s))
10+
ex_str = string(ex)
11+
return :(Symbol($ex_str), Symbol($s))
1212
elseif ex.head == :ref
1313
_2 = ex
1414
_1 = ""
@@ -143,9 +143,9 @@ Example:
143143
```julia
144144
@model gauss() = begin
145145
s ~ InverseGamma(2,3)
146-
m ~ Normal(0,sqrt(s))
147-
1.5 ~ Normal(m, sqrt(s))
148-
2.0 ~ Normal(m, sqrt(s))
146+
m ~ Normal(0,sqrt.(s))
147+
1.5 ~ Normal(m, sqrt.(s))
148+
2.0 ~ Normal(m, sqrt.(s))
149149
return(s, m)
150150
end
151151
```
@@ -154,7 +154,7 @@ macro model(fexpr)
154154
# Compiler design: sample(fname_compiletime(x,y), sampler)
155155
# fname_compiletime(x=nothing,y=nothing; data=data,compiler=compiler) = begin
156156
# ex = quote
157-
# fname_runtime(;vi=VarInfo,sampler=nothing) = begin
157+
# fname_runtime(vi::VarInfo,sampler::Sampler) = begin
158158
# x=x,y=y
159159
# # pour all variables in data dictionary, e.g.
160160
# k = data[:k]
@@ -168,6 +168,7 @@ macro model(fexpr)
168168

169169

170170
dprintln(1, fexpr)
171+
fexpr = translate(fexpr)
171172

172173
fname = fexpr.args[1].args[1] # Get model name f
173174
fargs = fexpr.args[1].args[2:end] # Get model parameters (x,y;z=..)
@@ -178,31 +179,32 @@ macro model(fexpr)
178179
# ==> f(x,y;)
179180
# f(x,y; c=1)
180181
# ==> unchanged
182+
181183
if (length(fargs) == 0 || # e.g. f()
182184
isa(fargs[1], Symbol) || # e.g. f(x,y)
183185
fargs[1].head == :kw) # e.g. f(x,y=1)
184186
insert!(fargs, 1, Expr(:parameters))
185187
end
186188

187189
dprintln(1, fname)
188-
dprintln(1, fargs)
190+
# dprintln(1, fargs)
189191
dprintln(1, fbody)
190192

191193
# Remove positional arguments from inner function, e.g.
192194
# f((x,y; c=1)
193195
# ==> f(; c=1)
194196
# f(x,y;)
195197
# ==> f(;)
196-
fargs_inner = deepcopy(fargs)[1:1]
198+
# fargs_inner = deepcopy(fargs)[1:1]
197199

198200
# Add keyword arguments, e.g.
199201
# f(; c=1)
200202
# ==> f(; c=1, :vi=VarInfo(), :sample=nothing)
201203
# f(;)
202204
# ==> f(; :vi=VarInfo(), :sample=nothing)
203-
push!(fargs_inner[1].args, Expr(:kw, :vi, :(Turing.VarInfo())))
204-
push!(fargs_inner[1].args, Expr(:kw, :sampler, :(nothing)))
205-
dprintln(1, fargs_inner)
205+
# push!(fargs_inner[1].args, Expr(:kw, :vi, :(Turing.VarInfo())))
206+
# push!(fargs_inner[1].args, Expr(:kw, :sampler, :(nothing)))
207+
# dprintln(1, fargs_inner)
206208

207209
# Modify fbody, so that we always return VarInfo
208210
fbody_inner = deepcopy(fbody)
@@ -234,19 +236,31 @@ macro model(fexpr)
234236

235237
dprintln(1, fbody_inner)
236238

237-
fname_inner = Symbol("$(fname)_model")
239+
fname_inner_str = "$(fname)_model"
240+
fname_inner = Symbol(fname_inner_str)
238241
fdefn_inner = Expr(:(=), fname_inner,
239242
Expr(:function, Expr(:call, fname_inner))) # fdefn = :( $fname() )
240-
push!(fdefn_inner.args[2].args[1].args, fargs_inner...) # set parameters (x,y;data..)
243+
# push!(fdefn_inner.args[2].args[1].args, fargs_inner...) # set parameters (x,y;data..)
244+
245+
push!(fdefn_inner.args[2].args[1].args, :(vi::Turing.VarInfo))
246+
push!(fdefn_inner.args[2].args[1].args, :(sampler::Union{Void,Turing.Sampler}))
247+
241248
push!(fdefn_inner.args[2].args, deepcopy(fbody_inner)) # set function definition
242249
dprintln(1, fdefn_inner)
250+
251+
fdefn_inner_callback_1 = parse("$fname_inner_str(vi::Turing.VarInfo)=$fname_inner_str(vi,nothing)")
252+
fdefn_inner_callback_2 = parse("$fname_inner_str(sampler::Turing.Sampler)=$fname_inner_str(Turing.VarInfo(),nothing)")
253+
fdefn_inner_callback_3 = parse("$fname_inner_str()=$fname_inner_str(Turing.VarInfo(),nothing)")
243254

244255
compiler = Dict(:fname => fname,
245256
:fargs => fargs,
246257
:fbody => fbody,
247258
:dvars => Set{Symbol}(), # data
248259
:pvars => Set{Symbol}(), # parameter
249-
:fdefn_inner => fdefn_inner)
260+
:fdefn_inner => fdefn_inner,
261+
:fdefn_inner_callback_1 => fdefn_inner_callback_1,
262+
:fdefn_inner_callback_2 => fdefn_inner_callback_2,
263+
:fdefn_inner_callback_3 => fdefn_inner_callback_3)
250264

251265
# Outer function defintion 1: f(x,y) ==> f(x,y;data=Dict())
252266
fargs_outer = deepcopy(fargs)
@@ -263,14 +277,34 @@ macro model(fexpr)
263277

264278
fdefn_outer = Expr(:function, Expr(:call, fname, fargs_outer...),
265279
Expr(:block, Expr(:return, fname_inner)))
266-
280+
281+
unshift!(fdefn_outer.args[2].args, :(Main.eval(fdefn_inner_callback_3)))
282+
unshift!(fdefn_outer.args[2].args, :(Main.eval(fdefn_inner_callback_2)))
283+
unshift!(fdefn_outer.args[2].args, :(Main.eval(fdefn_inner_callback_1)))
267284
unshift!(fdefn_outer.args[2].args, :(Main.eval(fdefn_inner)))
268285
unshift!(fdefn_outer.args[2].args, quote
269286
# Check fargs, data
270287
eval(Turing, :(_compiler_ = deepcopy($compiler)))
271-
fargs = Turing._compiler_[:fargs];
272-
fdefn_inner = Turing._compiler_[:fdefn_inner];
273-
fdefn_inner.args[2].args[1].args[1] = gensym((fdefn_inner.args[2].args[1].args[1]))
288+
fargs = Turing._compiler_[:fargs];
289+
290+
# Copy the expr of function definition and callbacks
291+
fdefn_inner = Turing._compiler_[:fdefn_inner];
292+
fdefn_inner_callback_1 = Turing._compiler_[:fdefn_inner_callback_1];
293+
fdefn_inner_callback_2 = Turing._compiler_[:fdefn_inner_callback_2];
294+
fdefn_inner_callback_3 = Turing._compiler_[:fdefn_inner_callback_3];
295+
296+
# Add gensym to function name
297+
fname_inner_with_gensym = gensym((fdefn_inner.args[2].args[1].args[1]));
298+
299+
# Change the name of inner function definition to the one with gensym()
300+
fdefn_inner.args[2].args[1].args[1] = fname_inner_with_gensym
301+
fdefn_inner_callback_1.args[1].args[1] = fname_inner_with_gensym
302+
fdefn_inner_callback_1.args[2].args[2].args[1] = fname_inner_with_gensym
303+
fdefn_inner_callback_2.args[1].args[1] = fname_inner_with_gensym
304+
fdefn_inner_callback_2.args[2].args[2].args[1] = fname_inner_with_gensym
305+
fdefn_inner_callback_3.args[1].args[1] = fname_inner_with_gensym
306+
fdefn_inner_callback_3.args[2].args[2].args[1] = fname_inner_with_gensym
307+
274308
# Copy data dictionary
275309
for k in keys(data)
276310
if fdefn_inner.args[2].args[2].args[1].head == :line
@@ -295,7 +329,7 @@ macro model(fexpr)
295329
if _k != nothing
296330
_k_str = string(_k)
297331
dprintln(1, _k_str, " = ", _k)
298-
_ = quote
332+
data_check_ex = quote
299333
if haskey(data, keytype(data)($_k_str))
300334
if nothing != $_k
301335
Turing.dwarn(0, " parameter "*$_k_str*" found twice, value in data dictionary will be used.")
@@ -305,7 +339,7 @@ macro model(fexpr)
305339
data[keytype(data)($_k_str)] == nothing && Turing.derror(0, "Data `"*$_k_str*"` is not provided.")
306340
end
307341
end
308-
unshift!(fdefn_outer.args[2].args, _)
342+
unshift!(fdefn_outer.args[2].args, data_check_ex)
309343
end
310344
end
311345
unshift!(fdefn_outer.args[2].args, quote data = copy(data) end)
@@ -331,3 +365,15 @@ getvsym(expr::Expr) = begin
331365
end
332366
curr
333367
end
368+
369+
370+
translate!(ex::Any) = ex
371+
translate!(ex::Expr) = begin
372+
if (ex.head === :call && ex.args[1] === :(~))
373+
ex.head = :macrocall; ex.args[1] = Symbol("@~")
374+
else
375+
map(translate!, ex.args)
376+
end
377+
ex
378+
end
379+
translate(ex::Expr) = translate!(deepcopy(ex))

src/core/container.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Data structure for particle filters
55
- consume(pc::ParticleContainer): return incremental likelihood
66
"""
77

8-
typealias Particle Trace
8+
const Particle = Trace
99

1010
type ParticleContainer{T<:Particle}
1111
model :: Function
@@ -16,7 +16,7 @@ type ParticleContainer{T<:Particle}
1616
# conditional :: Union{Void,Conditional} # storing parameters, helpful for implementing rejuvenation steps
1717
conditional :: Void # storing parameters, helpful for implementing rejuvenation steps
1818
n_consume :: Int # helpful for rejuvenation steps, e.g. in SMC2
19-
ParticleContainer(m::Function,n::Int) = new(m,n,Array{Particle,1}(),Array{Float64,1}(),0.0,nothing,0)
19+
ParticleContainer{T}(m::Function,n::Int) where {T} = new(m,n,Array{Particle,1}(),Array{Float64,1}(),0.0,nothing,0)
2020
end
2121

2222
(::Type{ParticleContainer{T}}){T}(m) = ParticleContainer{T}(m, 0)
@@ -127,7 +127,7 @@ end
127127
function weights(pc :: ParticleContainer)
128128
@assert pc.num_particles == length(pc)
129129
logWs = pc.logWs
130-
Ws = exp(logWs-maximum(logWs))
130+
Ws = exp.(logWs-maximum(logWs))
131131
logZ = log(sum(Ws)) + maximum(logWs)
132132
Ws = Ws ./ sum(Ws)
133133
return Ws, logZ
@@ -196,5 +196,5 @@ getsample(pc :: ParticleContainer) = begin
196196
w = pc.logE
197197
Ws, z = weights(pc)
198198
s = map((i)->getsample(pc, i, Ws[i]), 1:length(pc))
199-
return exp(w), s
199+
return exp.(w), s
200200
end

src/core/io.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ getjuliatype(s::Sample, v::Symbol, cached_syms=nothing) = begin
1717
# NOTE: cached_syms is used to cache the filter entiries in svalue. This is helpful when the dimension of model is huge.
1818
if cached_syms == nothing
1919
# Get all keys associated with the given symbol
20-
syms = collect(filter(k -> search(string(k), string(v)*"[") != 0:-1, keys(s.value)))
20+
syms = collect(Iterators.filter(k -> search(string(k), string(v)*"[") != 0:-1, keys(s.value)))
2121
else
22-
syms = filter(k -> search(string(k), string(v)) != 0:-1, cached_syms)
22+
syms = collect((Iterators.filter(k -> search(string(k), string(v)) != 0:-1, cached_syms)))
2323
end
2424
# Map to the corresponding indices part
2525
idx_str = map(sym -> replace(string(sym), string(v), ""), syms)
2626
# Get the indexing component
27-
idx_comp = map(idx -> filter(str -> str != "", split(string(idx), [']','['])), idx_str)
27+
idx_comp = map(idx -> collect(Iterators.filter(str -> str != "", split(string(idx), [']','[']))), idx_str)
2828

2929
# Deal with v is really a symbol, e.g. :x
3030
if length(idx_comp) == 0

0 commit comments

Comments
 (0)