Implemented property based testing via PyCall.jl and hypothesis.py

This commit is contained in:
2024-02-15 11:41:35 +00:00
parent 88048baf31
commit 43a3d7a098
11 changed files with 359 additions and 84 deletions

View File

@@ -1,5 +1,4 @@
[deps]
AirspeedVelocity = "1c8270ee-6884-45cc-9545-60fa71ec23e4"
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
PropCheck = "ca382230-33be-11e9-0059-d981d03070e4"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

View File

@@ -67,10 +67,10 @@ if abspath(PROGRAM_FILE) == @__FILE__
current_package_version = Pkg.TOML.parsefile("$ROOT_DIR/Project.toml")["version"]
function display_to_file(io, x)
show(IOContext(io, :limit => false), "text/plain", x)
show(IOContext(io, :limit => false, :color => true), "text/plain", x)
end
open("$ROOT_DIR/benchmarks/$current_package_version.txt", "w") do io
open("$ROOT_DIR/benchmarks/$current_package_version.ansi", "w") do io
println(io, "C Implementation")
display_to_file(io, result[1])
println(io, "\n\nFortran Implementation")

View File

@@ -53,6 +53,6 @@ done
# And, finally, we can compile the C, and Fortran implementations
cd $bhmie_dir
gcc -shared -fPIC -o bhmie-c/bhmie.so bhmie-c/bhmie.c bhmie-c/complex.c bhmie-c/nrutil.c -lm -Wno-builtin-declaration-mismatch -Wno-implicit-function-declaration
gfortran -shared -fPIC -o bhmie-f/bhmie.so bhmie-f/bhmie.f
gfortran -shared -fPIC -o bhmie-f/bhmie_f77.so bhmie-f/bhmie_f77.f
gcc -g -shared -fPIC -o bhmie-c/bhmie.so bhmie-c/bhmie.c bhmie-c/complex.c bhmie-c/nrutil.c -lm -Wno-builtin-declaration-mismatch -Wno-implicit-function-declaration
gfortran -g -shared -fPIC -o bhmie-f/bhmie.so bhmie-f/bhmie.f
gfortran -g -shared -fPIC -o bhmie-f/bhmie_f77.so bhmie-f/bhmie_f77.f

View File

@@ -1,9 +1,13 @@
using Test
using Random
using PropCheck
using Debugger
using PyCall
include("../anchors.jl")
import .Anchors: TEST_DIR, SRC_DIR
import .Anchors: TEST_DIR, SRC_DIR, ROOT_DIR
if !@isdefined TestUtils
include(joinpath(TEST_DIR, "testutils.jl"))
@@ -15,85 +19,129 @@ if !@isdefined FFIWraps
include(joinpath(TEST_DIR, "ffi_wraps.jl"))
end
# function julia_vs_c(args::Tuple{Float64, Float64, Float64, UInt32, Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}})
# x, cxref_re, cxref_im, nang, cxs1_re, cxs1_im, cxs2_re, cxs2_im = args
function julia_vs_c(x, cxref_re, cxref_im, nang, cxs1_re, cxs1_im, cxs2_re, cxs2_im)
cxref, cxs1, cxs2 = ComplexF64(cxref_re, cxref_im), ComplexF64.(cxs1_re, cxs1_im), ComplexF64.(cxs2_re, cxs2_im)
x_c, cxref_c, nang_c, cxs1_c, cxs2_c = Float32(x), ComplexF32(cxref), UInt32(nang), ComplexF32.(cxs1), ComplexF32.(cxs2)
return isapprox(
miemfp.bhmie(x, cxref, nang),
FFIWraps.bhmie_c(x_c, cxref_c, nang_c, cxs1_c, cxs2_c),
rtol=0.1,
)
# Set up the Python environment
run(`$ROOT_DIR/setup_venv.sh`)
ENV["PYTHON"] = joinpath(ROOT_DIR, ".venv/bin/python")
@pyinclude(joinpath(TEST_DIR, "miemfp_tests.py"))
miemfp.bhmie(
x::Float64,
cxref::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
) = miemfp.bhmie(x, cxref, UInt32(nang))
function miemfp.bhmie(
x::Float64,
cxref::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
event::PyObject,
)
event.clear()
result = miemfp.bhmie(x, cxref, nang, s1, s2)
event.set()
return result
end
# function julia_vs_fortran(args::Tuple{Float64, Float64, Float64, UInt32, Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}})
# x, cxref_re, cxref_im, nang, cxs1_re, cxs1_im, cxs2_re, cxs2_im = args
function julia_vs_fortran(x, cxref_re, cxref_im, nang, cxs1_re, cxs1_im, cxs2_re, cxs2_im)
cxref, cxs1, cxs2 = ComplexF64(cxref_re, cxref_im), ComplexF64.(cxs1_re, cxs1_im), ComplexF64.(cxs2_re, cxs2_im)
x_f, cxref_f, nang_f, cxs1_f, cxs2_f = Float32(x), ComplexF32(cxref), Int32(nang), ComplexF32.(cxs1), ComplexF32.(cxs2)
b = miemfp.bhmie(x, cxref, nang)
f = FFIWraps.bhmie_fortran(x_f, cxref_f, nang_f, cxs1_f, cxs2_f)
# open("bhmie_julia_vs_fortran.txt", "a") do io
# println(io, "julia: ", b)
# println(io, "fortran: ", f)
# end
# return is_approx(b, f)
return isapprox(
miemfp.bhmie(x, cxref, nang),
FFIWraps.bhmie_fortran(x_f, cxref_f, nang_f, cxs1_f, cxs2_f),
rtol=0.1,
)
end
# function julia_vs_fortran77(args::Tuple{Float64, Float64, Float64, UInt32, Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}})
# x, cxref_re, cxref_im, nang, cxs1_re, cxs1_im, cxs2_re, cxs2_im = args
function julia_vs_fortran77(x, cxref_re, cxref_im, nang, cxs1_re, cxs1_im, cxs2_re, cxs2_im)
cxref, cxs1, cxs2 = ComplexF64(cxref_re, cxref_im), ComplexF64.(cxs1_re, cxs1_im), ComplexF64.(cxs2_re, cxs2_im)
x_f, cxref_f, nang_f, cxs1_f, cxs2_f = Float32(x), ComplexF32(cxref), Int32(nang), ComplexF32.(cxs1), ComplexF32.(cxs2)
return isapprox(
miemfp.bhmie(x, cxref, nang),
FFIWraps.bhmie_fortran77(x_f, cxref_f, nang_f, cxs1_f, cxs2_f),
rtol=0.1,
)
end
f64_gen = PropCheck.itype(Float64)
UInt32_gen = PropCheck.itype(UInt32)
f64_vec_gen = PropCheck.vector(isample(1:100), f64_gen)
bhmie_gen = PropCheck.interleave(
f64_gen,
f64_gen,
f64_gen,
UInt32_gen,
f64_vec_gen,
f64_vec_gen,
f64_vec_gen,
f64_vec_gen,
FFIWraps.bhmie_fortran(
x::Float64,
refrel::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
) = FFIWraps.bhmie_fortran(
Float32(x),
ComplexF32(refrel),
Int32(nang),
ComplexF32.(s1),
ComplexF32.(s2),
)
@testset "bhmie" begin
function FFIWraps.bhmie_fortran(
x::Float64,
refrel::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
event::PyObject,
)
event.clear()
result = FFIWraps.bhmie_fortran(x, refrel, nang, s1, s2)
event.set()
return result
end
FFIWraps.bhmie_fortran77(
x::Float64,
refrel::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
) = FFIWraps.bhmie_fortran77(
Float32(x),
ComplexF32(refrel),
Int32(nang),
ComplexF32.(s1),
ComplexF32.(s2),
)
function FFIWraps.bhmie_fortran77(
x::Float64,
refrel::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
event::PyObject,
)
event.clear()
result = FFIWraps.bhmie_fortran77(x, refrel, nang, s1, s2)
event.set()
return result
end
FFIWraps.bhmie_c(
x::Float64,
refrel::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
) = FFIWraps.bhmie_c(
Float32(x),
ComplexF32(refrel),
UInt32(nang),
ComplexF32.(s1),
ComplexF32.(s2),
)
function FFIWraps.bhmie_c(
x::Float64,
refrel::ComplexF64,
nang::Int64,
s1::Vector{ComplexF64},
s2::Vector{ComplexF64},
event::PyObject,
)
event.clear()
result = FFIWraps.bhmie_c(x, refrel, nang, s1, s2)
event.set()
return result
end
@testset "miemfp" begin
@testset "miemfp.bhmie" begin
c_check = PropCheck.check(julia_vs_c, bhmie_gen)
c_result = c_check == true
if !c_result
println("Fail vs C, PropCheck:")
display(c_check)
end
@test c_result
f_check = PropCheck.check(julia_vs_fortran, bhmie_gen)
f_result = f_check == true
if !f_result
println("Fail vs Fortran, PropCheck:")
display(f_check)
end
@test f_result
f77_check = PropCheck.check(julia_vs_fortran77, bhmie_gen)
f77_result = f77_check == true
if !f77_result
println("Fail vs Fortran77, PropCheck:")
display(f77_check)
end
@test f77_result
event1, event2 = py"asyncio.Event"(), py"asyncio.Event"()
event1.set(), event2.set()
result, output = py"compare_bhmie_functions"(miemfp.bhmie, FFIWraps.bhmie_fortran, event1, event2)
@test result
result, output = py"compare_bhmie_functions"(miemfp.bhmie, FFIWraps.bhmie_fortran77, event1, event2)
@test result
result, output = py"compare_bhmie_functions"(miemfp.bhmie, FFIWraps.bhmie_c, event1, event2)
@test result
end
end

49
test/miemfp_tests.py Normal file
View File

@@ -0,0 +1,49 @@
from typing import List, Tuple
import asyncio
import numpy as np
from hypothesis import errors, given, settings, strategies as st # type: ignore
def compare_bhmie_functions(f1, f2, event1: asyncio.Event, event2: asyncio.Event) -> Tuple[bool, str]:
async def async_closure(
x: float,
cxref: Tuple[float, float],
cxs1: List[Tuple[float, float]],
cxs2: List[Tuple[float, float]],
) -> bool:
cxref = complex(*cxref)
cxs1 = [complex(*c) for c in cxs1]
cxs2 = [complex(*c) for c in cxs2]
# This is to ensure that only one instance of each function is running at a time
# to avoid memory issues in the FFI code
await event1.wait()
f1_result = f1(x, cxref, 2, cxs1, cxs2)[:2]
await event2.wait()
f2_result = f2(x, cxref, 2, cxs1, cxs2)[:2]
return np.all(np.isclose(f1_result, f2_result))
@settings(deadline=None)
@given(
# Must be bigger than an atom but still nanoscale
x=st.floats(min_value=0.1, max_value=100),
# Refractive indeces must be within a physically reasonable range
cxref=st.tuples(st.floats(min_value=0.1, max_value=4.0), st.floats(min_value=0.1, max_value=4.0)),
cxs1=st.lists(st.tuples(st.floats(min_value=0.1, allow_infinity=False), st.floats(min_value=0.1, allow_infinity=False)), min_size=10, max_size=100),
cxs2=st.lists(st.tuples(st.floats(min_value=0.1, allow_infinity=False), st.floats(min_value=0.1, allow_infinity=False)), min_size=10, max_size=100),
)
def sync_closure(
x: float,
cxref: Tuple[float, float],
cxs1: List[Tuple[float, float]],
cxs2: List[Tuple[float, float]],
) -> bool:
assert asyncio.run(async_closure(x, cxref, cxs1, cxs2))
try:
sync_closure()
return True, "Test passed"
except AssertionError as e:
return False, f"AssertionError: {str(e)}"
except errors.HypothesisException as e:
return False, f"HypothesisException: {str(e)}"

View File

@@ -22,4 +22,9 @@ function test_from_serialized(fn::Function, filename::String)
@test deep_compare([fn(a...; kw...) for (a, kw) in argskwargs], out)
end
end # module TestUtils
function asymmetric_floatvec_to_complexvec(vec_a::Vector{Float32}, vec_b::Vector{Float32})
shortest = min(length(vec_a), length(vec_b))
ComplexF32.(vec_a[1:shortest], vec_b[1:shortest])
end
end