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

161
.gitignore vendored
View File

@@ -72,6 +72,167 @@ Manifest.toml
*.out
*.app
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Project specific gitignores
# Original notes from when developing
notes/*

View File

@@ -1,15 +1,17 @@
name = "nanoconc"
uuid = "9a947172-b1ea-4b16-84a6-f3d50752424d"
authors = ["Cian Hughes <chughes000@gmail.com>"]
version = "0.1.0"
version = "0.1.1"
[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Debugger = "31a5f54b-26ea-5ae9-a837-f05ce5417438"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
Memoize = "c03570c3-d221-55d1-a50c-7939bbd78826"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
attrs==23.2.0
hypothesis==6.98.4
numpy==1.26.4
sortedcontainers==2.4.0

5
setup_venv.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

View File

@@ -49,6 +49,7 @@ columns entitled "w","n" and "k" for wavelength in nm, n and k respectively)
function addmaterial(z::Float64, am::Float64, rho::Float64, res::Float64,
filepath::String, material::String, description::String;
disp::Bool=true)
flag = false
try
flag = h5open(matfile, "r") do file
has(file, material)
@@ -96,6 +97,7 @@ An alternative version of the addmaterial function for materials with known mie
function addmaterial(omp::Float64, om0::Float64, fv::Float64,
filepath::String, material::String, description::String;
disp::Bool=true)
flag = false
try
flag = h5open(matfile, "r") do file
has(file, material)

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