From 43a3d7a0981628226dca9fd6225ffbc608135087 Mon Sep 17 00:00:00 2001 From: Cian Hughes Date: Thu, 15 Feb 2024 11:41:35 +0000 Subject: [PATCH] Implemented property based testing via PyCall.jl and hypothesis.py --- .gitignore | 161 ++++++++++++++++++++++++++++++++++ Project.toml | 4 +- requirements.txt | 4 + setup_venv.sh | 5 ++ src/nanoconc.jl | 2 + test/Project.toml | 1 - test/benchmarks.jl | 4 +- test/build_ffi.sh | 6 +- test/miemfp_tests.jl | 200 +++++++++++++++++++++++++++---------------- test/miemfp_tests.py | 49 +++++++++++ test/testutils.jl | 7 +- 11 files changed, 359 insertions(+), 84 deletions(-) create mode 100644 requirements.txt create mode 100755 setup_venv.sh create mode 100644 test/miemfp_tests.py diff --git a/.gitignore b/.gitignore index 4ff3b13..dc7d376 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/Project.toml b/Project.toml index 38e9942..3e33293 100644 --- a/Project.toml +++ b/Project.toml @@ -1,15 +1,17 @@ name = "nanoconc" uuid = "9a947172-b1ea-4b16-84a6-f3d50752424d" authors = ["Cian Hughes "] -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" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4ff5c38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +attrs==23.2.0 +hypothesis==6.98.4 +numpy==1.26.4 +sortedcontainers==2.4.0 diff --git a/setup_venv.sh b/setup_venv.sh new file mode 100755 index 0000000..ada1526 --- /dev/null +++ b/setup_venv.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt \ No newline at end of file diff --git a/src/nanoconc.jl b/src/nanoconc.jl index 791f47d..088f6da 100755 --- a/src/nanoconc.jl +++ b/src/nanoconc.jl @@ -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) diff --git a/test/Project.toml b/test/Project.toml index 306ba5b..755c045 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -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" diff --git a/test/benchmarks.jl b/test/benchmarks.jl index 8f9c222..c816ef7 100644 --- a/test/benchmarks.jl +++ b/test/benchmarks.jl @@ -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") diff --git a/test/build_ffi.sh b/test/build_ffi.sh index 0060c69..895ee79 100755 --- a/test/build_ffi.sh +++ b/test/build_ffi.sh @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/test/miemfp_tests.jl b/test/miemfp_tests.jl index 040410f..c478faf 100644 --- a/test/miemfp_tests.jl +++ b/test/miemfp_tests.jl @@ -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 \ No newline at end of file diff --git a/test/miemfp_tests.py b/test/miemfp_tests.py new file mode 100644 index 0000000..8e5962b --- /dev/null +++ b/test/miemfp_tests.py @@ -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)}" \ No newline at end of file diff --git a/test/testutils.jl b/test/testutils.jl index a8181cc..b98c7ea 100644 --- a/test/testutils.jl +++ b/test/testutils.jl @@ -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 \ No newline at end of file +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 \ No newline at end of file