diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..997504b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.gitignore b/.gitignore index 6a74828..fbe326d 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,9 @@ x64 # test artifacts: /*.pdf -/*.py \ No newline at end of file +/*.py +/coverage + +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/LICENSE b/LICENSE index 1acd25c..e6cf52d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ NumDiff -- Numerical Differentiation Library https://github.com/jacobwilliams/NumDiff -Copyright (c) 2016-2022, Jacob Williams +Copyright (c) 2016-2026, Jacob Williams All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..b95d6c6 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Script to run tests with coverage and generate reports + +set -e + +COV_DIR="coverage" + +# Find the correct gcov (matching gfortran version) +GCOV=$(dirname $(which gfortran))/gcov + +echo "Using gcov: $GCOV" +echo "" +echo "================================================" +echo "Running tests with coverage..." +echo "================================================" + +# Clear the execution counts of any previous run (so they don't accumulate) +find build -name "*.gcda" -delete 2>/dev/null || true + +# Build and run tests with coverage flags +fpm test --flag "--coverage" + +echo "" +echo "================================================" +echo "Generating coverage report with lcov..." +echo "================================================" + +# Create a fresh coverage directory and copy the build artifacts into it. Only +# from the build just made: older coverage builds (with a different build-flag +# hash, e.g. after an fpm.toml change) hold stale data for the same files. +LATEST_GCNO=$(ls -t build/gfortran_*/*/*.gcno | head -1) +BUILD_DIR=$(dirname "$(dirname "$LATEST_GCNO")") +echo "Using build directory: $BUILD_DIR" +rm -rf "$COV_DIR" +mkdir -p "$COV_DIR" +cp -r "$BUILD_DIR"/*/* "$COV_DIR/" 2>/dev/null || true +# don't show coverage of test/examples programs: +rm -rf "$COV_DIR"/test_* +rm -rf "$COV_DIR"/example_* +# we want to skip the files that start with 'build_dependencies' because they are not part of the source code +find "$COV_DIR" -name "build_dependencies*" -exec rm -rf {} + + +# Capture initial coverage (baseline) +lcov --ignore-errors inconsistent --gcov-tool "$GCOV" --capture --initial --base-directory . --directory "$COV_DIR" --output-file "$COV_DIR/coverage.base" --quiet + +# Capture test execution coverage +lcov --ignore-errors inconsistent --gcov-tool "$GCOV" --capture --base-directory . --directory "$COV_DIR" --output-file "$COV_DIR/coverage.capture" --quiet + +# Combine baseline and test coverage +lcov --add-tracefile "$COV_DIR/coverage.base" --add-tracefile "$COV_DIR/coverage.capture" --output-file "$COV_DIR/coverage.info" --quiet + +echo "" +echo "================================================" +echo "Coverage Summary" +echo "================================================" + +# Display summary +lcov --summary "$COV_DIR/coverage.info" + +echo "" +echo "Coverage data saved to: $COV_DIR/coverage.info" +echo "" +echo "generate HTML report:" + +genhtml $COV_DIR/coverage.info --output-directory $COV_DIR/html --dark-mode + +echo "" diff --git a/fpm.toml b/fpm.toml index 92b1053..afbe149 100644 --- a/fpm.toml +++ b/fpm.toml @@ -1,8 +1,8 @@ name = "NumDiff" -version = "1.5.1" +version = "1.8.2" author = "Jacob Williams" maintainer = "Jacob Williams" -copyright = "Copyright (c) 2016-2022, Jacob Williams" +copyright = "Copyright (c) 2016-2026, Jacob Williams" license = "BSD-3" description = "Modern Fortran Numerical Differentiation Library" homepage = "https://github.com/jacobwilliams/NumDiff" @@ -14,24 +14,9 @@ source-dir = "src" library = true [build] -auto-executables = false -auto-examples = false -auto-tests = false +auto-executables = true +auto-examples = true +auto-tests = true [dev-dependencies] pyplot-fortran = { git="https://github.com/jacobwilliams/pyplot-fortran.git", tag = "3.2.1" } - -[[test]] -name = "dsm_test" -source-dir = "tests" -main = "dsm_test.f90" - -[[test]] -name = "test1" -source-dir = "tests" -main = "test1.f90" - -[[test]] -name = "test2" -source-dir = "tests" -main = "test2.f90" diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..c03685a --- /dev/null +++ b/pixi.lock @@ -0,0 +1,2229 @@ +version: 7 +platforms: +- name: osx-arm64 + virtual-packages: + - __unix=0=0 + - __osx=13.0 + - __archspec=0=m1 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-51.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt22_osx-arm64-22.1.8-hb8825d9_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-22.1.8-hce30654_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ford-7.0.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_osx-arm64-15.3.0-h647fcfe_107.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-include-0.7.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pcpp-1.30-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.12.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.3-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.20.3-pyh91182bf_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-markdown-math-0.8-pyhff2d567_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-12.6.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.10-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toposort-1.7-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.70.1-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-8_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-hf00d406_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-he4a93e4_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.6-h580cee5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools-1030.6.3-llvm22_1_hbe26303_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_impl_osx-arm64-1030.6.3-llvm22_1_he9db816_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1030.6.3-llvm22_1_hbe26303_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-22-22.1.8-default_ha685127_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-22.1.8-default_cfg_ha6dc760_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-22.1.8-default_h79071a4_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-22.1.8-hce30654_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt22-22.1.8-hdb3d66b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/conda-gcc-specs-15.3.0-h64e22ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.4.0-py314hb1acb19_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.18.3-h81aa574_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.66.1-py314haf53d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fpm-0.13.0-h00726df_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.17-hb001647_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gcc-15.3.0-h500d687_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gcc_impl_osx-arm64-15.3.0-hdd9f9fa_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.8-h5867e2c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gfortran-15.3.0-h500d687_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gfortran_impl_osx-arm64-15.3.0-hc97e033_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.90.0-h06d1d6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-h784d473_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-14.5.0-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.1-py314hb1acb19_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hce41798_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcov-2.5-h820172f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-956.6-llvm22_1_h5b97f1b_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-956.6-llvm22_1_hf30805f_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.2.0-h1eee2c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-11_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-h1dcdb26_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-h5295a6a_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-h2ddc9cb_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-11_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp22.1-22.1.8-default_h79071a4_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcompiler-rt-22.1.8-hdb3d66b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-23.1.2-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-he7e0567_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.5-h6111c0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-h2ed5691_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-16.2.0-h3cf6597_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-h05bcc79_12.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-16.2.0-h07b0088_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-16.2.0-hdb7a957_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.90.0-hccd281b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libharfbuzz-14.5.0-hcda0f7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libharfbuzz-devel-14.5.0-hcda0f7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-he4c29f2_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-hae62183_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-11_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm22-22.1.8-h759d1ac_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.34-openmp_h5e6e99c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-hf5e6511_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.14.7-h4311e70_106_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libraqm-0.11.0-h45af499_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.62.4-hc8ac265_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.4-hca69786_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-hf67920b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h202fb40_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hbffa61f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.4-heb56d2d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.4-h550178d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-23.1.2-hdb3d66b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22-22.1.8-h79441dc_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22.1.8-hdb3d66b_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-3.11.2-py314h314fc0d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.11.2-py314heb334b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.5.3-py314he06036b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-h4d1e80c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.4-h55eecbc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.58.2-hf9b25ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-he63d830_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-5.32.1-10_hb001647_perl5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-capture-tiny-0.48-pl5321hce30654_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-devel-stacktrace-2.04-pl5321ha040cf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.3.0-py314h709c99d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h784d473_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hb001647_1004.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.7-h0ae1c2c_106_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1600.0.11.8-hb561403_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.10-py314hcd3153d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hb001647_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hb001647_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-51.0-unix_0.conda + sha256: def01b148aa54d4de29b7d4bf58d941c8699582201db14cf78c6c89fa1666434 + md5: 6f62266fb2bfc35a5071cfa1d6ffdcc9 + depends: + - __unix + - hicolor-icon-theme + - librsvg + license: LGPL-3.0-or-later OR CC-BY-SA-3.0 + run_exports: {} + size: 687517 + timestamp: 1790100765464 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + sha256: aed4b9dcf68ec2a75e5645fed14d77fd884d38d2e52bfa6ef4b278d90cd88781 + md5: 3b261da3fe9b4168738712832410b022 + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + run_exports: {} + size: 92704 + timestamp: 1780853175566 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + sha256: 0a0544cf95f64394fe4959286f5c71f5444ad58feb0602e53becb27448d24da6 + md5: 0f51e2391ade309db462a55611263e9c + depends: + - __unix + license: ISC + run_exports: {} + size: 131780 + timestamp: 1784754889428 +- conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.2-pyhcf101f3_0.conda + sha256: 170b256f706d5b1d8948851e6950e9b76d237b1dc342a0a48d787f565b7919c4 + md5: 46b67ffe16f62762f3f641b18152be7d + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 96051 + timestamp: 1786454409759 +- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt22_osx-arm64-22.1.8-hb8825d9_2.conda + sha256: 016121f654e8052edf0e9912ab2f430c2d6ac93adc8cfd484318de6a656cd0c9 + md5: e603f604aa1e281db38ff849cc4e4c5d + constrains: + - compiler-rt >=9.0.1 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 10565200 + timestamp: 1787293602816 +- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-22.1.8-hce30654_2.conda + sha256: 35a0429b875ff8252b173e4c0a3b028f58ea837da56b04e052ffc2c68c253633 + md5: 79874270cd835b658d889f4cd054677b + depends: + - compiler-rt22_osx-arm64 22.1.8 hb8825d9_2 + constrains: + - clang 22.1.8 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 16768 + timestamp: 1787293624614 +- conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + sha256: bb47aec5338695ff8efbddbc669064a3b10fe34ad881fb8ad5d64fbfa6910ed1 + md5: 4c2a8fef270f6c69591889b93f9f55c1 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 14778 + timestamp: 1764466758386 +- conda: https://conda.anaconda.org/conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2 + sha256: 63a83e62e0939bc1ab32de4ec736f6403084198c4639638b354a352113809c92 + md5: a362b2124b06aad102e2ee4581acee7d + depends: + - python >=3.7 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 9870 + timestamp: 1628958582931 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b + md5: 0c96522c6bdaed4b1566d11387caaf45 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 397370 + timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c + md5: 34893075a5c9e55cdafac56607368fc6 + license: OFL-1.1 + license_family: Other + run_exports: {} + size: 96530 + timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 + md5: 4d59c254e01d9cde7957100457e2d5fb + license: OFL-1.1 + license_family: Other + run_exports: {} + size: 700814 + timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 + md5: 49023d73832ef61042f6a237cb2687e7 + license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 + license_family: Other + run_exports: {} + size: 1620504 + timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 + md5: fee5683a3f04bd15cbd8318b096a27ab + depends: + - fonts-conda-forge + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 3667 + timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 + md5: a7970cd949a077b7cb9696379d338681 + depends: + - font-ttf-ubuntu + - font-ttf-inconsolata + - font-ttf-dejavu-sans-mono + - font-ttf-source-code-pro + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 4059 + timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/noarch/ford-7.0.13-pyhd8ed1ab_0.conda + sha256: f3b278d593a3d513fc5d715fc441010b53a56dd32727947bc135cbe4d776b4f1 + md5: 29731dfbbf5799036e4dac3b6197cfd2 + depends: + - beautifulsoup4 >=4.5.1 + - jinja2 >=2.11 + - markdown >=3.4.0,<3.5 + - markdown-include >=0.7.0,<0.8 + - pcpp >=1.30 + - pygments >=2.12,<2.13 + - python >=3.8 + - python-graphviz >=0.20.0,<0.21 + - python-markdown-math >=0.8,<0.9 + - rich >=12.0.0,<13 + - tomli >=1.1.0 + - toposort >=1.7.0,<1.8 + - tqdm >=4.66.3 + license: GPL-3.0-or-later + license_family: GPL + run_exports: {} + size: 753376 + timestamp: 1770365114418 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.1-pyhcf101f3_0.conda + sha256: 95a074a7d3f58b3494c068d4789c364dcb591c56b70ca6b12f149a540f9aeea4 + md5: 77ec68e0aa61c3fff9ecbf840f93d64e + depends: + - python >=3.10 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 34920 + timestamp: 1787943971257 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_osx-arm64-15.3.0-h647fcfe_107.conda + sha256: 89859324c8f2f89eae9a478071862d0571c1769a35148c36b8195d8273f5a1b3 + md5: 13a4106eb56b34138f6b2f78950172a2 + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 2064871 + timestamp: 1790301920884 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.4.4-pyhd8ed1ab_0.conda + sha256: cf1b3778cf99bd0301c4eb4f5d3d575e8c4248c15a019189b3892131a2675854 + md5: 6b8ab17bc8ff1ca89b3ea28ea3d566ca + depends: + - importlib-metadata >=4.4 + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 71222 + timestamp: 1690307552438 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-include-0.7.2-pyhd8ed1ab_0.conda + sha256: 0e835d809551855c1aaed7c7d5b947e5bf1052dd1c07ca996434e9b24783dfd8 + md5: 88701c78d064c347f02e8cbc8b688415 + depends: + - markdown >=3.0 + - python >=3.7 + license: GPL-3.0-or-later + license_family: GPL + run_exports: {} + size: 33134 + timestamp: 1669223442248 +- conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + sha256: d09c47c2cf456de5c09fa66d2c3c5035aa1fa228a1983a433c47b876aa16ce90 + md5: 37293a85a0f4f77bbd9cf7aaefc62609 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: Apache + run_exports: {} + size: 15851 + timestamp: 1749895533014 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda + sha256: c432626b16768b8dab228bfb706f7060c2d462a21c516d240f68f2f902b5a044 + md5: 936687ed80f295a1f5dbcf8bd34c252c + depends: + - python >=3.9 + - python + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 116363 + timestamp: 1785888127370 +- conda: https://conda.anaconda.org/conda-forge/noarch/pcpp-1.30-pyhd8ed1ab_0.tar.bz2 + sha256: 49e41764d0a2b48d1284bf971c02cf7313fbb927902d715d72a711ee50c4edd8 + md5: 4c49654ed25bc0bcd5bb0c7ed00a885a + depends: + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 72323 + timestamp: 1635522437316 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.12.0-pyhd8ed1ab_0.tar.bz2 + sha256: 682de96d5c7be7a6223ea2a26f71d45cf4864a84b185b1c2e6d42975fa53d024 + md5: cb27e2ded147e5bcc7eafc1c6d343cb3 + depends: + - python >=3.6 + - setuptools + license: BSD-2-Clause + license_family: BSD + run_exports: {} + size: 836908 + timestamp: 1650904579146 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.3-pyh5ded981_0.conda + sha256: dc10d64d4b9a1310083b2f563416ab45560ee2a04fd6baab40f223f2b1ab37bc + md5: 8bf43ae1312e9b966063459e04c20fa7 + depends: + - python >=3.11 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 114046 + timestamp: 1790063282695 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.20.3-pyh91182bf_2.conda + sha256: c8f5d3d23b5962524217f33549add8d6c5af22fe839b49603f4588771154a51c + md5: f822f0e13849c2283f72ec4aa120eeaa + depends: + - graphviz >=2.46.1 + - python >=3.9 + license: MIT + license_family: MIT + run_exports: {} + size: 38220 + timestamp: 1733792086212 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-markdown-math-0.8-pyhff2d567_1.conda + sha256: e708431e3c609ae66fce812cad16ca7396566ef502ad65fb2a9d34feb79d697e + md5: 7d1823207591ae620832b038918a9318 + depends: + - markdown >=3.0 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 11571 + timestamp: 1736274958975 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-9_cp314.conda + build_number: 9 + sha256: e7f8e965bbfb98e08feb2365cacdad8bb218fa5b5a0a2752f747287f425f213c + md5: 350d89b50d7de805abf2e63e29dee451 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 6791 + timestamp: 1788302824440 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-12.6.0-pyhd8ed1ab_0.tar.bz2 + sha256: 0cdb3105d3e50de8b6d9f4aea7e228b4872fb8b066daeb1e51c9519bc92ceffd + md5: 162ed4cc104133be23e592faa1e055e3 + depends: + - commonmark >=0.9.0,<0.10.0 + - dataclasses >=0.7,<0.9 + - pygments >=2.6.0,<3.0.0 + - python >=3.6.2 + - typing_extensions >=4.0.0,<5.0.0 + license: MIT + license_family: MIT + run_exports: {} + size: 174557 + timestamp: 1664752667299 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda + sha256: 9e200ee5f9ff19a4d94e4b51c4856d53dec849f91032f345cf0c6bc3d51a7183 + md5: 62ac906f1cd582c6c264c95625cb9d6f + depends: + - python >=3.10 + license: MIT + license_family: MIT + run_exports: {} + size: 524488 + timestamp: 1786282924579 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.10-pyhd8ed1ab_0.conda + sha256: c0621a3f324de3eae52092416659674aa9af3617154380cb2a1a7ad7ab487f29 + md5: 6c2382c11f0c0486f3300a761e5c1205 + depends: + - python >=3.11 + license: MIT + license_family: MIT + run_exports: {} + size: 39948 + timestamp: 1790238121393 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd + md5: b5325cf06a000c5b14970462ff5e4d58 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 21561 + timestamp: 1774492402955 +- conda: https://conda.anaconda.org/conda-forge/noarch/toposort-1.7-pyhd8ed1ab_0.tar.bz2 + sha256: b605b620c3a926da994bfcf47c9343daad59553fbafd29bbf2cce6ead86f26cd + md5: ce711f7c322298edf8e7bd592a06b4b7 + depends: + - python >=3 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 12565 + timestamp: 1632856142463 +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.70.1-pyhfa0c392_0.conda + sha256: 7c40c84de3de3624c2a753496122dbb880903c796d183e9d4be6c15c310c4bab + md5: ffc91f29264eeb8d3ba96fdc897316c3 + depends: + - python >=3.11 + - __unix + - python + constrains: + - envwrap >=0.2 + - ipywidgets >=6.0 + license: MPL-2.0 and MIT + run_exports: {} + size: 95977 + timestamp: 1789187326570 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + sha256: b141933ece3518f6d7b75dfb59451e2f26b405a44c18e2518a83e9a02e09315c + md5: c680b5747e8c4c8f23dca0bb7042a8fc + depends: + - typing_extensions ==4.16.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 94080 + timestamp: 1783002732887 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + sha256: 2d888f90af0686044882c74193ec80a90ec1943145d94a7b1b048958acda1848 + md5: c70ad746c22219b9700931707482992c + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 52631 + timestamp: 1783002732887 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + sha256: b928c30ddcb0e3f544c6eade8352737e6e610e263276b90232db6a578ef899d8 + md5: fcb489df604d100968b737f2cb6076c6 + license: LicenseRef-Public-Domain + run_exports: {} + size: 118849 + timestamp: 1784250406640 +- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda + sha256: 6ed46549d156bc109803f43fd0c336bc4604aabdeee3de2a6e8ce3eff25b1a60 + md5: fa127b9f29f64174437524d0d61432f4 + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + run_exports: {} + size: 133826 + timestamp: 1760304475989 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda + sha256: 210bd31c22bb88f5e2a167df24c95bb5f152b2ada7502f9b8c49d1f5366db423 + md5: ba3dcdc8584155c97c648ae9c044b7a3 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 24190 + timestamp: 1779159948016 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-8_kmp_llvm.conda + build_number: 8 + sha256: c7c98ce74f6a7ff25aaa4706d06fa41d63a333add4d697b73aa4d8d2d7ad7651 + md5: a2d706b4a7d603524133037c34f7fb76 + depends: + - llvm-openmp >=9.0.1 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - _openmp_mutex >=4.5 + size: 8016 + timestamp: 1788046437162 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + sha256: b0747f9b1bc03d1932b4d8c586f39a35ac97e7e72fe6e63f2b2a2472d466f3c1 + md5: 57301986d02d30d6805fdce6c99074ee + depends: + - __osx >=11.0 + - libcxx >=16 + - libglib >=2.80.0,<3.0a0 + - libintl >=0.22.5,<1.0a0 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + run_exports: + weak: + - atk-1.0 >=2.38.0 + size: 347530 + timestamp: 1713896411580 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-hf00d406_4.conda + sha256: 14d8592fefabdc35055b857925fd47b375cb202a15a2914e94dfbbed4fd82c64 + md5: e32ad50b0f977e3fa616a578a2643e46 + depends: + - __osx >=11.0 + - brotli-bin 1.2.0 he4a93e4_4 + - libbrotlidec 1.2.0 h5295a6a_4 + - libbrotlienc 1.2.0 h2ddc9cb_4 + license: MIT + license_family: MIT + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 20609 + timestamp: 1788480374173 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-he4a93e4_4.conda + sha256: f95bc05d7733d1c78a2077e83c5a05235f8b743b6e2e977c688cdba4d56c4537 + md5: d065d0732d02ace747059c3d58338fcd + depends: + - __osx >=11.0 + - libbrotlidec 1.2.0 h5295a6a_4 + - libbrotlienc 1.2.0 h2ddc9cb_4 + license: MIT + license_family: MIT + run_exports: {} + size: 18849 + timestamp: 1788480367234 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda + sha256: 8ec22f0ba25cbfc2e64d70cf29459eccd7ffdf6436f6a6ff15bbfef799f7d4f6 + md5: b50612e7d190b8061ab4e7dc119cf4d5 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 + size: 124965 + timestamp: 1785906749812 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.6-h580cee5_0.conda + sha256: 32ad9b731d0b4c6dd82061201206164b07bd695bc695e5eb3addc0087ed54cca + md5: 0d25672f63cd1c9b08653a0becfbf31a + depends: + - __osx >=11.0 + - fontconfig >=2.18.3,<3.0a0 + - fonts-conda-ecosystem + - icu >=78.3,<79.0a0 + - libcxx >=21 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.90.0,<3.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - pixman >=0.46.4,<1.0a0 + license: LGPL-2.1-only or MPL-1.1 + run_exports: + weak: + - cairo >=1.18.6,<2.0a0 + size: 844614 + timestamp: 1790045326889 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools-1030.6.3-llvm22_1_hbe26303_6.conda + sha256: ccd36657e3827f31c3178cdcd5aeb8bfbcf88c88f7d95cc45d57e15dfbda5b2b + md5: 233521d0cfed78f0f6948a43f87c43f9 + depends: + - cctools_impl_osx-arm64 1030.6.3 llvm22_1_he9db816_6 + - ld64 956.6 llvm22_1_h5b97f1b_6 + - libllvm22 >=22.1.8,<22.2.0a0 + license: APSL-2.0 + license_family: Other + run_exports: {} + size: 24212 + timestamp: 1787724419398 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_impl_osx-arm64-1030.6.3-llvm22_1_he9db816_6.conda + sha256: 64574fe7f951de7b5db1e2b8e060a491c9db3fedcc7f7be7a23358bee87a80b3 + md5: 5f6faaf53b8a59edaf218e779d1284de + depends: + - __osx >=11.0 + - ld64_osx-arm64 >=956.6,<956.7.0a0 + - libcxx + - libllvm22 >=22.1.8,<22.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - llvm-tools 22.1.* + - sigtool-codesign + constrains: + - cctools 1030.6.3.* + - clang 22.1.* + - ld64 956.6.* + license: APSL-2.0 + license_family: Other + run_exports: {} + size: 762321 + timestamp: 1787724408507 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1030.6.3-llvm22_1_hbe26303_6.conda + sha256: da1d1186c6d1bb4d899743c512a1bf103058e2f3980313d9c02da998a149d4df + md5: 0a9f3301e65cecb6a588df083a06bbea + depends: + - cctools_impl_osx-arm64 1030.6.3 llvm22_1_he9db816_6 + - ld64_osx-arm64 956.6 llvm22_1_hf30805f_6 + constrains: + - cctools 1030.6.3.* + license: APSL-2.0 + license_family: Other + run_exports: {} + size: 23488 + timestamp: 1787724422134 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-22-22.1.8-default_ha685127_10.conda + sha256: 952dd631907d7077090627c5a57beb81e0de12064a4603abc05e4afcdaaa8762 + md5: 30337bf4105b14645720fb1d06cc305f + depends: + - compiler-rt22 ==22.1.8 + - libclang-cpp22.1 ==22.1.8 default_h79071a4_10 + - libcxx >=22.1.8 + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.15.3 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libclang-cpp22.1 >=22.1.8,<22.2.0a0 + - libllvm22 >=22.1.8,<22.2.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 925060 + timestamp: 1788043670114 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-22.1.8-default_cfg_ha6dc760_10.conda + sha256: 41e2ef2929fa7bebf11575dac67719a4cc43cafc62a6a2ed67ccaa36e15cfd69 + md5: 4bbb0b7c7caab7dc134f01a414994b81 + depends: + - clang-22 ==22.1.8 default_* + - llvm-openmp >=22.1.8 + - clang_impl_osx-arm64 ==22.1.8 default_h79071a4_10 + - cctools + - ld64 + - ld64_osx-arm64 * llvm22_1_* + - llvm-tools ==22.1.8 + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 32397 + timestamp: 1788043670114 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-22.1.8-default_h79071a4_10.conda + sha256: c42809c90ba02e42254b469ef53c2f84463f4d6771add2929a97f1848922d394 + md5: 80c46de2bed15da2a7c50e205ad72077 + depends: + - clang-22 ==22.1.8 default_* + - compiler-rt_osx-arm64 ==22.1.8 + - compiler-rt ==22.1.8 + - cctools_impl_osx-arm64 + - ld64_osx-arm64 * llvm22_1_* + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.15.3 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 31926 + timestamp: 1788043670114 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-22.1.8-hce30654_2.conda + sha256: 28c01956aefbe38a8bddde58bb2398789fd580ffb341a12c54f44a542fa1a358 + md5: fa14e650717aac340bcc3251bab8117b + depends: + - compiler-rt22 22.1.8 hdb3d66b_2 + - libcompiler-rt 22.1.8 hdb3d66b_2 + constrains: + - clang 22.1.8 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 16634 + timestamp: 1787293624982 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt22-22.1.8-hdb3d66b_2.conda + sha256: cfa490c27082b9aca7a087548c2608daf5af2644f0eb8345fba7d4baa45fd03d + md5: da9362e9de3e1f80c9786a268ff22bfa + depends: + - __osx >=11.0 + - compiler-rt22_osx-arm64 22.1.8.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: {} + size: 99807 + timestamp: 1787293624048 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/conda-gcc-specs-15.3.0-h64e22ab_7.conda + sha256: f1a3ea87b09cc2965d67d874de5c3580246080659a371317506476b0d07da865 + md5: b63660060bb4f4f01541ed4a60d4e74a + depends: + - gcc_impl_osx-arm64 >=15.3.0,<15.3.1.0a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 103706 + timestamp: 1790302016076 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.4.0-py314hb1acb19_1.conda + sha256: 74455ad2fd9adc46a43c75cea7ca2c800588da887850ca3205e136de2cec03e4 + md5: 4dbacdb6f1f3dc54d63032a2fb8a9a4e + depends: + - numpy >=2.0 + - python + - __osx >=11.0 + - libcxx >=21 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 293117 + timestamp: 1789746849397 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + sha256: ba685b87529c95a4bf9de140a33d703d57dc46b036e9586ed26890de65c1c0d5 + md5: 3b87dabebe54c6d66a07b97b53ac5874 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - epoxy >=1.5.10,<1.6.0a0 + size: 296347 + timestamp: 1758743805063 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.18.3-h81aa574_1.conda + sha256: 004570d35fb0eff73ce3ae49b209a47622d0c2e580dd0dc4c61d59626b386a09 + md5: 8ac8cc3fe744b484de4387703134d8b2 + depends: + - __osx >=11.0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libintl >=0.25.1,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - fontconfig >=2.18.3,<3.0a0 + - fonts-conda-ecosystem + size: 265659 + timestamp: 1786667932256 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.66.1-py314haf53d71_0.conda + sha256: 846243cebfc233baa9a3bbde6dd90d55d2f9c47d5d2a34bf8a3a1924ec49b00c + md5: 9b3808df79953fa8114ccd3301e712d2 + depends: + - __osx >=11.0 + - brotli + - munkres + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + run_exports: {} + size: 3105434 + timestamp: 1790702856107 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fpm-0.13.0-h00726df_0.conda + sha256: 3c36a16901ac65852ea6a9c03077d14de3e0c27fab8e540ee637736a07506e08 + md5: d8af5a4b1a791f2902122220b167cb08 + depends: + - __osx >=11.0 + - libgfortran + - libgfortran5 >=14.3.0 + - llvm-openmp >=19.1.7 + license: MIT + license_family: MIT + run_exports: {} + size: 1126641 + timestamp: 1771426377724 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_2.conda + sha256: 90681f1d0658cb2fd49a074f644eb4774272499290487a4bebb1c4df01d6997b + md5: 2f4cc7dc2e6622591735c49dda1b92aa + depends: + - libfreetype 2.14.3 hce30654_2 + - libfreetype6 2.14.3 h2ed5691_2 + license: GPL-2.0-only OR FTL + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 175388 + timestamp: 1786641016583 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.17-hb001647_0.conda + sha256: 803e78ec4a832a646dae842f09de0bc04acd2613deafa9b11d1970399d24d163 + md5: 29b021cee334af0050bdca46bada6e0e + depends: + - __osx >=11.0 + license: LGPL-2.1-or-later + run_exports: + weak: + - fribidi >=1.0.17,<2.0a0 + size: 63577 + timestamp: 1790011838821 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gcc-15.3.0-h500d687_7.conda + sha256: f7fff997d8a198ef274d8bae96f5d6d6b7b493edea24753e2d057dbaf611d8ec + md5: 0c68aaee12368c7ea0fe3d7c8ec22121 + depends: + - conda-gcc-specs + - gcc_impl_osx-arm64 15.3.0 hdd9f9fa_7 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 100726 + timestamp: 1790302097564 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gcc_impl_osx-arm64-15.3.0-hdd9f9fa_7.conda + sha256: 49f180e56b14bc5fa77b754a8a5055a90f98c00ed11b718930761a789654d5ec + md5: 0a59d7ef1ab966e82dee4f3608ead2b1 + depends: + - cctools_osx-arm64 + - clang + - ld64_osx-arm64 + - libgcc >=15.3.0 + - libgcc-devel_osx-arm64 15.3.0 h647fcfe_107 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 49731022 + timestamp: 1790301960206 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.8-h5867e2c_2.conda + sha256: 96dcac22eaea2f24b9c92e8be078c3e39058829aa4db2a8103c1a4e85f8925ed + md5: 3a84786e0c929f4a05d08c07be5ec9b3 + depends: + - __osx >=11.0 + - libglib >=2.88.3,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.2,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + run_exports: + weak: + - gdk-pixbuf >=2.44.8,<3.0a0 + size: 552802 + timestamp: 1788490405006 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gfortran-15.3.0-h500d687_7.conda + sha256: 08bcdc2786da0471158487e351bb84c15a1105ba092456da2f9a8984eb35b2d8 + md5: 494f0201401df70c6b570ddb8e2ab923 + depends: + - conda-gcc-specs + - gcc 15.3.0 h500d687_7 + - gcc_impl_osx-arm64 15.3.0 hdd9f9fa_7 + - gfortran_impl_osx-arm64 15.3.0 hc97e033_7 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 100322 + timestamp: 1790302115518 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gfortran_impl_osx-arm64-15.3.0-hc97e033_7.conda + sha256: 69ae6942ff649a748b7c58f5faa7d30e344afd34bec36fe954d83232afaee08b + md5: 1a31dbbcff4880651b42a5e750b46939 + depends: + - gcc_impl_osx-arm64 15.3.0.* + - libgcc >=15.3.0 + - libgfortran5 >=15.3.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 13101317 + timestamp: 1790302052881 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.90.0-h06d1d6c_0.conda + sha256: 4c0cd7642611222bc573e93ac761b811839b015e4d0d968ae2e880b944e8b1a9 + md5: 62aed33caa374a6807ba9b2780603b70 + depends: + - libglib ==2.90.0 hccd281b_0 + - libffi + - __osx >=11.0 + - libintl >=0.25.1,<1.0a0 + license: LGPL-2.1-or-later + run_exports: {} + size: 202352 + timestamp: 1789478088421 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-h784d473_1.conda + sha256: 471f34a187fdb4f2df33e26f2e471b16c239a5b277903f643d9c3a8c9a9f44ec + md5: 0c7b78d9ffff4f5a6ca28d346f734d8f + depends: + - libcxx >=19 + - __osx >=11.0 + license: LGPL-2.0-or-later + license_family: LGPL + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 + size: 86493 + timestamp: 1786118637573 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda + sha256: 755c72d469330265f80a615912a3b522aef6f26cbc52763862b6a3c492fbf97c + md5: 1f3d859de3ca2bcaa845e92e87d73660 + depends: + - __osx >=11.0 + - adwaita-icon-theme + - cairo >=1.18.4,<2.0a0 + - fonts-conda-ecosystem + - gdk-pixbuf >=2.44.4,<3.0a0 + - gtk3 >=3.24.43,<4.0a0 + - gts >=0.7.6,<0.8.0a0 + - libcxx >=19 + - libexpat >=2.7.3,<3.0a0 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.86.3,<3.0a0 + - librsvg >=2.60.0,<3.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: EPL-1.0 + license_family: Other + run_exports: + weak: + - graphviz >=14.1.2,<15.0a0 + size: 2218284 + timestamp: 1769427599940 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda + sha256: 26862a9898054b8552e55e609e5ce73c7ef1eb28bbe6fb87f0b9109d73cd09df + md5: 5557a2433b1339b8e536c264afea41ef + depends: + - __osx >=11.0 + - atk-1.0 >=2.38.0 + - cairo >=1.18.4,<2.0a0 + - epoxy >=1.5.10,<1.6.0a0 + - fribidi >=1.0.16,<2.0a0 + - gdk-pixbuf >=2.44.5,<3.0a0 + - glib-tools + - harfbuzz >=13.2.1 + - hicolor-icon-theme + - libexpat >=2.7.4,<3.0a0 + - libfreetype >=2.14.2 + - libfreetype6 >=2.14.2 + - libglib >=2.86.4,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - liblzma >=5.8.2,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + run_exports: + weak: + - gtk3 >=3.24.52,<4.0a0 + - adwaita-icon-theme + size: 9385734 + timestamp: 1774288504338 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + sha256: e0f8c7bc1b9ea62ded78ffa848e37771eeaaaf55b3146580513c7266862043ba + md5: 21b4dd3098f63a74cf2aa9159cbef57d + depends: + - libcxx >=15.0.7 + - libglib >=2.76.3,<3.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + run_exports: + weak: + - gts >=0.7.6,<0.8.0a0 + size: 304331 + timestamp: 1686545503242 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-14.5.0-hce30654_0.conda + sha256: 51db7a95894bb507e420c97478096fa678eb8fcc01e61d48682fee15b4b473eb + md5: 3da3f1f60c9990f662e0b50d46864005 + depends: + - libharfbuzz-devel 14.5.0 hcda0f7c_0 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.5.0 + size: 11045 + timestamp: 1790061688272 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda + sha256: 46a4958f2f916c5938f2a6dc0709f78b175ece42f601d79a04e0276d55d25d07 + md5: cfb39109ac5fa8601eb595d66d5bf156 + license: GPL-2.0-or-later + license_family: GPL + run_exports: {} + size: 17616 + timestamp: 1771539622983 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda + sha256: 6cdb5dee54c72e56ab189fb3ad33cb28533553d42590e7e831160248f4416a43 + md5: a5efc0b42bb8b42e97d0a29ae3e3c187 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - icu >=78.3,<79.0a0 + size: 14070242 + timestamp: 1786545847761 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.1-py314hb1acb19_3.conda + sha256: a5f05b746fb71f9f2b767ebde44cf7b724b2550ff9649f11034acf4d82ba88a5 + md5: d0023eb610e4e46808270ee5a8a464c1 + depends: + - python + - libcxx >=21 + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 63984 + timestamp: 1790082005434 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hce41798_3.conda + sha256: 5f9b511f918e13dfddb8495c1764aec894b66c5a88359737aa302b8899ae25fa + md5: 4eed1ee02ef1992c046f2e81d5606633 + depends: + - __osx >=11.0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - libtiff >=4.7.2,<4.8.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - lcms2 >=2.19.1,<3.0a0 + size: 212689 + timestamp: 1789052560432 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcov-2.5-h820172f_0.conda + sha256: 08318c761074701ff6e7a26d7ac47a0b97ebc217d10c3735a0acdfdb6adb3a12 + md5: 56edf799c55b9d6f06788c353932b26f + depends: + - perl 5.* + - python + - xlsxwriter + - perl-capture-tiny + - perl-devel-stacktrace + license: GPL-2.0-or-later + license_family: GPL + run_exports: {} + size: 3107132 + timestamp: 1787643693949 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-956.6-llvm22_1_h5b97f1b_6.conda + sha256: 268748b65984e6c7429ee5f00bdf4c48865813d7c111efb0c1ee45dec69bddf3 + md5: 6a9ec7c91de697f07cfd26df1487820b + depends: + - ld64_osx-arm64 956.6 llvm22_1_hf30805f_6 + - libllvm22 >=22.1.8,<22.2.0a0 + constrains: + - cctools 1030.6.3.* + - cctools_osx-arm64 1030.6.3.* + license: APSL-2.0 + license_family: Other + run_exports: {} + size: 21703 + timestamp: 1787724414240 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-956.6-llvm22_1_hf30805f_6.conda + sha256: 875cf24cd8cdc49f47ca3ebd13116d20cd82dc4d3a016b970d638dee714e1cfb + md5: f30fdc593ae66f0319087d778c613106 + depends: + - __osx >=11.0 + - libcxx + - libllvm22 >=22.1.8,<22.2.0a0 + - sigtool-codesign + - tapi >=1600.0.11.8,<1601.0a0 + constrains: + - cctools 1030.6.3.* + - cctools_impl_osx-arm64 1030.6.3.* + - clang 22.1.* + - ld64 956.6.* + license: APSL-2.0 + license_family: Other + run_exports: {} + size: 980553 + timestamp: 1787724392778 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.2.0-h1eee2c3_0.conda + sha256: c97aa17d16d2ac332ba9f184e82ce8f72dcb10e9a10c5f299030be2d44e191b9 + md5: e429aec4037d5cb8fd34ded9f5dadd39 + depends: + - __osx >=11.0 + - libcxx >=19 + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - lerc >=4.2.0,<5.0a0 + size: 166477 + timestamp: 1785036480092 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-11_h51639a9_openblas.conda + build_number: 11 + sha256: 718c3e1a16ad946caeeba041520dc18b7af461226bd8c28faef78b181920dbb4 + md5: 364639b771dbb4ede335d965f55a23ac + depends: + - libopenblas >=0.3.34,<0.3.35.0a0 + - libopenblas >=0.3.34,<1.0a0 + constrains: + - blas 2.311 openblas + - libcblas 3.11.0 11*_openblas + - liblapack 3.11.0 11*_openblas + - liblapacke 3.11.0 11*_openblas + - mkl <2027 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libblas >=3.11.0,<4.0a0 + size: 18300 + timestamp: 1789061064192 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-h1dcdb26_4.conda + sha256: 8a6e4789a6049dabb41c6fcaf6719232245484d81dfeb199b5abdb95c84c9fc2 + md5: 8f3981871d167a6cd323e636e1929dd4 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 79648 + timestamp: 1788480342707 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-h5295a6a_4.conda + sha256: 4e1f5989561de4546209b42c935795a41fa4265e0b670861480f1adffec623a8 + md5: da537a1643ef3e13bdccf16cca206f2c + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 h1dcdb26_4 + license: MIT + license_family: MIT + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 29864 + timestamp: 1788480352084 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-h2ddc9cb_4.conda + sha256: 4d2da97825b221054a39f492e4dffe361e1931c8381d0367f1e0598a50d02f34 + md5: 39a25d500b191c16996239c4afa104f1 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 h1dcdb26_4 + license: MIT + license_family: MIT + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 295505 + timestamp: 1788480359341 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-11_hb0561ab_openblas.conda + build_number: 11 + sha256: 3cebd672d8bd7c776a381422ea3fc48131343aeda2879974734715ac6a05c292 + md5: 73bde7d713e790b3168707e02102e87b + depends: + - libblas 3.11.0 11_h51639a9_openblas + constrains: + - blas 2.311 openblas + - liblapack 3.11.0 11*_openblas + - liblapacke 3.11.0 11*_openblas + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libcblas >=3.11.0,<4.0a0 + size: 18324 + timestamp: 1789061068736 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp22.1-22.1.8-default_h79071a4_10.conda + sha256: ec2ee8724deb386a3c6f88a9361565d0a2b1784fa908184c96ad73994c501dcd + md5: 6769c5ef2a426eb838343325e71d01d5 + depends: + - libcxx >=22.1.8 + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.15.3 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libllvm22 >=22.1.8,<22.2.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: + weak: + - libclang-cpp22.1 >=22.1.8,<22.2.0a0 + size: 15941789 + timestamp: 1788043670114 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcompiler-rt-22.1.8-hdb3d66b_2.conda + sha256: ed70856638465fa1e3dbce41b7512425a63789774368e4b9878249517daeecad + md5: 1ea9adef104f7304dc445153f1f19274 + depends: + - __osx >=11.0 + constrains: + - compiler-rt >=9.0.1 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: + weak: + - libcompiler-rt >=22.1.8 + size: 1374400 + timestamp: 1787293619574 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-23.1.2-h55c6f16_0.conda + sha256: 087a610bacee7901d259ef72c6f060d795ab26831bd7541ba75ced786a5a63f9 + md5: 01af3f03d850cbab9ae9b86d726a9921 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: {} + size: 579372 + timestamp: 1790163402114 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-he7e0567_1.conda + sha256: d896f4aa4ce4c590c2838678cb1917356fdb461d2a189991c0280c818c362172 + md5: 78650d671cb56909bb3e5c13bce310f9 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 55727 + timestamp: 1785909153744 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.5-h6111c0a_0.conda + sha256: 50aa761b83fc7a116ca88fe0cb4cefce5abababb10a926bf8afbafd5c298d102 + md5: 5383c550b785bdaafcc4eaf638071a7c + depends: + - __osx >=11.0 + constrains: + - expat 2.8.5.* + license: MIT + license_family: MIT + run_exports: {} + size: 69565 + timestamp: 1790370026495 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + sha256: 2783389a7b9dda04c62cc54c6bbeb03dd3cef24b3fc642715d02a3fa19464a9a + md5: 216bbcc23c11e9695b3db4a8771d76eb + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - libffi >=3.7.0,<3.8.0a0 + size: 43637 + timestamp: 1787753403688 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_2.conda + sha256: 19ce8bd320414cb6b9889ecbf48a3ded848b0d1068b76ff6bea099bd7387d6f3 + md5: ee1fc5bba400ff0cae27fbf141a1ae0c + depends: + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL + run_exports: {} + size: 8367 + timestamp: 1786641013393 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-h2ed5691_2.conda + sha256: d9b203d6484ad491b5b5f33d49a9d7a91189d776a9adae53d2b63af18ec11e6a + md5: 881cfb44ea02cc9849f612725a959660 + depends: + - __osx >=11.0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL + run_exports: {} + size: 340923 + timestamp: 1786641012794 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-16.2.0-h3cf6597_7.conda + sha256: deacf62bd6cbbe05eb085dd69a2ca120053597c37066d53c8f7cf6815f26e9eb + md5: f100c8baeb0c816052b0d43948555bf6 + depends: + - _openmp_mutex + constrains: + - libgcc-ng ==16.2.0=*_7 + - libgomp 16.2.0 7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 365973 + timestamp: 1790301995634 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-h05bcc79_12.conda + sha256: 269edce527e204a80d3d05673301e0207efcd0dbeebc036a118ceb52690d6341 + md5: fa4a92cfaae9570d89700a292a9ca714 + depends: + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - icu >=78.1,<79.0a0 + - libexpat >=2.7.3,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libiconv >=1.18,<2.0a0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - libpng >=1.6.53,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GD + license_family: BSD + run_exports: + weak: + - libgd >=2.3.3,<2.4.0a0 + size: 159247 + timestamp: 1766331953491 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-16.2.0-h07b0088_7.conda + sha256: a5b4b63d0ef4c7c59c37225b9f0307b5cd2a3a8499b2a40b6ddd2b9f072712a8 + md5: 2efa9518e126c501a7c7d638b2fa3a21 + depends: + - libgfortran5 16.2.0 hdb7a957_7 + constrains: + - libgfortran-ng ==16.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 99745 + timestamp: 1790302083840 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-16.2.0-hdb7a957_7.conda + sha256: 19297e5cf39de976cdd1a46499a4e15fdf54d249d3231f3b2a9d43af3be0bc66 + md5: c9d80dd8775b8fe76e825d63d8ccaf1b + depends: + - libgcc >=16.2.0 + constrains: + - libgfortran 16.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + run_exports: {} + size: 554793 + timestamp: 1790302001166 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.90.0-hccd281b_0.conda + sha256: 4ec64c476881b31f211842952f192208a3af0925a596da227f4d97fb9a63e0ac + md5: 8f63a5f35547d0c6d819830ebe0d3245 + depends: + - __osx >=11.0 + - libintl >=0.25.1,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + - libffi >=3.7.0,<3.8.0a0 + - libiconv >=1.18,<2.0a0 + - pcre2 >=10.47,<10.48.0a0 + constrains: + - glib >2.66 + license: LGPL-2.1-or-later + run_exports: + weak: + - libglib >=2.90.0,<3.0a0 + size: 4489939 + timestamp: 1789478088421 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libharfbuzz-14.5.0-hcda0f7c_0.conda + sha256: aea77f22cd3ad570ed67d5337554bd7be9b05624cd4d92fa9aa5266601366cdc + md5: 8b507b344b5585932964b45ff966c520 + depends: + - __osx >=11.0 + - cairo >=1.18.6,<2.0a0 + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libcxx >=21 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.90.0,<3.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 1003999 + timestamp: 1790061666650 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libharfbuzz-devel-14.5.0-hcda0f7c_0.conda + sha256: fc6ac81a44d4ce53f302bc5cd602c9ad1caff42515b45b32cf1da853904675f1 + md5: f61ec2bd3aeda96b1f27ecc3ef10cdef + depends: + - __osx >=11.0 + - cairo >=1.18.6,<2.0a0 + - freetype + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libcxx >=21 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.90.0,<3.0a0 + - libharfbuzz 14.5.0 hcda0f7c_0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.5.0 + size: 1551182 + timestamp: 1790061683284 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-he4c29f2_3.conda + sha256: 689a14968267f2f97c07112fca5636e7d756036b9aee969911267c20bd3eea1f + md5: 17f4744c0873e9f77ac9b5cd0a27c187 + depends: + - __osx >=11.0 + license: LGPL-2.1-only + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 750816 + timestamp: 1787033961086 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-hae62183_2.conda + sha256: b43c6645e668f86d04d5c06d6dc17efe921bbd9244be4b77946d56fb950a3fcc + md5: a3a4193101bc5427e6cf8be81d5eeddf + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + license: LGPL-2.1-or-later + run_exports: + weak: + - libintl >=0.25.1,<1.0a0 + size: 90024 + timestamp: 1790118675563 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_1.conda + sha256: 05006418f9392c9b723e8428808db106de0350a497832f95f921b85ff1072310 + md5: b2f8c8e5a7651a1d7c05404c3a159517 + depends: + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 558459 + timestamp: 1785896382474 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-11_hd9741b5_openblas.conda + build_number: 11 + sha256: e7697df761eeb243272801a8c883dc1de6018a7f738746b443ef1396f0a78075 + md5: d67ef1462e77d20cd3e0d27c876c3c9a + depends: + - libblas 3.11.0 11_h51639a9_openblas + constrains: + - blas 2.311 openblas + - libcblas 3.11.0 11*_openblas + - liblapacke 3.11.0 11*_openblas + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - liblapack >=3.11.0,<3.12.0a0 + size: 18284 + timestamp: 1789061072707 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm22-22.1.8-h759d1ac_3.conda + sha256: 70d7bbba65233fec41fb2294e81351d81b8d7e21206113135604fed4dd7fdcf6 + md5: 3b912a5fea164495d16d3edc64242f3a + depends: + - __osx >=11.0 + - libcxx >=21 + - libxml2 + - libxml2-16 >=2.15.4 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: + weak: + - libllvm22 >=22.1.8,<22.2.0a0 + size: 30242555 + timestamp: 1790103714094 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda + sha256: 23d0630046a3e8b164d8f80f2b74ed2605af2e7050ab9913018056402fae4311 + md5: 8ab10323068b107661a4b9a4af84f3b5 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 91720 + timestamp: 1786348695846 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_2.conda + sha256: 04cc136c5a956a73aa14e0a160b5822b0b29714e67b299fa1b1fd16dbcba5366 + md5: ff33a4dbd93abc8a798cc4e0e7c8136d + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + run_exports: {} + size: 73289 + timestamp: 1786651074391 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.34-openmp_h5e6e99c_1.conda + sha256: 18f167fdaa26f67a123c59766f4730b41866d2c0920b5c29d74a93d3a2e92a96 + md5: 9c874eb9d4441a658ef3acf6ac56dc3e + depends: + - __osx >=11.0 + - libgcc >=15 + - libgfortran + - libgfortran5 >=15.3.0 + - llvm-openmp >=21.1.8 + constrains: + - openblas >=0.3.34,<0.3.35.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libopenblas >=0.3.34,<1.0a0 + size: 4249236 + timestamp: 1789023664380 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-hf5e6511_1.conda + sha256: f88da60b348ee1f3e1a9c20fe02142fdafe889f08da0b1cc33c1f3f14460239d + md5: e0466c58fd07db190b9a81b5e58539f2 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 290219 + timestamp: 1786616561185 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.14.7-h4311e70_106_cp314.conda + build_number: 106 + sha256: b239028180e1ba2f7daff4053c89e41f7c0f1a3c21bb82f6dcb09384d123ec1f + md5: 4d7a303343eff82e8b92d52c5fd3991a + depends: + - __osx >=11.0 + - libcxx >=20 + license: Python-2.0 + run_exports: {} + size: 1875368 + timestamp: 1788383432953 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libraqm-0.11.0-h45af499_0.conda + sha256: b110f7f9b2cec61ea793c521950414e00cd64826e18aeb4ab40369acf05c0039 + md5: 46ea0eb4e71bffa35ab7070678bcb053 + depends: + - __osx >=11.0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libharfbuzz >=14.2.1 + - fribidi >=1.0.16,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libraqm >=0.11.0,<0.12.0a0 + size: 31333 + timestamp: 1784850348342 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.62.4-hc8ac265_0.conda + sha256: 3bd54de0136907782249a0d5797744ecac5bbd77026f5f2712dd62390c5c9f77 + md5: 878c79beee628e3b683ddc9094aaad1e + depends: + - __osx >=11.0 + - pango >=1.58.2,<2.0a0 + - libglib >=2.90.0,<3.0a0 + - fontconfig >=2.18.3,<3.0a0 + - fonts-conda-ecosystem + - libxml2-16 >=2.15.4 + - gdk-pixbuf >=2.44.8,<3.0a0 + - libharfbuzz >=14.5.0 + - cairo >=1.18.6,<2.0a0 + constrains: + - __osx >=11.0 + license: LGPL-2.1-or-later + run_exports: + weak: + - librsvg >=2.62.4,<3.0a0 + size: 3888310 + timestamp: 1790276358897 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_1.conda + sha256: fcfa03afe31f40dfabf011629d99836adbebbc3ed1b38c7e9eee9ffc7581975b + md5: c70eb797aa92a294b09ef8f41f6bd578 + depends: + - __osx >=11.0 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 36651 + timestamp: 1786115069018 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.4-hca69786_1.conda + sha256: 839b31d4830e896b4d315b551d27f2bb08026bc946df04bcc360d8627c3ba2cd + md5: fde96d40ebe9a9cb34e4122380189ddb + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.4,<4.0a0 + size: 942754 + timestamp: 1787051243846 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-hf67920b_1.conda + sha256: 847d22a86ae28f66d3888702698254d25bb4eac3a0f64c1fabfb1842ac00be4c + md5: b6b191267455bf202af79f973690ed5d + depends: + - __osx >=11.0 + - lerc >=4.2.0,<5.0a0 + - libcxx >=21 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 380645 + timestamp: 1787756067271 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h202fb40_1.conda + sha256: 0ff54650d470c7e54cbeffdd53a8c063e055a7bcf784388c0385ce5c4741b0f4 + md5: 168a13e329259710b28277abc1395b8e + depends: + - __osx >=11.0 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 + size: 294522 + timestamp: 1785955350410 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hbffa61f_2.conda + sha256: f55c15b66c8ae27d8e30445193acc049b280eb51a47d6f1753bed5ac2937a217 + md5: 786c0680e77665d1938ce9cdd7cf82a7 + depends: + - __osx >=11.0 + - pthread-stubs + - xorg-libxau >=1.0.12,<2.0a0 + - xorg-libxdmcp >=1.1.5,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 + size: 324234 + timestamp: 1787885764666 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.4-heb56d2d_0.conda + sha256: 8a26f7c94ab849c1930da36c298f4b82e892382b70ae865cd7274188d9d783e2 + md5: b40633f711a5eb8617917e0e944b9965 + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - libxml2 2.15.4 + license: MIT + license_family: MIT + run_exports: {} + size: 469299 + timestamp: 1788635201153 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.4-h550178d_0.conda + sha256: 81217fcd4d4abdc69ee63cdb9cdb22e05a20b9ba7a76f596579914a838fe5960 + md5: 4b97a34d06740b578c32deba195daf5a + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.4 heb56d2d_0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libxml2 + - libxml2-16 >=2.15.4 + size: 41375 + timestamp: 1788635206084 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda + sha256: a18fa5d5bac452401459f966cf0d872224e8080c4ff93c77e168d43ab42ef9d7 + md5: f39288f0ea63ae962e1a2e4f355a0d75 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_3 + license: Zlib + license_family: Other + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 47822 + timestamp: 1785277049190 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-23.1.2-hdb3d66b_0.conda + sha256: f9aea67117079c00b9663ee0bbca7e5409dbe94625c34e68881ce2c97c3d47c9 + md5: 97fa48b6e1d996689e6d3f822a41f1bf + depends: + - __osx >=11.0 + constrains: + - intel-openmp <0.0a0 + - openmp 23.1.2|23.1.2.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: + strong: + - llvm-openmp >=23.1.2 + size: 287714 + timestamp: 1790195505384 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22-22.1.8-h79441dc_3.conda + sha256: c12ad5ec1b2c49035ee7a3cce1cd50e343501197690c72f2c2a39ab191cb1b31 + md5: 5ace3459a3856a60a9186e690814f308 + depends: + - __osx >=11.0 + - libcxx >=21 + - libllvm22 22.1.8 h759d1ac_3 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: {} + size: 17967559 + timestamp: 1790103797234 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22.1.8-hdb3d66b_3.conda + sha256: 67137f6b0a06e4eb2a06db9f5abf9abaa24a939c2685c2adc8d9d3fbeadcdb25 + md5: 923076e1f8d6be491765135bdbf829f6 + depends: + - __osx >=11.0 + - libllvm22 22.1.8 h759d1ac_3 + - llvm-tools-22 22.1.8 h79441dc_3 + constrains: + - clang 22.1.8 + - clang-tools 22.1.8 + - llvm 22.1.8 + - llvmdev 22.1.8 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: {} + size: 53908 + timestamp: 1790103835437 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + sha256: 411153d14ee0d98be6e3751cf5cc0502db17bce2deebebb8779e33d29d0e525f + md5: d33c0a15882b70255abdd54711b06a45 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 27256 + timestamp: 1772445397216 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-3.11.2-py314h314fc0d_1.conda + sha256: 3a856ebbf2571ac643459be3bb65097a33bbc51b7302ceb026d517e6473eaf79 + md5: 33e0ab88c137fd363cbb13372c00f168 + depends: + - matplotlib-base >=3.11.2,<3.11.3.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tornado >=5 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 15007 + timestamp: 1789824062128 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.11.2-py314heb334b3_1.conda + sha256: 123d5d4a269e96d5c0133614cffcab56bbaf314dab65290d57fb3ebf17986fe0 + md5: c00a0cfa60f7db715d294a9f14766946 + depends: + - __osx >=11.0 + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.28.2 + - freetype + - kiwisolver >=1.3.1 + - libcxx >=21 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libraqm >=0.11.0,<0.12.0a0 + - numpy >=1.25 + - numpy >=1.25,<3 + - packaging >=20.0 + - pillow >=9 + - pyparsing >=3 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.7 + - python_abi 3.14.* *_cp314 + - qhull >=2020.2,<2020.3.0a0 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 8961419 + timestamp: 1789824046629 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda + sha256: 7024a48c8c0d0114ed4ab53c76bf9275d50e91ba7cea367a9aead638d3c29c68 + md5: 3dfa0d0316dc246cd44937a557de4501 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + run_exports: + weak: + - ncurses >=6.6,<7.0a0 + size: 804298 + timestamp: 1786355189145 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.5.3-py314he06036b_0.conda + sha256: 693fa3d8d4303c35250025409d1cbc6d0d6270fd3778303dfe9ea89e29003871 + md5: 37bb35c87a060e9ab1551c5bd987074f + depends: + - python + - __osx >=11.0 + - libcxx >=21 + - libblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - numpy >=1.25,<3 + size: 7273776 + timestamp: 1788732001328 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-h4d1e80c_2.conda + sha256: e71bd04b5d0e39f39a3c039d021d3b76cd65bee3032e7f859ab7082f0ace8be5 + md5: 487d92910109a711fdb77631bf1aabf0 + depends: + - __osx >=11.0 + - libcxx >=21 + - libtiff >=4.7.2,<4.8.0a0 + - libzlib >=1.3.2,<2.0a0 + - libpng >=1.6.58,<1.7.0a0 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openjpeg >=2.5.4,<3.0a0 + size: 377958 + timestamp: 1788425205328 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.4-h55eecbc_0.conda + sha256: f23239eacd75c4c50705e68fae1aa3292da473e6a3a4abe2330f1e6afa680704 + md5: ae71ab40048c19a389a7dcccb86c2481 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - openssl >=3.6.4,<4.0a0 + size: 3110142 + timestamp: 1787698648639 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.58.2-hf9b25ef_1.conda + sha256: 9ba23103198cffa523488ff583b184ad37eb6d90985d330a683a1039dc917771 + md5: 4a71e68873ce37dc6da671a899d09a46 + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.18.3,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.16,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.3,<3.0a0 + - libharfbuzz >=14.4.0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + license: LGPL-2.1-or-later + run_exports: + weak: + - pango >=1.58.2,<2.0a0 + size: 442274 + timestamp: 1788491046895 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-he63d830_1.conda + sha256: f8c415329b542e1fca1ff2917b80e687c18302dfa0353530668b90cb225b494f + md5: 5ab90033816cbe4097e8a297e5179f67 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 + size: 851704 + timestamp: 1787294559157 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-5.32.1-10_hb001647_perl5.conda + build_number: 10 + sha256: 657cd963e25925bbb71dca27c01b47fef76be97e8a8517b8fcfc420233730614 + md5: 91c1e7fbb919b74499ce900a083fd517 + depends: + - __osx >=11.0 + license: GPL-1.0-or-later OR Artistic-1.0-Perl + run_exports: + weak: + - perl >=5.32.1,<5.33.0a0 *_perl5 + noarch: + - perl >=5.32.1,<6.0a0 *_perl5 + - __unix + size: 12278211 + timestamp: 1790377655459 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-capture-tiny-0.48-pl5321hce30654_1.tar.bz2 + sha256: 16bd59b9fd3dcd7f54c70844d6227c4811b317ac4530d48b48f4f8374277cb72 + md5: aa3af199b5675fa543e985c7e5d2e625 + depends: + - perl >=5.32.1,<5.33.0a0 *_perl5 + license: Artistic-2.0 + run_exports: {} + size: 16478 + timestamp: 1655720247857 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-devel-stacktrace-2.04-pl5321ha040cf9_0.conda + sha256: b963b564b2f8004ff978a96165f5a9c5249eec382ded5aeae7b26a45eee8e575 + md5: 850d67c0aac1222d3b67dc0164ba051d + depends: + - perl >=5.32.1,<5.33.0a0 *_perl5 + license: Artistic-2.0 + run_exports: + weak: + - perl-devel-stacktrace =2.04 + size: 20146 + timestamp: 1745394687575 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.3.0-py314h709c99d_4.conda + sha256: 734cd2c0fb24436f5ebddd29403472a6d8aa1594652e83555393f3a3c890b73a + md5: 51c68057c585094507bb9816326d6252 + depends: + - python + - __osx >=11.0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - zlib-ng >=2.3.3,<2.4.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libwebp-base >=1.6.0,<2.0a0 + - libtiff >=4.7.2,<4.8.0a0 + - lcms2 >=2.19.1,<3.0a0 + - python_abi 3.14.* *_cp314 + - libxcb >=1.17.0,<2.0a0 + - openjpeg >=2.5.4,<3.0a0 + - tk >=8.6.13,<8.7.0a0 + license: HPND + run_exports: {} + size: 1025614 + timestamp: 1789122697364 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h784d473_3.conda + sha256: 4779cd57231ce2e96fac643fcbdd4a6f51a57986264545445574e9c4acf526d3 + md5: 9a99c0b60efe41c194d01c182d000733 + depends: + - __osx >=11.0 + - libcxx >=19 + license: MIT + license_family: MIT + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 198717 + timestamp: 1786106922508 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hb001647_1004.conda + sha256: 3f79452dbda3febaf7053486e4844fa945accf4d74eac4169e2c4d5dc5e4dd0b + md5: f0833c4a0b68798f50935dd83384c705 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: {} + size: 9662 + timestamp: 1788382422327 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.7-h0ae1c2c_106_cp314.conda + build_number: 106 + sha256: 5ffc8349ca089ec5ac39ad36a095323ad177ebe05cbfc690c1e135560cfd68b7 + md5: 5fd37c78250b4f57c2bcf9ad2e54beb1 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.7.0,<3.8.0a0 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libpython 3.14.7 h4311e70_106_cp314 + - libsqlite >=3.53.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.8,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + run_exports: + weak: + - python_abi 3.14.* *_cp314 + noarch: + - python + size: 12223441 + timestamp: 1788383470498 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + sha256: 873ac689484262a51fd79bc6103c1a1bedbf524924d7f0088fb80703042805e4 + md5: 6483b1f59526e05d7d894e466b5b6924 + depends: + - __osx >=11.0 + - libcxx >=16 + license: LicenseRef-Qhull + run_exports: + weak: + - qhull >=2020.2,<2020.3.0a0 + size: 516376 + timestamp: 1720814307311 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda + sha256: d6782b430e6dce4fe8c7ac118cd988d5a193eb4a7f60741edfaede58016b6110 + md5: 9347fd56a002e6b58946831d48fe62f6 + depends: + - __osx >=11.0 + - ncurses >=6.6,<7.0a0 + license: GPL-3.0-only + license_family: GPL + run_exports: + weak: + - readline >=8.3,<9.0a0 + size: 314395 + timestamp: 1787033878899 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_1.conda + sha256: 7efd6d7d18bf9cc5788bfe1c1e1620d9dbbe00a81c646814929a82ff31944cf3 + md5: a322c0a0d3b5be99ee11f9ccec99b60e + depends: + - __osx >=11.0 + - libsigtool 0.1.3 h98dc951_1 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 114653 + timestamp: 1786115093248 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1600.0.11.8-hb561403_4.conda + sha256: 6effea9587f049e702c838e8e57d98ac92b89ca1f3f1edd51c78dd252d24e83b + md5: fdb31cc6c306f60e194f29f17397a63c + depends: + - libcxx >=19.0.0.a0 + - __osx >=11.0 + - ncurses >=6.6,<7.0a0 + license: NCSA + run_exports: {} + size: 202107 + timestamp: 1789668272580 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda + sha256: 857d89087ae7f2c328cf256728affcb7343031a148b4af847334d248a9cf564c + md5: 90a01bf394d1c594e0eb9258f967d828 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: TCL + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3342183 + timestamp: 1787272852357 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.10-py314hcd3153d_0.conda + sha256: b18706fdd0022f0467912956a24d2e28180ba37d14555be5e382bea51869a6ae + md5: 9079ddcc60b0cf5848f665fa0e7088f9 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + run_exports: {} + size: 952431 + timestamp: 1789911042938 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hb001647_2.conda + sha256: cde7cecabb662b435e6537591e5845e870a0648887542d3e538c9f6c20dd6be5 + md5: 610f557e75d6a0d3f676914da5822227 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 + size: 17325 + timestamp: 1788960997645 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hb001647_2.conda + sha256: 30240b39d7b956e8f7a8231ffa0c1fce097124e40b3eb48a523d13d3937e3194 + md5: 14a26fe87734f988eed32abf6afd4c3e + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 + size: 21021 + timestamp: 1789677329602 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda + sha256: 489a29915a3e1b425cff4df10a448f55bbaaf29d777a0f8a9e381444e6a284f1 + md5: 4621ded2fd5b36f51454d5f81bff4ec1 + depends: + - __osx >=11.0 + - libcxx >=21 + license: Zlib + license_family: Other + run_exports: + weak: + - zlib-ng >=2.3.3,<2.4.0a0 + size: 95857 + timestamp: 1786737243497 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda + sha256: da867f5092eb0cb746d353694f0098031fd9817a4ce7d5743121209ae0f406ca + md5: 4ec2684c73812cc2c3d78379384a39cc + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 + size: 433687 + timestamp: 1786599629846 diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..600fc8f --- /dev/null +++ b/pixi.toml @@ -0,0 +1,17 @@ +[workspace] +authors = ["Jacob Williams "] +channels = ["conda-forge"] +name = "NumDiff" +platforms = ["osx-arm64"] +version = "1.8.2" + +[tasks] + +[dependencies] +gfortran = "15.*" +fpm = ">=0.13.0,<0.14" +python = ">=3.14.7,<3.15" +graphviz = ">=14.1.2,<15" +ford = ">=7.0.13,<8" +matplotlib = ">=3.11.2,<4" +lcov = ">=2.5,<3" diff --git a/src/cache_module.f90 b/src/cache_module.f90 index fb4cd0b..7384434 100644 --- a/src/cache_module.f90 +++ b/src/cache_module.f90 @@ -70,7 +70,7 @@ subroutine initialize_cache(me,isize,n,m,chunk_size) me%m = m if (present(chunk_size)) then - me%chunk_size = chunk_size + me%chunk_size = max(1,chunk_size) else me%chunk_size = 100 end if @@ -95,7 +95,7 @@ subroutine print_cache(me,iunit) write(iunit,'(A)') '' write(iunit,'(A)') '------------------------' if (allocated(me%c)) then - do i = 1, size(me%c) + do i = lbound(me%c,1), ubound(me%c,1) ! note: the table is 0-based if (allocated(me%c(i)%x)) then write(iunit,'(A)') '' write(iunit,'(A,1X,I10)') 'Entry ',i diff --git a/src/dsm_module.f90 b/src/dsm_module.f90 index 78085dd..9b2d944 100644 --- a/src/dsm_module.f90 +++ b/src/dsm_module.f90 @@ -94,9 +94,11 @@ subroutine dsm(m,n,Npairs,Indrow,Indcol,Ngrp,Maxgrp,Mingrp,Info,Ipntr,Jpntr) !! note that `jpntr(n+1)-1` is then the number of non-zero !! elements of the matrix `a`. - integer,dimension(max(m,6*n)) :: iwa !! an integer work array + integer,dimension(:),allocatable :: iwa !! an integer work array integer :: i , ir , j , jp , k , maxclq , nnz , numgrp + allocate(iwa(max(m,6*n))) + ! check the input data. Info = 0 @@ -395,7 +397,9 @@ subroutine ido(m,n,Indrow,Jpntr,Indcol,Ipntr,Ndeg,List,Maxclq, & do ir = 1 , m maxlst = maxlst + (Ipntr(ir+1)-Ipntr(ir))**2 enddo - maxlst = maxlst/n + ! at least one column must be examined, otherwise + ! jcol would not be set in the selection loop below: + maxlst = max(1,maxlst/n) Maxclq = 0 numord = 1 diff --git a/src/numerical_differentiation_module.f90 b/src/numerical_differentiation_module.f90 index 9bb2a90..1959736 100644 --- a/src/numerical_differentiation_module.f90 +++ b/src/numerical_differentiation_module.f90 @@ -21,6 +21,10 @@ module numerical_differentiation_module real(wp),parameter :: zero = 0.0_wp + integer,parameter,public :: max_num_sparsity_points = 50 !! the maximum allowed value of + !! `num_sparsity_points`. Larger values would + !! produce duplicate points in [[divide_interval]]. + type,public :: finite_diff_method !! defines the finite difference method @@ -83,10 +87,24 @@ module numerical_differentiation_module !! column `jcol` belongs to group `ngrp(jcol)`. !! `size(n)` + integer,dimension(:),allocatable :: col_ptr !! column pointers into `col_idx` (size `n+1`). + !! the elements in column `j` are + !! `col_idx(col_ptr(j):col_ptr(j+1)-1)` + integer,dimension(:),allocatable :: col_idx !! indices of the nonzero elements (into `irow`, + !! `icol`, and `jac`), sorted by column + !! (size `num_nonzero_elements`) + integer,dimension(:),allocatable :: grp_ptr !! group pointers into `grp_cols` (size `maxgrp+1`). + !! the columns in group `g` are + !! `grp_cols(grp_ptr(g):grp_ptr(g+1)-1)` + integer,dimension(:),allocatable :: grp_cols !! the columns, sorted by partition group (size `n`) + contains private procedure :: dsm_wrapper procedure :: compute_indices + procedure :: compute_group_index + procedure :: get_partition_pattern + procedure :: partition_is_consistent procedure,public :: destroy => destroy_sparsity procedure,public :: print => print_sparsity procedure,public :: columns_in_partition_group @@ -112,7 +130,7 @@ module numerical_differentiation_module logical :: print_messages = .true. !! if true, warning messages are printed !! to the `error_unit` for any errors. - integer :: chunk_size = 100 !! chuck size for allocating the arrays (>0) + integer :: chunk_size = 100 !! chunk size for allocating the arrays (>0) integer :: perturb_mode = 1 !! perturbation mode: !! @@ -142,11 +160,13 @@ module numerical_differentiation_module logical :: compute_linear_sparsity_pattern = .false. !! to also compute the linear sparsity pattern real(wp) :: linear_sparsity_tol = epsilon(1.0_wp) !! the equality tolerance for derivatives to !! indicate a constant jacobian element (linear sparsity) - real(wp) :: function_precision_tol = epsilon(1.0_wp) !! the function precision. two functions values - !! that are the within this tolerance are - !! considered the same value. This is used + !! (relative to the largest derivative magnitude) + real(wp) :: function_precision_tol = epsilon(1.0_wp) !! the relative function precision. two function values + !! that are the same to within this relative tolerance + !! are considered the same value. This is used !! when estimating the sparsity pattern when !! `sparsity_mode=2` in [[compute_sparsity_random]] + !! or `sparsity_mode=4` in [[compute_sparsity_random_2]] integer :: mode = 1 !! **1** = use `meth` (specified methods), !! **2** = use `class` (specified class, method is selected on-the-fly). @@ -156,6 +176,9 @@ module numerical_differentiation_module integer,dimension(:),allocatable :: class !! the class of method to use to !! compute the `n`th column of the Jacobian !! `size(n)`. Either this or `meth` is used + type(meth_array) :: sparsity_class_meths !! the 2-point methods used to compute the + !! sparsity pattern when `sparsity_mode=4` + !! (set once by [[set_sparsity_mode]]) type(meth_array),dimension(:),allocatable :: class_meths !! array of methods for the specified classes. !! used with `class` when `mode=2` @@ -231,8 +254,10 @@ module numerical_differentiation_module procedure :: compute_sparsity_perturbation_vector procedure :: perturb_x_and_compute_f procedure :: perturb_x_and_compute_f_partitioned + procedure :: compute_nominal_function procedure :: set_numdiff_sparsity_bounds procedure :: set_sparsity_mode + procedure :: set_num_sparsity_points procedure :: generate_dense_sparsity_partition procedure :: compute_jacobian_for_sparsity procedure :: resize_sparsity_vectors @@ -381,6 +406,11 @@ subroutine compute_function_with_cache(me,x,f,funcs_to_compute) integer :: i !! index in the cache logical,dimension(size(funcs_to_compute)) :: ffound !! functions found in the cache logical :: xfound !! if `x` was found in the cache + integer,dimension(:),allocatable :: missing !! the functions not found in the cache + real(wp),dimension(size(f)) :: ftmp !! the missing functions. these are computed into + !! a separate array, since `f` is `intent(out)` + !! in the user function and would otherwise + !! lose the values found in the cache. if (me%exception_raised) return ! check for exceptions @@ -393,13 +423,17 @@ subroutine compute_function_with_cache(me,x,f,funcs_to_compute) ! compute the ones that weren't found, ! and add them to the cache: - call me%problem_func(x,f,pack(funcs_to_compute,mask=(.not. ffound))) - call me%cache%put(i,x,f,pack(funcs_to_compute,mask=(.not. ffound))) + missing = pack(funcs_to_compute,mask=(.not. ffound)) + call me%problem_func(x,ftmp,missing) + if (me%exception_raised) return ! check for exceptions + f(missing) = ftmp(missing) + call me%cache%put(i,x,ftmp,missing) else ! compute the function and add it to the cache: call me%problem_func(x,f,funcs_to_compute) + if (me%exception_raised) return ! check for exceptions call me%cache%put(i,x,f,funcs_to_compute) end if @@ -1000,7 +1034,7 @@ subroutine initialize_numdiff_for_diff(me,n,m,xlow,xhigh,& !! It can be used to perform any !! setup operations. integer,intent(in),optional :: chunk_size !! chunk size for allocating the arrays - !! (must be >0) [default is 100] + !! (the absolute value is used, and values <1 are treated as 1) [default is 100] real(wp),intent(in),optional :: eps !! tolerance parameter for [[diff]] !! if not present, default is `1.0e-9_wp` real(wp),intent(in),optional :: acc !! tolerance parameter for [[diff]] @@ -1024,13 +1058,16 @@ subroutine initialize_numdiff_for_diff(me,n,m,xlow,xhigh,& !! **3** - perturbation is `dx=dpert*(1+x)` integer,intent(in),optional :: num_sparsity_points !! for `sparsity_mode=4`, the number of jacobian !! evaluations used to estimate the sparsity pattern. + !! must be in the range `[1, max_num_sparsity_points]` (default is 3). real(wp),intent(in),optional :: linear_sparsity_tol !! the equality tolerance for derivatives to !! indicate a constant jacobian element (linear sparsity) - real(wp),intent(in),optional :: function_precision_tol !! the function precision. two functions values - !! that are the within this tolerance are - !! considered the same value. This is used + !! (relative to the largest derivative magnitude) + real(wp),intent(in),optional :: function_precision_tol !! the relative function precision. two function values + !! that are the same to within this relative tolerance + !! are considered the same value. This is used !! when estimating the sparsity pattern when !! `sparsity_mode=2` in [[compute_sparsity_random]] + !! or `sparsity_mode=4` in [[compute_sparsity_random_2]] logical,intent(in),optional :: print_messages !! if true, print error messages to `error_unit`. !! default is True. @@ -1085,10 +1122,10 @@ subroutine initialize_numdiff_for_diff(me,n,m,xlow,xhigh,& ! if these aren't present, they will just keep the defaults: if (present(linear_sparsity_tol)) me%linear_sparsity_tol = linear_sparsity_tol if (present(function_precision_tol)) me%function_precision_tol = function_precision_tol - if (present(num_sparsity_points)) me%num_sparsity_points = num_sparsity_points + if (present(num_sparsity_points)) call me%set_num_sparsity_points(num_sparsity_points) ! optional: - if (present(chunk_size)) me%chunk_size = abs(chunk_size) + if (present(chunk_size)) me%chunk_size = max(1,abs(chunk_size)) if (present(eps)) me%eps = eps if (present(acc)) me%acc = acc if (present(info)) me%info_function => info @@ -1193,6 +1230,8 @@ subroutine set_sparsity_mode(me,sparsity_mode,xlow_for_sparsity,xhigh_for_sparsi call me%set_numdiff_sparsity_bounds(xlow_for_sparsity,xhigh_for_sparsity) case(4) ! compute 2-point jacobian in specified number of points me%compute_sparsity => compute_sparsity_random_2 + ! 2-point methods (simple differences) are used for the sparsity jacobians: + me%sparsity_class_meths = get_all_methods_in_class(2) ! in this case, we have the option of specifying ! separate bounds for computing the sparsity: call me%set_numdiff_sparsity_bounds(xlow_for_sparsity,xhigh_for_sparsity) @@ -1281,6 +1320,31 @@ subroutine set_dpert(me,dpert) end subroutine set_dpert !******************************************************************************* +!******************************************************************************* +!> +! Set the number of points used to estimate the sparsity pattern +! when `sparsity_mode=4`. Must be in the range `[1, max_num_sparsity_points]`. + + subroutine set_num_sparsity_points(me,num_sparsity_points) + + implicit none + + class(numdiff_type),intent(inout) :: me + integer,intent(in) :: num_sparsity_points !! number of points + + if (me%exception_raised) return ! check for exceptions + + if (num_sparsity_points<1 .or. num_sparsity_points>max_num_sparsity_points) then + call me%raise_exception(32,'set_num_sparsity_points',& + 'num_sparsity_points must be between 1 and '//& + integer_to_string(max_num_sparsity_points)//'.') + else + me%num_sparsity_points = num_sparsity_points + end if + + end subroutine set_num_sparsity_points +!******************************************************************************* + !******************************************************************************* !> ! Initialize a [[numdiff_type]] class. This must be called first. @@ -1335,7 +1399,7 @@ subroutine initialize_numdiff(me,n,m,xlow,xhigh,perturb_mode,dpert,& !! It can be used to perform any !! setup operations. integer,intent(in),optional :: chunk_size !! chunk size for allocating the arrays - !! (must be >0) [default is 100] + !! (the absolute value is used, and values <1 are treated as 1) [default is 100] logical,intent(in),optional :: partition_sparsity_pattern !! if the sparisty pattern is to !! be partitioned using [[DSM]] !! [default is False] @@ -1357,13 +1421,16 @@ subroutine initialize_numdiff(me,n,m,xlow,xhigh,perturb_mode,dpert,& !! **3** - perturbation is `dx=dpert*(1+x)` real(wp),intent(in),optional :: linear_sparsity_tol !! the equality tolerance for derivatives to !! indicate a constant jacobian element (linear sparsity) - real(wp),intent(in),optional :: function_precision_tol !! the function precision. two functions values - !! that are the within this tolerance are - !! considered the same value. This is used + !! (relative to the largest derivative magnitude) + real(wp),intent(in),optional :: function_precision_tol !! the relative function precision. two function values + !! that are the same to within this relative tolerance + !! are considered the same value. This is used !! when estimating the sparsity pattern when !! `sparsity_mode=2` in [[compute_sparsity_random]] + !! or `sparsity_mode=4` in [[compute_sparsity_random_2]] integer,intent(in),optional :: num_sparsity_points !! for `sparsity_mode=4`, the number of jacobian !! evaluations used to estimate the sparsity pattern. + !! must be in the range `[1, max_num_sparsity_points]` (default is 3). integer :: i !! counter logical :: found !! flag for [[get_finite_difference_method]] @@ -1444,9 +1511,19 @@ subroutine initialize_numdiff(me,n,m,xlow,xhigh,perturb_mode,dpert,& me%mode = 2 me%class = classes allocate(me%class_meths(n)) - do i=1,n - me%class_meths(i) = get_all_methods_in_class(me%class(i)) - end do + ! look up each distinct class only once: + block + integer,dimension(:),allocatable :: unique_classes !! the distinct values in `classes` + type(meth_array) :: meths !! the methods for one class + integer :: k !! counter + unique_classes = unique(me%class,chunk_size=10) + do k = 1, size(unique_classes) + meths = get_all_methods_in_class(unique_classes(k)) + do i=1,n + if (me%class(i)==unique_classes(k)) me%class_meths(i) = meths + end do + end do + end block if (me%partition_sparsity_pattern) then call me%raise_exception(11,'initialize_numdiff',& 'when using partitioned sparsity pattern, '//& @@ -1481,7 +1558,7 @@ subroutine initialize_numdiff(me,n,m,xlow,xhigh,perturb_mode,dpert,& ! optional: if (present(info)) me%info_function => info - if (present(chunk_size)) me%chunk_size = abs(chunk_size) + if (present(chunk_size)) me%chunk_size = max(1,abs(chunk_size)) ! set the jacobian function, depending on the options: if (me%partition_sparsity_pattern) then @@ -1495,7 +1572,7 @@ subroutine initialize_numdiff(me,n,m,xlow,xhigh,perturb_mode,dpert,& if (present(linear_sparsity_tol)) me%linear_sparsity_tol = linear_sparsity_tol if (present(function_precision_tol)) me%function_precision_tol = function_precision_tol - if (present(num_sparsity_points)) me%num_sparsity_points = num_sparsity_points + if (present(num_sparsity_points)) call me%set_num_sparsity_points(num_sparsity_points) if (present(dpert_for_sparsity)) then me%dpert_for_sparsity = abs(dpert_for_sparsity) @@ -1570,18 +1647,120 @@ subroutine dsm_wrapper(me,n,m,info) allocate(ipntr(m+1)) allocate(jpntr(n+1)) + if (allocated(me%ngrp)) deallocate(me%ngrp) allocate(me%ngrp(n)) - irow = me%irow - icol = me%icol + call me%get_partition_pattern(irow,icol) + + if (size(irow)==0) then + ! no elements (e.g., the functions do not depend on x), + ! so all the columns can be in one group: + ! [dsm does not accept an empty pattern] + me%maxgrp = 1 + me%ngrp = 1 + info = 1 + call me%compute_group_index() + return + end if - call dsm(m,n,me%num_nonzero_elements,& + call dsm(m,n,size(irow),& irow,icol,& me%ngrp,me%maxgrp,& mingrp,info,ipntr,jpntr) + if (info==1) call me%compute_group_index() + end subroutine dsm_wrapper !******************************************************************************* +!******************************************************************************* +!> +! Returns the pattern that the columns must be partitioned on: the nonlinear +! elements plus the linear (constant) elements. The linear elements must be +! included, since the function still depends on those variables: two columns +! that share a row through a linear element cannot be perturbed together. + + subroutine get_partition_pattern(me,irow,icol) + + implicit none + + class(sparsity_pattern),intent(in) :: me + integer,dimension(:),allocatable,intent(out) :: irow !! rows of the elements + integer,dimension(:),allocatable,intent(out) :: icol !! columns of the elements + + if (allocated(me%irow)) then + irow = me%irow + icol = me%icol + else + allocate(irow(0), icol(0)) + end if + if (me%linear_sparsity_computed .and. allocated(me%linear_irow)) then + irow = [irow, me%linear_irow] + icol = [icol, me%linear_icol] + end if + + end subroutine get_partition_pattern +!******************************************************************************* + +!******************************************************************************* +!> +! Returns true if the partition (`ngrp`, `maxgrp`) is consistent with the +! sparsity pattern (including the linear elements): no two columns in the +! same group may have an element in the same row. + + function partition_is_consistent(me,m) result(consistent) + + implicit none + + class(sparsity_pattern),intent(in) :: me + integer,intent(in) :: m !! number of rows of the jacobian + logical :: consistent + + integer,dimension(:),allocatable :: irow, icol !! the pattern to check + integer,dimension(:),allocatable :: row_ptr !! row pointers into `by_row` + integer,dimension(:),allocatable :: by_row !! element indices, sorted by row + integer,dimension(:),allocatable :: next !! next free position for each row + integer,dimension(:),allocatable :: group_row !! last row in which each group was seen + integer,dimension(:),allocatable :: group_col !! the column in that row for each group + integer :: k, r, j, c, g + + consistent = .false. + if (.not. allocated(me%ngrp) .or. me%maxgrp<1) return + call me%get_partition_pattern(irow,icol) + + ! sort the elements by row (counting sort): + allocate(row_ptr(m+1), by_row(size(irow))) + row_ptr = 0 + do k = 1, size(irow) + row_ptr(irow(k)+1) = row_ptr(irow(k)+1) + 1 + end do + row_ptr(1) = 1 + do r = 1, m + row_ptr(r+1) = row_ptr(r+1) + row_ptr(r) + end do + next = row_ptr(1:m) + do k = 1, size(irow) + by_row(next(irow(k))) = k + next(irow(k)) = next(irow(k)) + 1 + end do + + ! in each row, each group may only appear for one column: + allocate(group_row(me%maxgrp), group_col(me%maxgrp)) + group_row = 0 + group_col = 0 + do r = 1, m + do j = row_ptr(r), row_ptr(r+1)-1 + c = icol(by_row(j)) + g = me%ngrp(c) + if (group_row(g)==r .and. group_col(g)/=c) return ! two columns of group g in row r + group_row(g) = r + group_col(g) = c + end do + end do + consistent = .true. + + end function partition_is_consistent +!******************************************************************************* + !******************************************************************************* !> ! Returns the columns in a sparsity partition group. @@ -1603,54 +1782,41 @@ subroutine columns_in_partition_group(me,igroup,n_cols,cols,nonzero_rows,indices logical,intent(out) :: status_ok !! true if the partition is valid integer :: i !! counter - integer :: num_nonzero_elements_in_col !! number of nonzero elements in a column - integer :: num_nonzero_elements_in_group !! number of nonzero elements in a group - integer,dimension(:),allocatable :: col_indices !! nonzero indices in `jac` for a column - - if (me%maxgrp>0 .and. allocated(me%ngrp)) then - - status_ok = .true. - - n_cols = count(me%ngrp==igroup) - if (n_cols>0) then - allocate(cols(n_cols)) - cols = pack([(i,i=1,size(me%ngrp))],mask=me%ngrp==igroup) - end if + integer :: c !! column number + integer :: k !! counter for the nonzero elements in the group + integer :: nnz_col !! number of nonzero elements in a column + integer :: num_nonzero_elements_in_group !! number of nonzero elements in a group + + status_ok = me%maxgrp>0 .and. allocated(me%ngrp) .and. & + allocated(me%grp_ptr) .and. allocated(me%col_ptr) + if (status_ok) status_ok = igroup>=1 .and. igroup<=me%maxgrp + if (.not. status_ok) then + n_cols = 0 + return + end if - ! get all the non-zero elements in each column: - num_nonzero_elements_in_group = 0 ! initialize - do i = 1, n_cols - num_nonzero_elements_in_col = count(me%icol==cols(i)) - if (num_nonzero_elements_in_col/=0) then ! there are functions to - ! compute in this column - num_nonzero_elements_in_group = num_nonzero_elements_in_group + & - num_nonzero_elements_in_col - if (allocated(col_indices)) deallocate(col_indices) - allocate(col_indices(num_nonzero_elements_in_col)) - !col_indices = pack(me%indices,mask=me%icol==cols(i)) - block - integer :: j,n - n = 0 - do j = 1, size(me%icol) - if (me%icol(j)==cols(i)) then - n = n + 1 - col_indices(n) = j - end if - end do - end block - if (allocated(nonzero_rows)) then - nonzero_rows = [nonzero_rows,me%irow(col_indices)] - indices = [indices,col_indices] - else - nonzero_rows = me%irow(col_indices) - indices = col_indices - end if - end if - end do + n_cols = me%grp_ptr(igroup+1) - me%grp_ptr(igroup) + if (n_cols==0) return + cols = me%grp_cols(me%grp_ptr(igroup):me%grp_ptr(igroup+1)-1) - else - status_ok = .false. - end if + ! get all the non-zero elements in each column: + num_nonzero_elements_in_group = 0 + do i = 1, n_cols + c = cols(i) + num_nonzero_elements_in_group = num_nonzero_elements_in_group + & + me%col_ptr(c+1) - me%col_ptr(c) + end do + if (num_nonzero_elements_in_group==0) return ! no functions to compute in this group + + allocate(indices(num_nonzero_elements_in_group)) + k = 0 + do i = 1, n_cols + c = cols(i) + nnz_col = me%col_ptr(c+1) - me%col_ptr(c) + indices(k+1:k+nnz_col) = me%col_idx(me%col_ptr(c):me%col_ptr(c+1)-1) + k = k + nnz_col + end do + nonzero_rows = me%irow(indices) end subroutine columns_in_partition_group !******************************************************************************* @@ -1672,25 +1838,96 @@ end subroutine destroy_sparsity_pattern !******************************************************************************* !> -! Computes the `indices` vector in the class. +! Computes the `indices` vector in the class, and the column index +! (`col_ptr`, `col_idx`) of the sparsity pattern. +! +!@note The column index is only computed if `icol` has been populated +! (or there are no nonzero elements). It must be called again +! if `icol` is changed. - subroutine compute_indices(me) + subroutine compute_indices(me,n) implicit none class(sparsity_pattern),intent(inout) :: me + integer,intent(in) :: n !! number of columns in the jacobian integer :: i !! counter + integer :: j !! column number + integer,dimension(:),allocatable :: next !! next free position in `col_idx` for each column + if (allocated(me%indices)) deallocate(me%indices) allocate(me%indices(me%num_nonzero_elements)) - !me%indices = [(i,i=1,me%num_nonzero_elements)] do i = 1, me%num_nonzero_elements me%indices(i) = i end do + ! column index (a stable counting sort of the elements by column, + ! so the elements in each column stay in their original order): + if (allocated(me%col_ptr)) deallocate(me%col_ptr) + if (allocated(me%col_idx)) deallocate(me%col_idx) + if (.not. allocated(me%icol) .and. me%num_nonzero_elements>0) return + allocate(me%col_ptr(n+1)) + allocate(me%col_idx(me%num_nonzero_elements)) + me%col_ptr = 0 + do i = 1, me%num_nonzero_elements + j = me%icol(i) + me%col_ptr(j+1) = me%col_ptr(j+1) + 1 + end do + me%col_ptr(1) = 1 + do j = 1, n + me%col_ptr(j+1) = me%col_ptr(j+1) + me%col_ptr(j) + end do + next = me%col_ptr(1:n) + do i = 1, me%num_nonzero_elements + j = me%icol(i) + me%col_idx(next(j)) = i + next(j) = next(j) + 1 + end do + end subroutine compute_indices !******************************************************************************* +!******************************************************************************* +!> +! Computes the group index (`grp_ptr`, `grp_cols`) of the sparsity +! partition. Must be called whenever `ngrp` or `maxgrp` is changed. + + subroutine compute_group_index(me) + + implicit none + + class(sparsity_pattern),intent(inout) :: me + + integer :: j !! column number + integer :: g !! group number + integer,dimension(:),allocatable :: next !! next free position in `grp_cols` for each group + + if (allocated(me%grp_ptr)) deallocate(me%grp_ptr) + if (allocated(me%grp_cols)) deallocate(me%grp_cols) + if (.not. allocated(me%ngrp) .or. me%maxgrp<1) return + + allocate(me%grp_ptr(me%maxgrp+1)) + allocate(me%grp_cols(size(me%ngrp))) + me%grp_ptr = 0 + do j = 1, size(me%ngrp) + g = me%ngrp(j) + me%grp_ptr(g+1) = me%grp_ptr(g+1) + 1 + end do + me%grp_ptr(1) = 1 + do g = 1, me%maxgrp + me%grp_ptr(g+1) = me%grp_ptr(g+1) + me%grp_ptr(g) + end do + next = me%grp_ptr(1:me%maxgrp) + do j = 1, size(me%ngrp) + g = me%ngrp(j) + me%grp_cols(next(g)) = j + next(g) = next(g) + 1 + end do + + end subroutine compute_group_index +!******************************************************************************* + !******************************************************************************* !> ! To specify the sparsity pattern directly if it is already known. @@ -1719,47 +1956,26 @@ subroutine set_sparsity_pattern(me,irow,icol,linear_irow,linear_icol,linear_vals if (me%exception_raised) return ! check for exceptions - if (size(irow)/=size(icol) .or. any(irow>me%m) .or. any(icol>me%n)) then + if (size(irow)/=size(icol) .or. any(irow<1) .or. any(irow>me%m) .or. & + any(icol<1) .or. any(icol>me%n)) then call me%raise_exception(15,'set_sparsity_pattern',& 'invalid inputs') return - else - - me%sparsity%sparsity_computed = .true. - me%sparsity%num_nonzero_elements = size(irow) - me%sparsity%irow = irow - me%sparsity%icol = icol - - call me%sparsity%compute_indices() - if (me%partition_sparsity_pattern) then - if (present(maxgrp) .and. present(ngrp)) then - ! use the user-input partition: - if (maxgrp>0 .and. all(ngrp>=1 .and. ngrp<=maxgrp) .and. size(ngrp)==me%n) then - me%sparsity%maxgrp = maxgrp - me%sparsity%ngrp = ngrp - else - call me%raise_exception(28,'set_sparsity_pattern',& - 'invalid sparsity partition inputs.') - return - end if - else - call me%sparsity%dsm_wrapper(me%n,me%m,info) - if (info/=1) then - call me%raise_exception(16,'set_sparsity_pattern',& - 'error partitioning sparsity pattern.') - return - end if - end if - end if - end if - ! linear pattern: + me%sparsity%sparsity_computed = .true. + me%sparsity%num_nonzero_elements = size(irow) + me%sparsity%irow = irow + me%sparsity%icol = icol + call me%sparsity%compute_indices(me%n) + + ! linear pattern (this must be set before the partition is computed, + ! since the partition must also account for the linear elements): if (present(linear_irow) .and. present(linear_icol) .and. present(linear_vals)) then if (size(linear_irow)/=size(linear_icol) .or. & size(linear_vals)/=size(linear_icol) .or. & - any(linear_irow>me%m) .or. & - any(linear_icol>me%n)) then + any(linear_irow<1) .or. any(linear_irow>me%m) .or. & + any(linear_icol<1) .or. any(linear_icol>me%n)) then call me%raise_exception(17,'set_sparsity_pattern',& 'invalid linear sparsity pattern') return @@ -1772,6 +1988,35 @@ subroutine set_sparsity_pattern(me,irow,icol,linear_irow,linear_icol,linear_vals end if end if + if (me%partition_sparsity_pattern) then + if (present(maxgrp) .and. present(ngrp)) then + ! use the user-input partition: + if (maxgrp>0 .and. size(ngrp)==me%n .and. all(ngrp>=1 .and. ngrp<=maxgrp)) then + me%sparsity%maxgrp = maxgrp + me%sparsity%ngrp = ngrp + if (.not. me%sparsity%partition_is_consistent(me%m)) then + call me%raise_exception(33,'set_sparsity_pattern',& + 'the sparsity partition is not consistent with the '//& + 'sparsity pattern (two columns in the same group '//& + 'have an element in the same row).') + return + end if + call me%sparsity%compute_group_index() + else + call me%raise_exception(28,'set_sparsity_pattern',& + 'invalid sparsity partition inputs.') + return + end if + else + call me%sparsity%dsm_wrapper(me%n,me%m,info) + if (info/=1) then + call me%raise_exception(16,'set_sparsity_pattern',& + 'error partitioning sparsity pattern.') + return + end if + end if + end if + end subroutine set_sparsity_pattern !******************************************************************************* @@ -1798,8 +2043,6 @@ subroutine compute_sparsity_dense(me,x) allocate(me%sparsity%irow(me%sparsity%num_nonzero_elements)) allocate(me%sparsity%icol(me%sparsity%num_nonzero_elements)) - call me%sparsity%compute_indices() - ! create the dense matrix: i = 0 do c = 1, me%n @@ -1810,6 +2053,8 @@ subroutine compute_sparsity_dense(me,x) end do end do + call me%sparsity%compute_indices(me%n) + ! No real need for this, since it can't be partitioned (all elements are true) if (me%partition_sparsity_pattern) call me%generate_dense_sparsity_partition() @@ -1835,6 +2080,7 @@ subroutine generate_dense_sparsity_partition(me) me%sparsity%maxgrp = me%n allocate(me%sparsity%ngrp(me%n)) me%sparsity%ngrp = [(i, i=1,me%n)] + call me%sparsity%compute_group_index() end subroutine generate_dense_sparsity_partition !******************************************************************************* @@ -1940,7 +2186,7 @@ subroutine compute_sparsity_random(me,x) if (me%exception_raised) return ! check for exceptions do j = 1, me%m ! each function (rows of Jacobian) - if (equal_within_tol([f1(j),f2(j),f3(j)], me%function_precision_tol)) then + if (equal_within_tol([f1(j),f2(j),f3(j)], me%function_precision_tol, relative=.true.)) then ! no change in the function, so no sparsity element here. cycle else @@ -1951,7 +2197,7 @@ subroutine compute_sparsity_random(me,x) dfdx1 = (f1(j)-f2(j)) / (x1(i)-x2(i)) ! slope of line from 1->2 dfdx2 = (f1(j)-f3(j)) / (x1(i)-x3(i)) ! slope of line from 1->3 dfdx3 = (f1(j)-f4(j)) / (x1(i)-x4(i)) ! slope of line from 1->4 - if (equal_within_tol([dfdx1,dfdx2,dfdx3],me%linear_sparsity_tol)) then + if (equal_within_tol([dfdx1,dfdx2,dfdx3],me%linear_sparsity_tol,relative=.true.)) then ! this is a linear element (constant value) dfdx = (dfdx1 + dfdx2 + dfdx3) / 3.0_wp ! just take the average and use that call expand_vector(me%sparsity%linear_icol,n_linear_icol,me%chunk_size,val=i) @@ -1974,7 +2220,7 @@ subroutine compute_sparsity_random(me,x) end associate - call me%sparsity%compute_indices() + call me%sparsity%compute_indices(me%n) if (me%partition_sparsity_pattern) then call me%sparsity%dsm_wrapper(me%n,me%m,info) if (info/=1) then @@ -2068,9 +2314,13 @@ subroutine compute_sparsity_random_2(me,x) integer :: n_linear_icol !! `linear_icol` size counter integer :: n_linear_irow !! `linear_irow` size counter integer :: n_linear_vals !! `linear_vals` size counter - type(meth_array) :: class_meths !! set of finite diff methods to use real(wp),dimension(:),allocatable :: jac !! array of jacobian element values integer :: info !! status output form [[dsm]] + real(wp),dimension(:,:),allocatable :: f0 !! function value at each `xp` point + !! (size `m,num_sparsity_points`) + logical,dimension(:),allocatable :: f0_computed !! if `f0` has been computed for each point + real(wp),dimension(:),allocatable :: dx_col !! the perturbation used for the current + !! column at each point ! initialize: call me%destroy_sparsity_pattern() @@ -2095,7 +2345,7 @@ subroutine compute_sparsity_random_2(me,x) me%sparsity%irow = [(irow,irow=1,me%m)] me%sparsity%sparsity_computed = .true. me%sparsity%num_nonzero_elements = me%m - call me%sparsity%compute_indices() + call me%sparsity%compute_indices(me%n) if (me%partition_sparsity_pattern) call me%generate_dense_sparsity_partition() n_icol = 0 n_irow = 0 @@ -2114,8 +2364,12 @@ subroutine compute_sparsity_random_2(me,x) xp(:,i) = me%xlow_for_sparsity + (me%xhigh_for_sparsity-me%xlow_for_sparsity)*coeffs(i) end do - ! we will use 2-point methods (simple differences): - class_meths = get_all_methods_in_class(2) + ! the nominal function values at each point are + ! computed once and reused for all the columns: + allocate(f0(me%m,me%num_sparsity_points)) + allocate(f0_computed(me%num_sparsity_points)) + f0_computed = .false. + allocate(dx_col(me%num_sparsity_points)) do icol = 1, me%n ! column loop @@ -2126,7 +2380,8 @@ subroutine compute_sparsity_random_2(me,x) ! compute the ith column of the jacobian: me%sparsity%icol = [(icol, j=1,me%m)] do i = 1, me%num_sparsity_points - call me%compute_jacobian_for_sparsity( icol, class_meths, xp(:,i), jac_array(i)%jac ) + call me%compute_jacobian_for_sparsity( icol, xp(:,i), jac_array(i)%jac, & + f0(:,i), f0_computed(i), dx_col(i) ) if (me%exception_raised) return ! check for exceptions end do @@ -2138,13 +2393,16 @@ subroutine compute_sparsity_random_2(me,x) jac(j) = jac_array(j)%jac(irow) end do - ! put the results into the tmp_sparsity_pattern - if (equal_within_tol([0.0_wp,jac],me%linear_sparsity_tol)) then + ! put the results into the tmp_sparsity_pattern. + ! the element is zero if, at every point, the change in the function + ! caused by the perturbation is within the relative function precision. + ! [note: the 2-point methods always evaluate f(x), so f0 is available] + if (all(abs(jac)*dx_col <= me%function_precision_tol*abs(f0(irow,:)))) then ! they are all zero cycle else if (me%compute_linear_sparsity_pattern) then - if (equal_within_tol(jac,me%linear_sparsity_tol)) then + if (equal_within_tol(jac,me%linear_sparsity_tol,relative=.true.)) then ! this is a linear element (constant value) dfdx = sum(jac) / me%num_sparsity_points ! just take the average and use that call expand_vector(tmp_sparsity_pattern%linear_icol,n_linear_icol,me%chunk_size,val=icol) @@ -2168,7 +2426,7 @@ subroutine compute_sparsity_random_2(me,x) call me%resize_sparsity_vectors(n_icol,n_irow,n_linear_icol,& n_linear_irow,n_linear_vals) - call me%sparsity%compute_indices() + call me%sparsity%compute_indices(me%n) if (me%partition_sparsity_pattern) then call me%sparsity%dsm_wrapper(me%n,me%m,info) if (info/=1) then @@ -2313,7 +2571,7 @@ end subroutine compute_jacobian_dense ! evaluation is done, to avoid having to allocate more temporary storage. subroutine perturb_x_and_compute_f(me,x,dx_factor,dx,& - df_factor,column,idx,df) + df_factor,column,idx,df,f0,f0_computed) implicit none @@ -2329,12 +2587,28 @@ subroutine perturb_x_and_compute_f(me,x,dx_factor,dx,& real(wp),dimension(me%m),intent(inout) :: df !! the accumulated function value !! note: for the first call, this !! should be set to zero + real(wp),dimension(me%m),intent(inout),optional :: f0 !! function value at the nominal `x`, + !! reused when `dx_factor=0` + !! (see [[compute_nominal_function]]) + logical,intent(inout),optional :: f0_computed !! if `f0` has already been computed. + !! must be set to false before the first call. real(wp),dimension(me%n) :: xp !! the perturbed variable vector real(wp),dimension(me%m) :: f !! function evaluation if (me%exception_raised) return ! check for exceptions + if (dx_factor==zero .and. present(f0) .and. present(f0_computed)) then + ! reuse the nominal function value: + if (.not. f0_computed) then + call me%compute_nominal_function(x,f0) + if (me%exception_raised) return ! check for exceptions + f0_computed = .true. + end if + df(idx) = df(idx) + df_factor * f0(idx) + return + end if + xp = x if (dx_factor/=zero) xp(column) = xp(column) + dx_factor * dx(column) call me%compute_function(xp,f,idx) @@ -2344,6 +2618,30 @@ subroutine perturb_x_and_compute_f(me,x,dx_factor,dx,& end subroutine perturb_x_and_compute_f !******************************************************************************* +!******************************************************************************* +!> +! Compute the function at the nominal (unperturbed) `x` for all the rows +! in the sparsity pattern. This is done once per Jacobian evaluation +! so it can be reused by every column (or group) whose finite difference +! formula includes \( f(x) \). + + subroutine compute_nominal_function(me,x,f0) + + implicit none + + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! nominal variable vector + real(wp),dimension(me%m),intent(out) :: f0 !! function value at `x` + !! (only the rows in the + !! sparsity pattern are computed) + + if (me%exception_raised) return ! check for exceptions + + call me%compute_function(x,f0,unique(me%sparsity%irow,chunk_size=me%chunk_size)) + + end subroutine compute_nominal_function +!******************************************************************************* + !******************************************************************************* !> ! Returns the product `J*v`, where `J` is the `m x n` Jacobian matrix @@ -2427,7 +2725,16 @@ subroutine compute_jacobian(me,x,jac) ! if we don't have a sparsity pattern yet then compute it: ! [also computes the indices vector] - if (.not. me%sparsity%sparsity_computed) call me%compute_sparsity(x) + if (.not. me%sparsity%sparsity_computed) then + if (.not. associated(me%compute_sparsity)) then + ! sparsity_mode=3, but set_sparsity_pattern was not called + call me%raise_exception(30,'compute_jacobian',& + 'the sparsity pattern has not been set.') + return + end if + call me%compute_sparsity(x) + if (me%exception_raised) return ! check for exceptions + end if if (me%sparsity%num_nonzero_elements==0) return ! size the jacobian vector: @@ -2456,21 +2763,25 @@ end subroutine compute_jacobian !> ! A separate version of [[compute_jacobian]] to be used only when ! computing the sparsity pattern in [[compute_sparsity_random_2]]. -! It uses `class_meths` and the sparsity dperts and bounds. +! It uses `sparsity_class_meths` and the sparsity dperts and bounds. ! !@note Based on [[compute_jacobian]]. The index manipulation here could be ! greatly simplified, since we realdy know we are computed all the ! elements in one column. - subroutine compute_jacobian_for_sparsity(me,i,class_meths,x,jac) + subroutine compute_jacobian_for_sparsity(me,i,x,jac,f0,f0_computed,dx_col) implicit none class(numdiff_type),intent(inout) :: me integer,intent(in) :: i !! the column being computed - type(meth_array),intent(in) :: class_meths !! set of finite diff methods to use real(wp),dimension(:),intent(in) :: x !! vector of variables (size `n`) real(wp),dimension(:),allocatable,intent(out) :: jac !! sparse jacobian vector + real(wp),dimension(me%m),intent(inout) :: f0 !! function value at `x` + !! (computed on first use and + !! reused for subsequent columns) + logical,intent(inout) :: f0_computed !! if `f0` has been computed + real(wp),intent(out) :: dx_col !! the perturbation used for column `i` real(wp),dimension(me%n) :: dx !! absolute perturbation (>0) for each variable integer,dimension(:),allocatable :: nonzero_elements_in_col !! the indices of the @@ -2493,6 +2804,7 @@ subroutine compute_jacobian_for_sparsity(me,i,class_meths,x,jac) ! compute the perturbation vector (really we only need dx(i)): call me%compute_sparsity_perturbation_vector(x,dx) if (me%exception_raised) return ! check for exceptions + dx_col = dx(i) ! initialize: jac = zero @@ -2504,7 +2816,7 @@ subroutine compute_jacobian_for_sparsity(me,i,class_meths,x,jac) nonzero_elements_in_col = pack(me%sparsity%irow,mask=me%sparsity%icol==i) call me%select_finite_diff_method(x(i),me%xlow_for_sparsity(i),me%xhigh_for_sparsity(i),& - dx(i),class_meths,fd,status_ok) + dx(i),me%sparsity_class_meths,fd,status_ok) if (.not. status_ok) then if (me%print_messages) then write(error_unit,'(A,1X,I5)') & @@ -2518,7 +2830,8 @@ subroutine compute_jacobian_for_sparsity(me,i,class_meths,x,jac) if (associated(me%info_function)) call me%info_function([i],j,x) call me%perturb_x_and_compute_f(x,fd%dx_factors(j),& dx,fd%df_factors(j),& - i,nonzero_elements_in_col,df) + i,nonzero_elements_in_col,df,& + f0,f0_computed) if (me%exception_raised) return ! check for exceptions end do df(nonzero_elements_in_col) = df(nonzero_elements_in_col) / & @@ -2559,20 +2872,31 @@ subroutine compute_jacobian_standard(me,x,dx,jac) !! specifying class rather than the method) logical :: status_ok !! error flag integer :: num_nonzero_elements_in_col !! number of nonzero elements in a column + real(wp),dimension(me%m) :: f0 !! function value at the nominal `x` + logical :: f0_computed !! if `f0` has been computed if (me%exception_raised) return ! check for exceptions ! initialize: jac = zero + f0_computed = .false. + + if (.not. allocated(me%sparsity%col_ptr)) then + call me%raise_exception(31,'compute_jacobian_standard',& + 'the sparsity column index has not been computed.') + return + end if ! compute Jacobian matrix column-by-column: do i=1,me%n ! determine functions to compute for this column: - num_nonzero_elements_in_col = count(me%sparsity%icol==i) + associate (col_indices => me%sparsity%col_idx(me%sparsity%col_ptr(i):me%sparsity%col_ptr(i+1)-1)) + + num_nonzero_elements_in_col = size(col_indices) if (num_nonzero_elements_in_col/=0) then ! there are functions to compute - nonzero_elements_in_col = pack(me%sparsity%irow,mask=me%sparsity%icol==i) + nonzero_elements_in_col = me%sparsity%irow(col_indices) select case (me%mode) case(1) ! use the specified methods @@ -2583,7 +2907,8 @@ subroutine compute_jacobian_standard(me,x,dx,jac) if (associated(me%info_function)) call me%info_function([i],j,x) call me%perturb_x_and_compute_f(x,me%meth(i)%dx_factors(j),& dx,me%meth(i)%df_factors(j),& - i,nonzero_elements_in_col,df) + i,nonzero_elements_in_col,df,& + f0,f0_computed) if (me%exception_raised) return ! check for exceptions end do @@ -2607,7 +2932,8 @@ subroutine compute_jacobian_standard(me,x,dx,jac) if (associated(me%info_function)) call me%info_function([i],j,x) call me%perturb_x_and_compute_f(x,fd%dx_factors(j),& dx,fd%df_factors(j),& - i,nonzero_elements_in_col,df) + i,nonzero_elements_in_col,df,& + f0,f0_computed) if (me%exception_raised) return ! check for exceptions end do df(nonzero_elements_in_col) = df(nonzero_elements_in_col) / & @@ -2620,11 +2946,12 @@ subroutine compute_jacobian_standard(me,x,dx,jac) end select ! put result into the output vector: - jac(pack(me%sparsity%indices,mask=me%sparsity%icol==i)) = & - df(nonzero_elements_in_col) + jac(col_indices) = df(nonzero_elements_in_col) end if + end associate + end do end subroutine compute_jacobian_standard @@ -2723,9 +3050,15 @@ function dfunc(this,xval) result(fx) real(wp) :: fx !! derivative of `ir` function !! w.r.t. `xval` variable + fx = 0.0_wp + if (use_info) then icount = icount + 1 call me%info_function([ic],icount,x) + if (me%exception_raised) then ! check for exceptions + call this%terminate() ! stop diff (it will return ifail=-1) + return + end if end if xp = x @@ -2733,8 +3066,7 @@ function dfunc(this,xval) result(fx) call me%compute_function(xp,fvec,funcs_to_compute=[ir]) if (me%exception_raised) then ! check for exceptions - fx = 0.0_wp - call me%terminate() + call this%terminate() ! stop diff (it will return ifail=-1) else fx = fvec(ir) end if @@ -2767,17 +3099,18 @@ subroutine compute_jacobian_partitioned(me,x,dx,jac) integer,dimension(:),allocatable :: nonzero_rows !! the indices of the nonzero Jacobian !! elementes (row numbers) in a group integer,dimension(:),allocatable :: indices !! nonzero indices in `jac` for a group - integer,dimension(:),allocatable :: col_indices !! nonzero indices in `jac` for a column real(wp),dimension(me%m) :: df !! accumulated function type(finite_diff_method) :: fd !! a finite different method (when !! specifying class rather than the method) logical :: status_ok !! error flag - integer :: num_nonzero_elements_in_col + real(wp),dimension(me%m) :: f0 !! function value at the nominal `x` + logical :: f0_computed !! if `f0` has been computed if (me%exception_raised) return ! check for exceptions ! initialize: jac = zero + f0_computed = .false. ! compute by group: do igroup = 1, me%sparsity%maxgrp @@ -2806,28 +3139,16 @@ subroutine compute_jacobian_partitioned(me,x,dx,jac) if (associated(me%info_function)) call me%info_function(cols,j,x) call me%perturb_x_and_compute_f_partitioned(x,me%meth(1)%dx_factors(j),& dx,me%meth(1)%df_factors(j),& - cols,nonzero_rows,df) + cols,nonzero_rows,df,& + f0,f0_computed) if (me%exception_raised) return ! check for exceptions end do ! divide by the denominator, which can be different for each column: do i = 1, n_cols - num_nonzero_elements_in_col = count(me%sparsity%icol==cols(i)) - if (allocated(col_indices)) deallocate(col_indices) - allocate(col_indices(num_nonzero_elements_in_col)) - ! col_indices = pack(me%sparsity%indices,mask=me%sparsity%icol==cols(i)) - block - integer :: j,n - n = 0 - do j = 1, size(me%sparsity%icol) - if (me%sparsity%icol(j)==cols(i)) then - n = n + 1 - col_indices(n) = j - end if - end do - end block - - df(me%sparsity%irow(col_indices)) = df(me%sparsity%irow(col_indices)) / & - (me%meth(1)%df_den_factor*dx(cols(i))) + associate (rows => me%sparsity%irow(me%sparsity%col_idx(& + me%sparsity%col_ptr(cols(i)):me%sparsity%col_ptr(cols(i)+1)-1))) + df(rows) = df(rows) / (me%meth(1)%df_den_factor*dx(cols(i))) + end associate end do case(2) ! select the method from the class so as not to violate @@ -2855,17 +3176,16 @@ subroutine compute_jacobian_partitioned(me,x,dx,jac) if (associated(me%info_function)) call me%info_function(cols,j,x) call me%perturb_x_and_compute_f_partitioned(x,fd%dx_factors(j),& dx,fd%df_factors(j),& - cols,nonzero_rows,df) + cols,nonzero_rows,df,& + f0,f0_computed) if (me%exception_raised) return ! check for exceptions end do ! divide by the denominator, which can be different for each column: do i = 1, n_cols - num_nonzero_elements_in_col = count(me%sparsity%icol==cols(i)) - if (allocated(col_indices)) deallocate(col_indices) - allocate(col_indices(num_nonzero_elements_in_col)) - col_indices = pack(me%sparsity%indices,mask=me%sparsity%icol==cols(i)) - df(me%sparsity%irow(col_indices)) = df(me%sparsity%irow(col_indices)) / & - (fd%df_den_factor*dx(cols(i))) + associate (rows => me%sparsity%irow(me%sparsity%col_idx(& + me%sparsity%col_ptr(cols(i)):me%sparsity%col_ptr(cols(i)+1)-1))) + df(rows) = df(rows) / (fd%df_den_factor*dx(cols(i))) + end associate end do case default @@ -2892,7 +3212,7 @@ end subroutine compute_jacobian_partitioned ! evaluation is done, to avoid having to allocate more temporary storage. subroutine perturb_x_and_compute_f_partitioned(me,x,dx_factor,dx,& - df_factor,columns,idx,df) + df_factor,columns,idx,df,f0,f0_computed) implicit none @@ -2908,12 +3228,28 @@ subroutine perturb_x_and_compute_f_partitioned(me,x,dx_factor,dx,& real(wp),dimension(me%m),intent(inout) :: df !! the accumulated function value !! note: for the first call, this !! should be set to zero + real(wp),dimension(me%m),intent(inout),optional :: f0 !! function value at the nominal `x`, + !! reused when `dx_factor=0` + !! (see [[compute_nominal_function]]) + logical,intent(inout),optional :: f0_computed !! if `f0` has already been computed. + !! must be set to false before the first call. real(wp),dimension(me%n) :: xp !! the perturbed variable vector real(wp),dimension(me%m) :: f !! function evaluation if (me%exception_raised) return ! check for exceptions + if (dx_factor==zero .and. present(f0) .and. present(f0_computed)) then + ! reuse the nominal function value: + if (.not. f0_computed) then + call me%compute_nominal_function(x,f0) + if (me%exception_raised) return ! check for exceptions + f0_computed = .true. + end if + df(idx) = df(idx) + df_factor * f0(idx) + return + end if + xp = x if (dx_factor/=zero) xp(columns) = xp(columns) + dx_factor * dx(columns) call me%compute_function(xp,f,idx) diff --git a/src/utilities_module.f90 b/src/utilities_module.f90 index c7c6625..ae5976d 100644 --- a/src/utilities_module.f90 +++ b/src/utilities_module.f90 @@ -7,6 +7,8 @@ module numdiff_utilities_module use numdiff_kinds_module + implicit none + integer,parameter :: max_size_for_insertion_sort = 20 !! max size for using insertion sort. private @@ -48,7 +50,8 @@ pure subroutine expand_vector_int(vec,n,chunk_size,val,finished) integer,intent(inout) :: n !! counter for last element added to `vec`. !! must be initialized to `size(vec)` !! (or 0 if not allocated) before first call - integer,intent(in) :: chunk_size !! allocate `vec` in blocks of this size (>0) + integer,intent(in) :: chunk_size !! allocate `vec` in blocks of this size + !! (values <1 are treated as 1) integer,intent(in),optional :: val !! the value to add to `vec` logical,intent(in),optional :: finished !! set to true to return `vec` !! as its correct size (`n`) @@ -59,14 +62,14 @@ pure subroutine expand_vector_int(vec,n,chunk_size,val,finished) if (allocated(vec)) then if (n==size(vec)) then ! have to add another chunk: - allocate(tmp(size(vec)+chunk_size)) + allocate(tmp(size(vec)+max(1,chunk_size))) tmp(1:size(vec)) = vec call move_alloc(tmp,vec) end if n = n + 1 else ! the first element: - allocate(vec(chunk_size)) + allocate(vec(max(1,chunk_size))) n = 1 end if vec(n) = val @@ -97,7 +100,8 @@ pure subroutine expand_vector_real(vec,n,chunk_size,val,finished) integer,intent(inout) :: n !! counter for last element added to `vec`. !! must be initialized to `size(vec)` !! (or 0 if not allocated) before first call - integer,intent(in) :: chunk_size !! allocate `vec` in blocks of this size (>0) + integer,intent(in) :: chunk_size !! allocate `vec` in blocks of this size + !! (values <1 are treated as 1) real(wp),intent(in),optional :: val !! the value to add to `vec` logical,intent(in),optional :: finished !! set to true to return `vec` !! as its correct size (`n`) @@ -108,14 +112,14 @@ pure subroutine expand_vector_real(vec,n,chunk_size,val,finished) if (allocated(vec)) then if (n==size(vec)) then ! have to add another chunk: - allocate(tmp(size(vec)+chunk_size)) + allocate(tmp(size(vec)+max(1,chunk_size))) tmp(1:size(vec)) = vec call move_alloc(tmp,vec) end if n = n + 1 else ! the first element: - allocate(vec(chunk_size)) + allocate(vec(max(1,chunk_size))) n = 1 end if vec(n) = val @@ -394,7 +398,7 @@ end subroutine swap_int !******************************************************************************* !> -! Swap two integer values. +! Swap two real values. pure elemental subroutine swap_real(i1,i2) @@ -415,18 +419,31 @@ end subroutine swap_real !******************************************************************************* !> ! Returns true if the values in the array are the same -! (to within the specified absolute tolerance). +! (to within the specified absolute or relative tolerance). +! +! For the relative test, the tolerance is scaled by the largest +! magnitude in `vals`, so a set of zeros is equal, and (for `tol<1`) +! any set containing both zero and a nonzero value is not. - pure function equal_within_tol(vals,tol) result (equal) + pure function equal_within_tol(vals,tol,relative) result (equal) implicit none - real(wp),dimension(:),intent(in) :: vals !! a set of values - real(wp),intent(in) :: tol !! a positive tolerance value - logical :: equal !! true if they are equal - !! within the tolerance + real(wp),dimension(:),intent(in) :: vals !! a set of values + real(wp),intent(in) :: tol !! a positive tolerance value + logical,intent(in),optional :: relative !! if true, `tol` is relative + !! [default is false: absolute] + logical :: equal !! true if they are equal + !! within the tolerance - equal = all ( abs(vals - vals(1)) <= abs(tol) ) + real(wp) :: scale !! scale factor for the tolerance + + scale = 1.0_wp + if (present(relative)) then + if (relative) scale = maxval(abs(vals)) + end if + + equal = all ( abs(vals - vals(1)) <= abs(tol)*scale ) end function equal_within_tol !******************************************************************************* @@ -444,6 +461,11 @@ end function equal_within_tol ! 1 2 3 !``` ! returns: `[0.25308641972530865, 0.5061728394506173, 0.759259259175926]`. +! +!@note For `num_points` greater than about 80, the largest points are +! clamped to the upper bound, and the duplicates are removed, so +! fewer than `num_points` points are returned. +! [[numdiff_type]] limits `num_sparsity_points` to 50 for this reason. function divide_interval(num_points) result(points) diff --git a/test/cache_test.f90 b/test/cache_test.f90 new file mode 100644 index 0000000..df57d72 --- /dev/null +++ b/test/cache_test.f90 @@ -0,0 +1,135 @@ +!******************************************************************************* +!> +! Tests for [[function_cache]], including the output of `print`. + + program cache_test + + use iso_fortran_env, only: output_unit, error_unit + use numdiff_cache_module, only: function_cache + use numdiff_kinds_module, only: wp + + implicit none + + integer :: n_failed = 0 !! number of failed checks + + call test_uninitialized() + call test_single_slot() + call test_all_slots_printed() + + if (n_failed==0) then + write(output_unit,'(A)') 'cache_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'cache_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine print_entries(cache, entries, n_entries, text) + !! print the cache to a scratch file and return the slot + !! numbers of the entries that were printed. + type(function_cache),intent(inout) :: cache + integer,dimension(:),intent(out) :: entries !! printed slot numbers + integer,intent(out) :: n_entries !! number of entries printed + character(len=:),allocatable,intent(out) :: text !! all the printed lines (concatenated) + integer :: iunit, istat + character(len=1000) :: line + open(newunit=iunit, status='scratch', form='formatted') + call cache%print(iunit) + rewind(iunit) + n_entries = 0 + text = '' + do + read(iunit,'(A)',iostat=istat) line + if (istat/=0) exit + text = text//trim(line)//' | ' + if (line(1:5)=='Entry') then + n_entries = n_entries + 1 + read(line(6:),*) entries(n_entries) + end if + end do + close(iunit) + end subroutine print_entries + + subroutine test_uninitialized() + !! printing a cache that was never initialized + type(function_cache) :: cache + integer,dimension(10) :: entries + integer :: n_entries + character(len=:),allocatable :: text + call print_entries(cache, entries, n_entries, text) + call check(n_entries==0, 'uninitialized: no entries printed') + call check(index(text,'Cache is not initialized')>0, 'uninitialized: message printed') + end subroutine test_uninitialized + + subroutine test_single_slot() + !! a cache with one slot: every x hashes to slot 0, + !! which is the slot the off-by-one loop skipped. + type(function_cache) :: cache + integer :: i + real(wp),dimension(2) :: f + logical :: xfound + logical,dimension(2) :: ffound + integer,dimension(10) :: entries + integer :: n_entries + character(len=:),allocatable :: text + + call cache%initialize(isize=1, n=1, m=2) + call cache%get([1.5_wp], [1,2], i, f, xfound, ffound) + call check(i==0, 'single slot: index is 0') + call check(.not. xfound, 'single slot: empty cache') + call cache%put(i, [1.5_wp], [5.0_wp, 6.0_wp], [1,2]) + + call print_entries(cache, entries, n_entries, text) + call check(n_entries==1, 'single slot: one entry printed') + if (n_entries==1) call check(entries(1)==0, 'single slot: entry 0 printed') + call check(index(text,'1.5000000000000000')>0, 'single slot: x printed') + call check(index(text,'5.0000000000000000')>0 .and. & + index(text,'6.0000000000000000')>0, 'single slot: f printed') + end subroutine test_single_slot + + subroutine test_all_slots_printed() + !! fill some slots of a larger table, and check that + !! exactly the occupied slots are printed. + integer,parameter :: isize = 4 + type(function_cache) :: cache + integer :: i, k + real(wp),dimension(1) :: f + logical :: xfound + logical,dimension(1) :: ffound + logical,dimension(0:isize-1) :: occupied + integer,dimension(isize+1) :: entries + integer :: n_entries + character(len=:),allocatable :: text + + call cache%initialize(isize=isize, n=1, m=1) + occupied = .false. + do k = 1, 20 + call cache%get([real(k,wp)], [1], i, f, xfound, ffound) + call check(i>=0 .and. i=isize) return + call cache%put(i, [real(k,wp)], [real(10*k,wp)], [1]) + occupied(i) = .true. + end do + + call print_entries(cache, entries, n_entries, text) + call check(n_entries==count(occupied), 'all slots: every occupied slot printed') + do k = 1, n_entries + call check(entries(k)>=0 .and. entries(k)=0 .and. entries(k) +! Test that chunk sizes < 1 are handled (they are treated as 1) in +! [[expand_vector]], [[unique]], the function cache, and [[numdiff_type]]. + + program chunk_size_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_utilities_module, only: expand_vector, unique + use numdiff_cache_module, only: function_cache + use numdiff_kinds_module, only: wp + + implicit none + + integer,dimension(*),parameter :: chunk_sizes = [0, -3, 1, 5, 100] + integer :: n_failed = 0 !! number of failed checks + integer :: i + character(len=30) :: label + + do i = 1, size(chunk_sizes) + write(label,'(A,I0)') 'chunk_size=', chunk_sizes(i) + call test_expand_vector(chunk_sizes(i), trim(label)) + call test_unique(chunk_sizes(i), trim(label)) + call test_cache(chunk_sizes(i), trim(label)) + call test_numdiff(chunk_sizes(i), trim(label)) + end do + + if (n_failed==0) then + write(output_unit,'(A)') 'chunk_size_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'chunk_size_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine test_expand_vector(chunk_size, label) + !! add 10 elements to integer and real vectors + integer,intent(in) :: chunk_size + character(len=*),intent(in) :: label + integer,dimension(:),allocatable :: ivec + real(wp),dimension(:),allocatable :: rvec + integer :: k, ni, nr + ni = 0 + nr = 0 + do k = 1, 10 + call expand_vector(ivec,ni,chunk_size,val=k) + call expand_vector(rvec,nr,chunk_size,val=real(k,wp)) + end do + call expand_vector(ivec,ni,chunk_size,finished=.true.) + call expand_vector(rvec,nr,chunk_size,finished=.true.) + call check(size(ivec)==10 .and. all(ivec==[(k,k=1,10)]), label//': expand_vector (integer)') + call check(size(rvec)==10 .and. all(rvec==[(real(k,wp),k=1,10)]), label//': expand_vector (real)') + end subroutine test_expand_vector + + subroutine test_unique(chunk_size, label) + !! unique elements of integer and real vectors + integer,intent(in) :: chunk_size + character(len=*),intent(in) :: label + integer,dimension(:),allocatable :: iu + real(wp),dimension(:),allocatable :: ru + iu = unique([3,1,3,2,1], chunk_size) + ru = unique([3.0_wp,1.0_wp,3.0_wp,2.0_wp], chunk_size) + call check(size(iu)==3, label//': unique (integer) size') + if (size(iu)==3) call check(all(iu==[1,2,3]), label//': unique (integer) values') + call check(size(ru)==3, label//': unique (real) size') + if (size(ru)==3) call check(all(ru==[1.0_wp,2.0_wp,3.0_wp]), label//': unique (real) values') + end subroutine test_unique + + subroutine test_cache(chunk_size, label) + !! add two sets of functions for the same x (merged with unique) + integer,intent(in) :: chunk_size + character(len=*),intent(in) :: label + type(function_cache) :: cache + integer :: i + real(wp),dimension(3) :: f + logical :: xfound + logical,dimension(3) :: ffound + call cache%initialize(isize=10, n=1, m=3, chunk_size=chunk_size) + call cache%get([1.0_wp],[1,2,3],i,f,xfound,ffound) + call cache%put(i,[1.0_wp],[10.0_wp,20.0_wp,30.0_wp],[1,3]) + call cache%put(i,[1.0_wp],[10.0_wp,20.0_wp,30.0_wp],[2]) + call cache%get([1.0_wp],[1,2,3],i,f,xfound,ffound) + call check(xfound .and. all(ffound), label//': cache merges function indices') + if (all(ffound)) call check(all(f==[10.0_wp,20.0_wp,30.0_wp]), label//': cache values') + end subroutine test_cache + + subroutine test_numdiff(chunk_size, label) + !! compute a sparsity pattern (which is accumulated with expand_vector) + integer,intent(in) :: chunk_size + character(len=*),intent(in) :: label + type(numdiff_type) :: prob + integer,dimension(:),allocatable :: irow, icol + call prob%initialize(3,2,[-10.0_wp,-10.0_wp,-10.0_wp],[10.0_wp,10.0_wp,10.0_wp],& + perturb_mode=1,dpert=[1.0e-6_wp,1.0e-6_wp,1.0e-6_wp],& + problem_func=func,sparsity_mode=2,jacobian_method=3,& + chunk_size=chunk_size) + call prob%compute_sparsity_pattern([1.0_wp,2.0_wp,3.0_wp],irow,icol) + call check(.not. prob%failed(), label//': numdiff sparsity computed') + if (allocated(irow) .and. allocated(icol)) then + call check(size(irow)==3 .and. size(icol)==3, label//': numdiff sparsity size') + if (size(irow)==3 .and. size(icol)==3) & + call check(all(irow==[1,2,2]) .and. all(icol==[1,2,3]), label//': numdiff sparsity pattern') + else + call check(.false., label//': numdiff sparsity allocated') + end if + end subroutine test_numdiff + + subroutine func(me,x,f,funcs_to_compute) + !! test function: f1 depends on x1, f2 depends on x2 and x3 + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + f(1) = x(1)**2 + f(2) = x(2)*x(3) + end subroutine func + + end program chunk_size_test +!******************************************************************************* diff --git a/test/diff_test.f90 b/test/diff_test.f90 new file mode 100644 index 0000000..fc980b0 --- /dev/null +++ b/test/diff_test.f90 @@ -0,0 +1,381 @@ +!******************************************************************************* +!> +! Tests for [[diff_module]] (Neville's process for the first, second, +! and third derivatives of a function of one variable). +! +! The main checks are: +! +! * the estimated error bound is not exceeded by the actual error, +! * `ifail` is consistent with the requested tolerance, +! * invalid inputs and too-small intervals are detected, +! * a user termination (at any function evaluation) returns `ifail=-1` +! with no further function evaluations. +! +! The cases are also chosen to exercise the code paths of [[diff]]: +! symmetric and one-sided formulas (both directions), all three orders, +! absolute, relative, and minimal-error tolerances, `x0=0`, very narrow +! and very wide intervals, and the overflow guards. + + module diff_test_functions + + use numdiff_kinds_module, only: wp + use diff_module, only: diff_func + + implicit none + + integer :: ncalls = 0 !! number of function evaluations + integer :: stop_at = 0 !! if >0, call `terminate` on this function evaluation + integer :: fn = 1 !! which test function to use (see [[f]]) + + ! test function IDs: + integer,parameter :: f_exp = 1 !! `exp(x)` + integer,parameter :: f_sin = 2 !! `sin(x)` + integer,parameter :: f_const = 3 !! `7` + integer,parameter :: f_quantize = 4 !! `anint(10*x)/10` (a step function) + integer,parameter :: f_square = 5 !! `x**2` + integer,parameter :: f_linear = 6 !! `x` + integer,parameter :: f_big = 7 !! `big*x` (derivative near overflow) + integer,parameter :: f_wiggle1 = 8 !! `exp(x) + 1e-6*sin(1e3*x)` + integer,parameter :: f_wiggle2 = 9 !! `exp(x) + 1e-4*sin(1e2*x)` + integer,parameter :: f_kink = 10 !! `min(x, x_kink)` + + real(wp),parameter :: big = huge(1.0_wp) / 20.0_wp !! slope for `f_big` + real(wp),parameter :: x_kink = 1.0_wp + 45.0_wp*epsilon(1.0_wp) !! kink location for `f_kink` + + contains + + function f(me,x) result(fx) + !! the test functions + class(diff_func),intent(inout) :: me + real(wp),intent(in) :: x + real(wp) :: fx + ncalls = ncalls + 1 + select case (fn) + case(f_exp); fx = exp(x) + case(f_sin); fx = sin(x) + case(f_const); fx = 7.0_wp + case(f_quantize); fx = anint(x*10.0_wp)/10.0_wp + case(f_square); fx = x**2 + case(f_linear); fx = x + case(f_big); fx = big*x + case(f_wiggle1); fx = exp(x) + 1.0e-6_wp*sin(1.0e3_wp*x) + case(f_wiggle2); fx = exp(x) + 1.0e-4_wp*sin(1.0e2_wp*x) + case(f_kink); fx = min(x, x_kink) + case default; error stop 'invalid test function' + end select + if (stop_at>0 .and. ncalls>=stop_at) call me%terminate() + end function f + + pure function exact(ifn,iord,x) result(d) + !! the exact derivative of order `iord` of test function `ifn` at `x` + integer,intent(in) :: ifn + integer,intent(in) :: iord + real(wp),intent(in) :: x + real(wp) :: d + select case (ifn) + case(f_exp) + d = exp(x) + case(f_sin) + select case (iord) + case(1); d = cos(x) + case(2); d = -sin(x) + case default; d = -cos(x) + end select + case(f_square) + select case (iord) + case(1); d = 2.0_wp*x + case(2); d = 2.0_wp + case default; d = 0.0_wp + end select + case(f_linear, f_big) + d = 0.0_wp + if (iord==1) d = 1.0_wp + if (iord==1 .and. ifn==f_big) d = big + case(f_wiggle1) + d = exp(x) + wiggle(1.0e-6_wp, 1.0e3_wp) + case(f_wiggle2) + d = exp(x) + wiggle(1.0e-4_wp, 1.0e2_wp) + case default + d = 0.0_wp + end select + contains + pure real(wp) function wiggle(a,b) + !! derivative of `a*sin(b*x)` + real(wp),intent(in) :: a, b + select case (iord) + case(1); wiggle = a*b*cos(b*x) + case(2); wiggle = -a*b**2*sin(b*x) + case default; wiggle = -a*b**3*cos(b*x) + end select + end function wiggle + end function exact + + end module diff_test_functions +!******************************************************************************* + +!******************************************************************************* + program diff_test + + use iso_fortran_env, only: output_unit, error_unit + use numdiff_kinds_module, only: wp + use diff_module, only: diff_func + use diff_test_functions + + implicit none + + type :: test_case + !! a set of inputs for `diff` + character(len=40) :: label = '' + integer :: fn = f_exp + real(wp) :: x0 = 0.0_wp + real(wp) :: xmin = 0.0_wp + real(wp) :: xmax = 0.0_wp + real(wp) :: eps = 1.0e-9_wp + real(wp) :: accr = 0.0_wp + end type test_case + + ! machine constants used by diff (for the precision-independent narrow intervals): + integer,parameter :: inf = -minexponent(1.0_wp) - 2 + integer,parameter :: sup = maxexponent(1.0_wp) - 1 + real(wp),parameter :: twoinf = 2.0_wp**(-inf) + real(wp),parameter :: min_width_x0_zero = 128.0_wp*twoinf !! smallest half-width allowed at `x0=0` + real(wp),parameter :: minh_iord3 = 2.0_wp**(-min(inf,sup)/3) !! smallest step for `iord=3` + + integer :: n_failed = 0 !! number of failed checks + type(diff_func) :: d + type(test_case),dimension(:),allocatable :: smooth_cases, terminate_cases + integer :: i, order + + call test_invalid_inputs() ! before set_function is called + + call d%set_function(f) + + ! smooth functions: accuracy and error bound, for all orders + smooth_cases = [ & + test_case('interior, near xmin side', f_exp, 0.3_wp, -1.0_wp, 2.0_wp), & + test_case('interior, near xmax side', f_exp, 1.5_wp, -1.0_wp, 2.0_wp), & + test_case('x0 = xmin (one-sided)', f_exp, 0.0_wp, 0.0_wp, 1.0_wp), & + test_case('x0 = xmax (one-sided)', f_exp, 1.0_wp, 0.0_wp, 1.0_wp), & + test_case('x0 close to xmin', f_exp, 1.0e-3_wp, 0.0_wp, 1.0_wp), & + test_case('x0 = 0', f_exp, 0.0_wp, -1.0_wp, 1.0_wp), & + test_case('x0 = 0, close to xmin', f_exp, 0.0_wp, -1.0e-3_wp, 1.0_wp), & + test_case('x0 = 0, close to xmax', f_exp, 0.0_wp, -1.0_wp, 1.0e-3_wp), & + test_case('relative eps', f_exp, 0.3_wp, -1.0_wp, 2.0_wp, eps=-1.0e-9_wp), & + test_case('minimal error (eps=0)', f_exp, 0.3_wp, -1.0_wp, 2.0_wp, eps=0.0_wp), & + test_case('tight eps', f_exp, 0.3_wp, -1.0_wp, 2.0_wp, eps=1.0e-15_wp), & + test_case('relative accr', f_exp, 0.3_wp, -1.0_wp, 2.0_wp, accr=-1.0e-15_wp), & + test_case('absolute accr', f_exp, 0.3_wp, -1.0_wp, 2.0_wp, accr=1.0e-15_wp), & + test_case('wide interval', f_sin, 0.3_wp, -1.0e7_wp, 1.0e7_wp), & + test_case('wide interval, one-sided', f_sin, 0.0_wp, 0.0_wp, 1.0e7_wp), & + test_case('wide interval', f_square, 0.3_wp, -1.0e7_wp, 1.0e7_wp), & + test_case('large x0', f_square, 1.0e20_wp, 0.0_wp, 2.0e20_wp), & + test_case('high-frequency term', f_wiggle1, 0.3_wp, -1.0_wp, 2.0_wp), & + test_case('high-frequency term', f_wiggle2, 0.3_wp, -1.0_wp, 2.0_wp), & + test_case('high-frequency term, eps=0', f_wiggle2, 0.3_wp, -1.0_wp, 2.0_wp, eps=0.0_wp), & + test_case('narrow interval', f_exp, 1.0_wp, 1.0_wp-450.0_wp*epsilon(1.0_wp), & + 1.0_wp+450.0_wp*epsilon(1.0_wp)) ] + ! the same cases for sin(x), which has different values for each order: + do i = 1, 13 + smooth_cases = [smooth_cases, smooth_cases(i)] + smooth_cases(size(smooth_cases))%fn = f_sin + end do + do order = 1, 3 + do i = 1, size(smooth_cases) + call test_accuracy(smooth_cases(i), order) + end do + end do + + call test_special_cases() + + ! user termination at every function evaluation: + terminate_cases = [ & + smooth_cases(1:8), & ! exp(x) + smooth_cases(22:29), & ! sin(x) + test_case('wide interval', f_sin, 0.3_wp, -1.0e7_wp, 1.0e7_wp), & + test_case('wide interval, one-sided', f_sin, 0.0_wp, 0.0_wp, 1.0e7_wp), & + test_case('minimal error (eps=0)', f_exp, 0.3_wp, -1.0_wp, 2.0_wp, eps=0.0_wp), & + test_case('kink near x0', f_kink, 1.0_wp, 1.0_wp-180.0_wp*epsilon(1.0_wp), & + 1.0_wp+180.0_wp*epsilon(1.0_wp)), & + test_case('x0 = 0, minimum interval', f_linear, 0.0_wp, -1.5_wp*min_width_x0_zero, & + 1.5_wp*min_width_x0_zero) ] + do order = 1, 3 + do i = 1, size(terminate_cases) + call test_terminate(terminate_cases(i), order) + end do + end do + + if (n_failed==0) then + write(output_unit,'(A)') 'diff_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'diff_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg, tc, iord) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + type(test_case),intent(in),optional :: tc + integer,intent(in),optional :: iord + if (.not. ok) then + n_failed = n_failed + 1 + if (present(tc) .and. present(iord)) then + write(error_unit,'(A,I0,A,I0,A)') 'FAILED: '//trim(tc%label)//' (fn=', tc%fn, & + ', iord=', iord, '): '//msg + else + write(error_unit,'(A)') 'FAILED: '//msg + end if + end if + end subroutine check + + subroutine run(tc, iord, deriv, error, ifail) + !! call `diff` for a test case + type(test_case),intent(in) :: tc + integer,intent(in) :: iord + real(wp),intent(out) :: deriv, error + integer,intent(out) :: ifail + fn = tc%fn + ncalls = 0 + call d%compute_derivative(iord,tc%x0,tc%xmin,tc%xmax,tc%eps,tc%accr,deriv,error,ifail) + end subroutine run + + subroutine test_accuracy(tc, iord) + !! the actual error is within the estimated error, and + !! `ifail` is consistent with the requested tolerance. + type(test_case),intent(in) :: tc + integer,intent(in) :: iord + real(wp) :: deriv, error, tol + integer :: ifail + call run(tc, iord, deriv, error, ifail) + call check(ifail==0 .or. ifail==1, 'ifail is 0 or 1', tc, iord) + if (ifail/=0 .and. ifail/=1) return + call check(abs(deriv - exact(tc%fn,iord,tc%x0)) <= error, & + 'actual error is within the estimated error', tc, iord) + if (tc%eps < 0.0_wp) then + tol = abs(tc%eps*deriv) + else + tol = tc%eps + end if + if (tc%eps==0.0_wp) then + call check(ifail==0, 'ifail=0 when minimizing the error', tc, iord) + else if (ifail==0) then + call check(error <= tol, 'ifail=0 only if the tolerance is met', tc, iord) + else + call check(error > tol, 'ifail=1 only if the tolerance is not met', tc, iord) + end if + end subroutine test_accuracy + + subroutine test_invalid_inputs() + !! invalid inputs return `ifail=2` + real(wp) :: deriv, error + integer :: ifail + type(test_case) :: tc + tc = test_case('ok', f_exp, 0.3_wp, -1.0_wp, 2.0_wp) + call run(tc, 1, deriv, error, ifail) + call check(ifail==2, 'function not set: ifail=2') + call d%set_function(f) + call run(tc, 0, deriv, error, ifail) + call check(ifail==2, 'iord=0: ifail=2') + call run(tc, 4, deriv, error, ifail) + call check(ifail==2, 'iord=4: ifail=2') + call run(test_case('', f_exp, 0.3_wp, 2.0_wp, -1.0_wp), 1, deriv, error, ifail) + call check(ifail==2, 'xmin>xmax: ifail=2') + call run(test_case('', f_exp, 0.3_wp, 1.0_wp, 1.0_wp), 1, deriv, error, ifail) + call check(ifail==2, 'xmin=xmax: ifail=2') + call run(test_case('', f_exp, -3.0_wp, -1.0_wp, 2.0_wp), 1, deriv, error, ifail) + call check(ifail==2, 'x0xmax: ifail=2') + call check(ncalls==0, 'invalid inputs: no function evaluations') + end subroutine test_invalid_inputs + + subroutine test_special_cases() + !! constant functions, intervals that are too small, and the overflow guard + real(wp) :: deriv, error + integer :: ifail, iord + type(test_case) :: tc + + ! constant function: + do iord = 1, 3 + call run(test_case('', f_const, 0.3_wp, -1.0_wp, 2.0_wp), iord, deriv, error, ifail) + call check(ifail==0 .and. deriv==0.0_wp .and. error==0.0_wp, 'constant function') + end do + + ! the interval is too small for x0: + call run(test_case('', f_exp, 1.0_wp, 1.0_wp-epsilon(1.0_wp), 1.0_wp+epsilon(1.0_wp)), & + 1, deriv, error, ifail) + call check(ifail==3, 'interval too small: ifail=3') + call check(ncalls==0, 'interval too small: no function evaluations') + + ! the function only changes on a scale that is too large for the interval: + call run(test_case('', f_quantize, 0.33_wp, -1.0_wp, 2.0_wp), 1, deriv, error, ifail) + call check(ifail==3, 'step function: ifail=3') + + ! at x0=0, the interval is large enough for the first derivative, + ! but too small for the third: + tc = test_case('narrow interval at x0=0', f_linear, 0.0_wp, -10.0_wp*minh_iord3, & + 10.0_wp*minh_iord3) + call run(tc, 1, deriv, error, ifail) + call check(ifail==0, 'narrow interval at x0=0, iord=1: ifail=0') + call check(abs(deriv-1.0_wp) <= error, 'narrow interval at x0=0, iord=1: within error') + call run(tc, 3, deriv, error, ifail) + call check(ifail==3, 'narrow interval at x0=0, iord=3: ifail=3') + + ! the smallest interval allowed at x0=0: + tc = test_case('minimum interval at x0=0', f_linear, 0.0_wp, -1.5_wp*min_width_x0_zero, & + 1.5_wp*min_width_x0_zero) + call run(tc, 1, deriv, error, ifail) + call check(ifail==0 .or. ifail==1, 'minimum interval at x0=0: ifail is 0 or 1') + call check(abs(deriv-1.0_wp) <= error, 'minimum interval at x0=0: within error') + + ! a kink just inside the interval (the function is not smooth, + ! so only check that the result is valid): + do iord = 1, 3 + call run(test_case('', f_kink, 1.0_wp, 1.0_wp-180.0_wp*epsilon(1.0_wp), & + 1.0_wp+180.0_wp*epsilon(1.0_wp)), iord, deriv, error, ifail) + call check(ifail==1 .or. ifail==3, 'kink: ifail is 1 or 3') + end do + + ! derivative near overflow: the difference quotient is capped to + ! avoid overflow, so the result is not accurate, but it is finite + ! and the requested accuracy is reported as not met. + ! [note: the estimated error does not account for the cap] + do iord = 1, 3 + call run(test_case('', f_big, 0.5_wp, 0.0_wp, 1.0_wp), iord, deriv, error, ifail) + call check(ifail==1, 'derivative near overflow: ifail=1') + call check(abs(deriv)<=huge(1.0_wp) .and. deriv==deriv, 'derivative near overflow: finite') + end do + end subroutine test_special_cases + + subroutine test_terminate(tc, iord) + !! terminating at each function evaluation returns `ifail=-1` + !! with no further evaluations, and the next call is not affected. + type(test_case),intent(in) :: tc + integer,intent(in) :: iord + real(wp) :: deriv, error, deriv0, error0 + integer :: ifail, ifail0, k, n + logical :: ok + stop_at = 0 + call run(tc, iord, deriv0, error0, ifail0) + n = ncalls + ok = .true. + do k = 1, n + stop_at = k + call run(tc, iord, deriv, error, ifail) + if (ifail/=-1 .or. ncalls/=k) then + ok = .false. + exit + end if + end do + stop_at = 0 + call check(ok, 'terminate at every function evaluation', tc, iord) + ! a subsequent call is not affected by the termination: + call run(tc, iord, deriv, error, ifail) + call check(ifail==ifail0 .and. ncalls==n .and. deriv==deriv0 .and. error==error0, & + 'not affected by a previous termination', tc, iord) + end subroutine test_terminate + + end program diff_test +!******************************************************************************* diff --git a/test/divide_interval_test.f90 b/test/divide_interval_test.f90 new file mode 100644 index 0000000..01d8dbd --- /dev/null +++ b/test/divide_interval_test.f90 @@ -0,0 +1,111 @@ +!******************************************************************************* +!> +! Unit tests for [[divide_interval]], and for the `num_sparsity_points` +! limits in [[numdiff_type]] that keep it in its valid range. + + program divide_interval_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_utilities_module, only: divide_interval + use numdiff_kinds_module, only: wp + + implicit none + + integer :: n_failed = 0 !! number of failed checks + integer :: n !! number of points + real(wp),dimension(:),allocatable :: points + real(wp),parameter :: tol = 10.0_wp * epsilon(1.0_wp) + + ! documented example: + points = divide_interval(3) + call check(size(points)==3, 'n=3: size') + if (size(points)==3) then + call check(all(abs(points - [0.25308641972530865_wp, & + 0.5061728394506173_wp, & + 0.759259259175926_wp]) <= tol), 'n=3: documented values') + end if + + ! every allowed number of points gives that many distinct interior points: + do n = 1, max_num_sparsity_points + points = divide_interval(n) + call check(size(points)==n, 'size', n) + if (size(points)/=n) cycle + call check(all(points > tol .and. points < 1.0_wp - tol), 'points inside (0,1)', n) + if (n>1) call check(all(points(2:n) > points(1:n-1)), 'strictly increasing', n) + end do + + ! num_sparsity_points is validated when initializing: + call check_num_sparsity_points(0, expect_ok=.false.) + call check_num_sparsity_points(max_num_sparsity_points+1, expect_ok=.false.) + call check_num_sparsity_points(1, expect_ok=.true.) + call check_num_sparsity_points(max_num_sparsity_points, expect_ok=.true.) + + if (n_failed==0) then + write(output_unit,'(A)') 'divide_interval_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'divide_interval_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, name, num_points) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: name + integer,intent(in),optional :: num_points !! number of points being tested + if (.not. ok) then + n_failed = n_failed + 1 + if (present(num_points)) then + write(error_unit,'(A,I0,A)') 'FAILED: n=', num_points, ': '//name + else + write(error_unit,'(A)') 'FAILED: '//name + end if + end if + end subroutine check + + subroutine check_num_sparsity_points(num_sparsity_points, expect_ok) + !! initialize with `sparsity_mode=4` and compute the jacobian of + !! a simple function, checking that invalid values are rejected + !! and that valid values give the correct sparsity pattern. + integer,intent(in) :: num_sparsity_points + logical,intent(in) :: expect_ok + integer,parameter :: nv = 3 !! number of variables + integer,parameter :: m = 2 !! number of functions + real(wp),dimension(nv),parameter :: xlow = -10.0_wp + real(wp),dimension(nv),parameter :: xhigh = 10.0_wp + real(wp),dimension(nv),parameter :: dpert = 1.0e-5_wp + type(numdiff_type) :: prob + real(wp),dimension(:),allocatable :: jac + integer,dimension(:),allocatable :: irow, icol + character(len=40) :: label + write(label,'(A,I0)') 'num_sparsity_points=', num_sparsity_points + call prob%initialize(nv,m,xlow,xhigh,perturb_mode=1,dpert=dpert,& + problem_func=func,sparsity_mode=4,jacobian_method=3,& + num_sparsity_points=num_sparsity_points) + if (.not. expect_ok) then + call check(prob%failed(), trim(label)//': rejected') + return + end if + call check(.not. prob%failed(), trim(label)//': accepted') + call prob%compute_jacobian([1.0_wp,2.0_wp,3.0_wp],jac) + call check(.not. prob%failed(), trim(label)//': jacobian computed') + if (prob%failed()) return + ! f1 depends on x1, f2 depends on x2 and x3: + call prob%get_sparsity_pattern(irow,icol) + call check(all(irow==[1,2,2]) .and. all(icol==[1,2,3]), trim(label)//': sparsity pattern') + end subroutine check_num_sparsity_points + + subroutine func(me,x,f,funcs_to_compute) + !! test function + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + f(1) = x(1)**2 + f(2) = x(2)*x(3) + end subroutine func + + end program divide_interval_test +!******************************************************************************* diff --git a/tests/dsm_test.f90 b/test/dsm_test.f90 similarity index 100% rename from tests/dsm_test.f90 rename to test/dsm_test.f90 diff --git a/test/jacobian_times_vector_test.f90 b/test/jacobian_times_vector_test.f90 new file mode 100644 index 0000000..0637ac6 --- /dev/null +++ b/test/jacobian_times_vector_test.f90 @@ -0,0 +1,159 @@ +!******************************************************************************* +!> +! Tests for [[compute_jacobian_times_vector]]. +! +! The test function has a known Jacobian with a constant element, +! a zero column, and a zero row: +!``` +! f1 = x1**2 + 3*x2 J = [ 2*x1 3 0 0 ] +! f2 = x2*x3 [ 0 x3 x2 0 ] +! f3 = sin(x1) [ cos(x1) 0 0 0 ] +! f4 = 5 [ 0 0 0 0 ] +!``` + + program jacobian_times_vector_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_kinds_module, only: wp + + implicit none + + integer,parameter :: n = 4 !! number of variables + integer,parameter :: m = 4 !! number of functions + real(wp),dimension(n),parameter :: xlow = -10.0_wp + real(wp),dimension(n),parameter :: xhigh = 10.0_wp + real(wp),parameter :: h = epsilon(1.0_wp)**(1.0_wp/3.0_wp) !! step for central differences + real(wp),dimension(n),parameter :: dpert = h + real(wp),parameter :: tol = 100.0_wp * epsilon(1.0_wp)**(2.0_wp/3.0_wp) !! central difference accuracy + real(wp),dimension(n),parameter :: x = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp] + real(wp),dimension(n),parameter :: v = [0.5_wp, -1.0_wp, 2.0_wp, 7.0_wp] + + integer :: n_failed = 0 !! number of failed checks + type(numdiff_type) :: prob + real(wp),dimension(m) :: z + real(wp),dimension(:,:),allocatable :: jac + + ! dense sparsity pattern: + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=1,jacobian_method=3) + call compute_and_check('dense', matmul(analytic_jacobian(x),v)) + + ! computed sparsity pattern (three-point method), not partitioned and partitioned: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=2,jacobian_method=3) + call compute_and_check('sparsity_mode=2', matmul(analytic_jacobian(x),v)) + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=2,jacobian_method=3,partition_sparsity_pattern=.true.) + call compute_and_check('sparsity_mode=2, partitioned', matmul(analytic_jacobian(x),v)) + + ! computed sparsity pattern (jacobians at several points): + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=4,jacobian_method=3) + call compute_and_check('sparsity_mode=4', matmul(analytic_jacobian(x),v)) + + ! user-specified pattern, with the constant element (1,2)=3 given + ! as a linear element (it is not in the nonlinear pattern): + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=3,jacobian_method=3) + call prob%set_sparsity_pattern(irow=[1,2,2,3], icol=[1,2,3,1],& + linear_irow=[1], linear_icol=[2], linear_vals=[3.0_wp]) + call compute_and_check('user pattern + linear element', matmul(analytic_jacobian(x),v)) + + ! the dense form must give the same product: + call prob%compute_jacobian_dense(x,jac) + call check(.not. prob%failed(), 'user pattern + linear element: dense jacobian computed') + if (allocated(jac)) then + call prob%compute_jacobian_times_vector(x,v,z) + call check(all(abs(z - matmul(jac,v)) <= tol*max(1.0_wp,abs(z))), & + 'user pattern + linear element: consistent with dense jacobian') + end if + + ! only linear elements (the nonlinear pattern is empty, + ! so compute_jacobian returns no elements): + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=3,jacobian_method=3) + call prob%set_sparsity_pattern(irow=[integer ::], icol=[integer ::],& + linear_irow=[1], linear_icol=[2], linear_vals=[3.0_wp]) + call compute_and_check('linear elements only', [3.0_wp*v(2), 0.0_wp, 0.0_wp, 0.0_wp]) + + ! a zero vector gives exactly zero: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=1,jacobian_method=3) + call prob%compute_jacobian_times_vector(x,[0.0_wp,0.0_wp,0.0_wp,0.0_wp],z) + call check(.not. prob%failed(), 'zero vector: no exception') + call check(all(z==0.0_wp), 'zero vector: z is exactly zero') + + if (n_failed==0) then + write(output_unit,'(A)') 'jacobian_times_vector_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'jacobian_times_vector_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine compute_and_check(label, z_expected) + !! compute `J*v` with the current settings in `prob` + !! and compare it to the expected value. + character(len=*),intent(in) :: label + real(wp),dimension(m),intent(in) :: z_expected + integer :: i + character(len=:),allocatable :: error_msg + call prob%compute_jacobian_times_vector(x,v,z) + if (prob%failed()) then + call prob%get_error_status(error_msg=error_msg) + call check(.false., label//': exception raised: '//error_msg) + return + end if + do i = 1, m + if (abs(z(i)-z_expected(i)) > tol*max(1.0_wp,abs(z_expected(i)))) then + call check(.false., label//': wrong value for row '//achar(iachar('0')+i)) + write(error_unit,'(A,2(1X,ES24.16))') ' got, expected:', z(i), z_expected(i) + end if + end do + end subroutine compute_and_check + + pure function analytic_jacobian(x) result(jac) + !! the exact Jacobian of [[func]] + real(wp),dimension(n),intent(in) :: x + real(wp),dimension(m,n) :: jac + jac = 0.0_wp + jac(1,1) = 2.0_wp*x(1) + jac(1,2) = 3.0_wp + jac(2,2) = x(3) + jac(2,3) = x(2) + jac(3,1) = cos(x(1)) + end function analytic_jacobian + + subroutine func(me,x,f,funcs_to_compute) + !! test function + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + f(1) = x(1)**2 + 3.0_wp*x(2) + f(2) = x(2)*x(3) + f(3) = sin(x(1)) + f(4) = 5.0_wp + end subroutine func + + end program jacobian_times_vector_test +!******************************************************************************* diff --git a/test/linear_partition_test.f90 b/test/linear_partition_test.f90 new file mode 100644 index 0000000..879720d --- /dev/null +++ b/test/linear_partition_test.f90 @@ -0,0 +1,223 @@ +!******************************************************************************* +!> +! Test the partitioned Jacobian when a linear (constant) sparsity pattern +! is specified. The partition must account for the linear elements: two +! columns that share a row through a linear element cannot be perturbed +! together, even though the linear elements themselves are not computed. +! +! Test function (n=5, m=4): +!``` +! f1 = x1**2 + 3*x2 nonlinear: (1,1) linear: (1,2)=3 +! f2 = x2*x3 nonlinear: (2,2),(2,3) +! f3 = sin(x4) - 2*x1 + 0.5*x5 nonlinear: (3,4) linear: (3,1)=-2, (3,5)=0.5 +! f4 = exp(x5) + 4*x3 nonlinear: (4,5) linear: (4,3)=4 +!``` +! Using only the nonlinear pattern, just columns 2 and 3 share a row, +! so a partition of it would perturb (for example) x1 and x2 together, +! which corrupts row 1. + + program linear_partition_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_kinds_module, only: wp + + implicit none + + integer,parameter :: n = 5 !! number of variables + integer,parameter :: m = 4 !! number of functions + real(wp),dimension(n),parameter :: xlow = -10.0_wp + real(wp),dimension(n),parameter :: xhigh = 10.0_wp + real(wp),dimension(n),parameter :: x0 = [1.0_wp, 2.0_wp, 3.0_wp, 0.5_wp, 0.25_wp] + real(wp),dimension(n),parameter :: v = [0.5_wp, -1.0_wp, 2.0_wp, 7.0_wp, -3.0_wp] + + ! the sparsity pattern: + integer,dimension(*),parameter :: irow = [1,2,2,3,4] + integer,dimension(*),parameter :: icol = [1,2,3,4,5] + integer,dimension(*),parameter :: linear_irow = [1,3,3,4] + integer,dimension(*),parameter :: linear_icol = [2,1,5,3] + real(wp),dimension(*),parameter :: linear_vals = [3.0_wp,-2.0_wp,0.5_wp,4.0_wp] + + ! step sizes and tolerances for forward and central differences: + real(wp),parameter :: h_forward = sqrt(epsilon(1.0_wp)) + real(wp),parameter :: h_central = epsilon(1.0_wp)**(1.0_wp/3.0_wp) + real(wp),parameter :: tol_forward = 100.0_wp * sqrt(epsilon(1.0_wp)) + real(wp),parameter :: tol_central = 100.0_wp * epsilon(1.0_wp)**(2.0_wp/3.0_wp) + + integer :: n_failed = 0 !! number of failed checks + + ! DSM partition, specific methods: + call test_dsm_partition('forward diffs', h_forward, tol_forward, jacobian_method=1) + call test_dsm_partition('central diffs', h_central, tol_central, jacobian_method=3) + ! DSM partition, method class (selected to stay within the bounds): + call test_dsm_partition('class 3', h_central, tol_central, class=3) + + call test_user_partition() + + if (n_failed==0) then + write(output_unit,'(A)') 'linear_partition_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'linear_partition_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine initialize(prob, h, partition, jacobian_method, class) + !! initialize the problem with the user-specified (mode 3) sparsity + type(numdiff_type),intent(out) :: prob + real(wp),intent(in) :: h + logical,intent(in) :: partition + integer,intent(in),optional :: jacobian_method + integer,intent(in),optional :: class + real(wp),dimension(n) :: dpert + dpert = h + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,problem_func=func,& + sparsity_mode=3,jacobian_method=jacobian_method,class=class,& + partition_sparsity_pattern=partition) + end subroutine initialize + + subroutine check_jacobian(prob, label, tol) + !! check the dense Jacobian and J*v against the analytic values + type(numdiff_type),intent(inout) :: prob + character(len=*),intent(in) :: label + real(wp),intent(in) :: tol + real(wp),dimension(:,:),allocatable :: jac + real(wp),dimension(m,n) :: jac_exact + real(wp),dimension(m) :: z + integer :: r, c + character(len=:),allocatable :: error_msg + character(len=100) :: element + jac_exact = analytic_jacobian(x0) + call prob%compute_jacobian_dense(x0,jac) + if (prob%failed()) then + call prob%get_error_status(error_msg=error_msg) + call check(.false., label//': exception: '//error_msg) + return + end if + do c = 1, n + do r = 1, m + if (abs(jac(r,c)-jac_exact(r,c)) > tol*max(1.0_wp,abs(jac_exact(r,c)))) then + write(element,'(A,I0,A,I0,A,ES12.4,A,ES12.4)') ' element (',r,',',c,'): got ', & + jac(r,c), ', expected ', jac_exact(r,c) + call check(.false., label//':'//trim(element)) + end if + end do + end do + call prob%compute_jacobian_times_vector(x0,v,z) + call check(all(abs(z - matmul(jac_exact,v)) <= tol*max(1.0_wp,abs(z))), label//': J*v') + end subroutine check_jacobian + + subroutine test_dsm_partition(label, h, tol, jacobian_method, class) + !! partition computed by DSM + character(len=*),intent(in) :: label + real(wp),intent(in) :: h, tol + integer,intent(in),optional :: jacobian_method, class + type(numdiff_type) :: prob, prob_unpartitioned + real(wp),dimension(:),allocatable :: jac, jac_unpartitioned + integer,dimension(:),allocatable :: ngrp, irow_out, icol_out + integer :: maxgrp + + call initialize(prob, h, .true., jacobian_method, class) + call prob%set_sparsity_pattern(irow,icol,linear_irow,linear_icol,linear_vals) + call check(.not. prob%failed(), label//': set_sparsity_pattern') + call check_jacobian(prob, label//' [partitioned]', tol) + + ! the partition must group some columns (otherwise this test is not + ! testing anything), and must be consistent with the full pattern: + call prob%get_sparsity_pattern(irow_out,icol_out,maxgrp=maxgrp,ngrp=ngrp) + call check(maxgrp +! Tests for the [[numdiff_type]] options that are not covered elsewhere: +! +! * per-variable methods (`jacobian_methods`) and classes (`classes`) +! * perturbation modes 2 and 3, and [[set_dpert]] +! * selecting methods within the variable bounds (including the case +! where no method in the class fits), with and without partitioning +! * separate sparsity bounds, `dpert_for_sparsity` and `sparsity_perturb_mode` +! * the [[diff]] method when `x` is outside the bounds +! * functions that do not depend on `x` (empty sparsity pattern) +! +! The test function records the perturbation actually applied to each +! variable, and whether any evaluation was outside the bounds. + + module options_test_functions + + use numerical_differentiation_module, only: numdiff_type + use numdiff_kinds_module, only: wp + + implicit none + + integer,parameter :: n = 3 !! number of variables + integer,parameter :: m = 3 !! number of functions + + real(wp),dimension(n) :: x_nominal = 0.0_wp !! the nominal point (for recording the steps) + real(wp),dimension(n) :: step = 0.0_wp !! largest perturbation of each variable from `x_nominal` + real(wp),dimension(n) :: x_min = 0.0_wp !! smallest value of each variable evaluated + real(wp),dimension(n) :: x_max = 0.0_wp !! largest value of each variable evaluated + logical :: constant = .false. !! if true, the function does not depend on `x` + + contains + + subroutine reset(x) + !! reset the recorded values + real(wp),dimension(n),intent(in) :: x + x_nominal = x + step = 0.0_wp + x_min = huge(1.0_wp) + x_max = -huge(1.0_wp) + end subroutine reset + + subroutine func(me,x,f,funcs_to_compute) + !! the test function: + !!``` + !! f1 = x1**2 + x2 + !! f2 = sin(x2)*x3 + !! f3 = exp(x3) + x1*x3 + !!``` + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + step = max(step, abs(x - x_nominal)) + x_min = min(x_min, x) + x_max = max(x_max, x) + ! only the requested functions are computed (the others are zero): + f = 0.0_wp + if (constant) then + if (any(funcs_to_compute==1)) f(1) = 1.0_wp + if (any(funcs_to_compute==2)) f(2) = 2.0_wp + if (any(funcs_to_compute==3)) f(3) = 3.0_wp + else + if (any(funcs_to_compute==1)) f(1) = x(1)**2 + x(2) + if (any(funcs_to_compute==2)) f(2) = sin(x(2))*x(3) + if (any(funcs_to_compute==3)) f(3) = exp(x(3)) + x(1)*x(3) + end if + end subroutine func + + pure function analytic_jacobian(x) result(jac) + !! the exact Jacobian of [[func]] + real(wp),dimension(n),intent(in) :: x + real(wp),dimension(m,n) :: jac + jac = 0.0_wp + jac(1,1) = 2.0_wp*x(1) + jac(1,2) = 1.0_wp + jac(2,2) = cos(x(2))*x(3) + jac(2,3) = sin(x(2)) + jac(3,1) = x(3) + jac(3,3) = exp(x(3)) + x(1) + end function analytic_jacobian + + end module options_test_functions +!******************************************************************************* + +!******************************************************************************* + program options_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_kinds_module, only: wp + use options_test_functions + + implicit none + + real(wp),parameter :: eps = epsilon(1.0_wp) + real(wp),dimension(n),parameter :: xlow = -10.0_wp + real(wp),dimension(n),parameter :: xhigh = 10.0_wp + real(wp),dimension(n),parameter :: x0 = [1.0_wp, 2.0_wp, 0.5_wp] + + ! step sizes for each column: forward, central, 5-point central + real(wp),dimension(n),parameter :: dpert_mixed = [sqrt(eps), eps**(1.0_wp/3.0_wp), eps**(1.0_wp/5.0_wp)] + ! tolerances for each column: + real(wp),dimension(n),parameter :: tol_mixed = 1000.0_wp * [sqrt(eps), eps**(2.0_wp/3.0_wp), eps**(4.0_wp/5.0_wp)] + ! for central differences: + real(wp),parameter :: h_central = eps**(1.0_wp/3.0_wp) + real(wp),parameter :: tol_central = 1000.0_wp * eps**(2.0_wp/3.0_wp) + + integer :: n_failed = 0 !! number of failed checks + + call test_jacobian_methods_and_classes() + call test_perturb_modes() + call test_set_dpert() + call test_bounds() + call test_sparsity_options() + call test_diff_outside_bounds() + call test_constant_function() + call test_cache() + + if (n_failed==0) then + write(output_unit,'(A)') 'options_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'options_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine check_jacobian(prob, x, tol, label, jac) + !! compute the dense jacobian and compare it to the analytic one. + !! `tol` is the tolerance for each column. + type(numdiff_type),intent(inout) :: prob + real(wp),dimension(n),intent(in) :: x + real(wp),dimension(n),intent(in) :: tol + character(len=*),intent(in) :: label + real(wp),dimension(:,:),allocatable,intent(out),optional :: jac + real(wp),dimension(:,:),allocatable :: j + real(wp),dimension(m,n) :: jexact + integer :: c + character(len=:),allocatable :: error_msg + call reset(x) + call prob%compute_jacobian_dense(x,j) + if (prob%failed()) then + call prob%get_error_status(error_msg=error_msg) + call check(.false., label//': exception: '//error_msg) + return + end if + jexact = analytic_jacobian(x) + do c = 1, n + call check(all(abs(j(:,c)-jexact(:,c)) <= tol(c)*max(1.0_wp,abs(jexact(:,c)))), & + label//': column '//achar(iachar('0')+c)) + end do + if (present(jac)) jac = j + end subroutine check_jacobian + + subroutine test_jacobian_methods_and_classes() + !! per-variable methods and classes + type(numdiff_type) :: prob + real(wp),dimension(:,:),allocatable :: jac_methods, jac_classes + integer :: istat + + ! forward, central, and 5-point central: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert_mixed,func,sparsity_mode=1,& + jacobian_methods=[1,3,10]) + call check_jacobian(prob, x0, tol_mixed, 'jacobian_methods', jac_methods) + + ! the first method in each of these classes (when within the bounds) + ! is the same as above, so the result must be identical: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert_mixed,func,sparsity_mode=1,& + classes=[2,3,5]) + call check_jacobian(prob, x0, tol_mixed, 'classes', jac_classes) + if (allocated(jac_methods) .and. allocated(jac_classes)) & + call check(all(jac_methods==jac_classes), 'classes: same as the equivalent jacobian_methods') + + ! these can't be used with a partitioned sparsity pattern: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert_mixed,func,sparsity_mode=1,& + jacobian_methods=[1,3,10],partition_sparsity_pattern=.true.) + call prob%get_error_status(istat=istat) + call check(istat==10, 'jacobian_methods with partitioning: exception 10') + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert_mixed,func,sparsity_mode=1,& + classes=[2,3,5],partition_sparsity_pattern=.true.) + call prob%get_error_status(istat=istat) + call check(istat==11, 'classes with partitioning: exception 11') + end subroutine test_jacobian_methods_and_classes + + subroutine test_perturb_modes() + !! the perturbation applied for each perturb_mode + type(numdiff_type) :: prob + real(wp),dimension(n),parameter :: x = [0.0_wp, 2.0_wp, -0.5_wp] ! includes a zero + real(wp),dimension(n) :: dpert, dx + integer :: mode + + dpert = h_central + do mode = 1, 3 + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,mode,dpert,func,sparsity_mode=1,jacobian_method=3) + call check_jacobian(prob, x, [tol_central,tol_central,tol_central], & + 'perturb_mode='//achar(iachar('0')+mode)) + select case (mode) + case(1); dx = dpert + case(2); dx = abs(dpert*x) + where (dx < eps) dx = dpert ! x=0 falls back to dpert + case(3); dx = dpert*(1.0_wp + abs(x)) + end select + call check(all(abs(step-dx) <= 10.0_wp*eps*max(1.0_wp,abs(x))), & + 'perturb_mode='//achar(iachar('0')+mode)//': perturbation') + end do + end subroutine test_perturb_modes + + subroutine test_set_dpert() + !! change the perturbation after initializing + type(numdiff_type) :: prob + real(wp),dimension(n),parameter :: dpert2 = [2.0e-5_wp, 3.0e-5_wp, 4.0e-5_wp] + integer :: istat + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,[1.0e-5_wp,1.0e-5_wp,1.0e-5_wp],func,& + sparsity_mode=1,jacobian_method=3) + call prob%set_dpert(-dpert2) ! the sign is ignored + call check_jacobian(prob, x0, [1.0e-6_wp,1.0e-6_wp,1.0e-6_wp], 'set_dpert') + call check(all(abs(step-dpert2) <= 10.0_wp*eps*max(1.0_wp,abs(x0))), 'set_dpert: perturbation') + call prob%set_dpert([1.0e-5_wp]) + call prob%get_error_status(istat=istat) + call check(istat==29, 'set_dpert: wrong size: exception 29') + end subroutine test_set_dpert + + subroutine test_bounds() + !! selecting a method within the variable bounds (class mode) + type(numdiff_type) :: prob + real(wp),dimension(n) :: dpert, x, lo, hi, tol + logical :: partition + integer :: i + + dpert = h_central + tol = tol_central + do i = 1, 2 + partition = i==2 + + ! x1 at the upper bound, x3 at the lower bound: a one-sided + ! method must be selected so the bounds are not violated: + x = [xhigh(1), x0(2), xlow(3)] + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,class=3,& + partition_sparsity_pattern=partition) + call check_jacobian(prob, x, tol, 'x on the bounds, partition='//merge('T','F',partition)) + call check(all(x_min>=xlow) .and. all(x_max<=xhigh), & + 'x on the bounds: no evaluations outside the bounds, partition='//merge('T','F',partition)) + + ! the interval for x2 is narrower than the perturbation, so + ! no method in the class fits. The first method is used + ! (with a warning), so the bounds are exceeded, but the + ! result is still correct (the function is defined there): + lo = xlow + hi = xhigh + lo(2) = x0(2) - 0.5_wp*dpert(2) + hi(2) = x0(2) + 0.5_wp*dpert(2) + call prob%destroy() + call prob%initialize(n,m,lo,hi,1,dpert,func,sparsity_mode=1,class=3,& + partition_sparsity_pattern=partition) + write(error_unit,'(A)') '[the following bounds warning is expected]' + call check_jacobian(prob, x0, tol, 'no method fits, partition='//merge('T','F',partition)) + call check(x_min(2)hi(2), & + 'no method fits: the first method is used, partition='//merge('T','F',partition)) + end do + end subroutine test_bounds + + subroutine test_sparsity_options() + !! separate bounds and perturbations for computing the sparsity + type(numdiff_type) :: prob + real(wp),dimension(n),parameter :: xlow_s = [0.5_wp, 1.5_wp, 0.25_wp] + real(wp),dimension(n),parameter :: xhigh_s = [1.5_wp, 2.5_wp, 0.75_wp] + real(wp),dimension(n),parameter :: dpert_s = [1.0e-4_wp, 2.0e-4_wp, 3.0e-4_wp] + integer,dimension(:),allocatable :: irow, icol + integer :: istat + + ! sparsity_mode=2 samples within the sparsity bounds: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,[h_central,h_central,h_central],func,& + sparsity_mode=2,jacobian_method=3,& + xlow_for_sparsity=xlow_s,xhigh_for_sparsity=xhigh_s) + call reset(x0) + call prob%compute_sparsity_pattern(x0,irow,icol) + call check(.not. prob%failed(), 'sparsity bounds (mode 2): no exception') + call check(all(x_min>=xlow_s) .and. all(x_max<=xhigh_s), 'sparsity bounds (mode 2): samples within bounds') + call check_pattern(irow, icol, 'sparsity bounds (mode 2)') + + ! sparsity_mode=4 with a separate perturbation for the sparsity: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,[h_central,h_central,h_central],func,& + sparsity_mode=4,jacobian_method=3,& + xlow_for_sparsity=xlow_s,xhigh_for_sparsity=xhigh_s,& + dpert_for_sparsity=dpert_s,sparsity_perturb_mode=1) + call reset(x0) + call prob%compute_sparsity_pattern(x0,irow,icol) + call check(.not. prob%failed(), 'sparsity dpert (mode 4): no exception') + call check(all(x_min>=xlow_s) .and. all(x_max<=xhigh_s), 'sparsity dpert (mode 4): samples within bounds') + call check_pattern(irow, icol, 'sparsity dpert (mode 4)') + ! each variable is perturbed by dpert_s from each sample point, and the + ! sample points are strictly inside the bounds, so: + call check(all(x_max - x_min > dpert_s), 'sparsity dpert (mode 4): perturbation used') + + ! invalid inputs: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,[h_central,h_central,h_central],func,& + sparsity_mode=2,jacobian_method=3,& + xlow_for_sparsity=xhigh_s,xhigh_for_sparsity=xlow_s) + call prob%get_error_status(istat=istat) + call check(istat==7, 'sparsity bounds reversed: exception 7') + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,[h_central,h_central,h_central],func,& + sparsity_mode=4,jacobian_method=3,sparsity_perturb_mode=4) + call prob%get_error_status(istat=istat) + call check(istat==14, 'invalid sparsity_perturb_mode: exception 14') + end subroutine test_sparsity_options + + subroutine check_pattern(irow, icol, label) + !! check the sparsity pattern of [[func]] (in column order) + integer,dimension(:),allocatable,intent(in) :: irow, icol + character(len=*),intent(in) :: label + if (.not. allocated(irow) .or. .not. allocated(icol)) then + call check(.false., label//': pattern computed') + else if (size(irow)/=6 .or. size(icol)/=6) then + call check(.false., label//': pattern size') + else + call check(all(irow==[1,3,1,2,2,3]) .and. all(icol==[1,1,2,2,3,3]), label//': pattern') + end if + end subroutine check_pattern + + subroutine test_diff_outside_bounds() + !! the diff method requires x to be within the bounds + type(numdiff_type) :: prob + real(wp),dimension(:),allocatable :: jac + integer :: istat + call prob%destroy() + call prob%diff_initialize(n,m,xlow,xhigh,func,sparsity_mode=1) + call check_jacobian(prob, x0, [1.0e-8_wp,1.0e-8_wp,1.0e-8_wp], 'diff') + call prob%compute_jacobian([xhigh(1)+1.0_wp, x0(2), x0(3)],jac) + call prob%get_error_status(istat=istat) + call check(istat==24, 'diff with x outside the bounds: exception 24') + end subroutine test_diff_outside_bounds + + subroutine test_constant_function() + !! a function that does not depend on x gives a zero jacobian, + !! with and without partitioning, for each sparsity mode. + type(numdiff_type) :: prob + real(wp),dimension(:,:),allocatable :: jac + integer :: mode, i + logical :: partition + character(len=60) :: label + constant = .true. + do i = 1, 2 + partition = i==2 + do mode = 1, 4 + write(label,'(A,I0,A,L1)') 'constant function, sparsity_mode=', mode, ', partition=', partition + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,[h_central,h_central,h_central],func,& + sparsity_mode=mode,jacobian_method=3,& + partition_sparsity_pattern=partition) + if (mode==3) call prob%set_sparsity_pattern([integer ::],[integer ::]) + call prob%compute_jacobian_dense(x0,jac) + call check(.not. prob%failed(), trim(label)//': no exception') + if (allocated(jac)) call check(all(jac==0.0_wp), trim(label)//': zero jacobian') + end do + end do + constant = .false. + end subroutine test_constant_function + + subroutine test_cache() + !! the function cache must not change the results. + !! + !! At the second point, x1 and x2 are so large that the perturbation + !! does not change them, so columns 1 and 2 are both evaluated at + !! the nominal point, with different rows (column 1: rows 1,3; + !! column 2: rows 1,2). This gives a partial cache hit. + type(numdiff_type) :: prob, prob_cached + real(wp),dimension(:),allocatable :: jac, jac_cached + real(wp),dimension(n,2) :: x + integer :: i + x(:,1) = x0 + x(:,2) = [1.0e20_wp, 1.0e20_wp, 1.0_wp] + do i = 1, 2 + call prob%destroy() + call prob%initialize(n,m,-2.0e20_wp*[1,1,1],2.0e20_wp*[1,1,1],1,[h_central,h_central,h_central],& + func,sparsity_mode=3,jacobian_method=3) + call prob%set_sparsity_pattern([1,3,1,2,2,3],[1,1,2,2,3,3]) + call prob_cached%destroy() + call prob_cached%initialize(n,m,-2.0e20_wp*[1,1,1],2.0e20_wp*[1,1,1],1,[h_central,h_central,h_central],& + func,sparsity_mode=3,jacobian_method=3,cache_size=100) + call prob_cached%set_sparsity_pattern([1,3,1,2,2,3],[1,1,2,2,3,3]) + call prob%compute_jacobian(x(:,i),jac) + call prob_cached%compute_jacobian(x(:,i),jac_cached) + call check(.not. prob%failed() .and. .not. prob_cached%failed(), 'cache: no exception') + if (allocated(jac) .and. allocated(jac_cached)) & + call check(all(jac==jac_cached), 'cache: same result as without the cache, point '//achar(iachar('0')+i)) + end do + end subroutine test_cache + + end program options_test +!******************************************************************************* diff --git a/test/sparsity_scaling_test.f90 b/test/sparsity_scaling_test.f90 new file mode 100644 index 0000000..1752609 --- /dev/null +++ b/test/sparsity_scaling_test.f90 @@ -0,0 +1,82 @@ +!******************************************************************************* +!> +! Test that the estimated sparsity pattern does not depend on the +! scale of the functions (the sparsity tolerances are relative). + + program sparsity_scaling_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_kinds_module, only: wp + + implicit none + + integer,parameter :: n = 3 !! number of variables + integer,parameter :: m = 2 !! number of functions + real(wp),dimension(n),parameter :: xlow = -10.0_wp + real(wp),dimension(n),parameter :: xhigh = 10.0_wp + real(wp),dimension(n),parameter :: dpert = 1.0e-5_wp + real(wp),dimension(n),parameter :: x = [1.0_wp,2.0_wp,3.0_wp] + integer,dimension(*),parameter :: sparsity_modes = [2,4] + real(wp),dimension(*),parameter :: scales = [1.0e-20_wp, 1.0_wp, 1.0e20_wp] + + real(wp) :: scale !! the function scale factor (used in [[func]]) + integer :: n_failed = 0 !! number of failed checks + integer :: i, j + type(numdiff_type) :: prob + integer,dimension(:),allocatable :: irow, icol + character(len=100) :: label + + do i = 1, size(sparsity_modes) + do j = 1, size(scales) + + scale = scales(j) + write(label,'(A,I0,A,ES8.1)') 'sparsity_mode=',sparsity_modes(i),', scale=',scale + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,perturb_mode=1,dpert=dpert,& + problem_func=func,sparsity_mode=sparsity_modes(i),& + jacobian_method=3) + call prob%compute_sparsity_pattern(x,irow,icol) + + if (prob%failed()) then + call fail(trim(label)//': exception raised') + else if (.not. allocated(irow) .or. .not. allocated(icol)) then + call fail(trim(label)//': no sparsity pattern') + else if (size(irow)/=3 .or. size(icol)/=3) then + call fail(trim(label)//': wrong number of elements') + else if (any(irow/=[1,2,2]) .or. any(icol/=[1,2,3])) then + call fail(trim(label)//': wrong sparsity pattern') + end if + + end do + end do + + if (n_failed==0) then + write(output_unit,'(A)') 'sparsity_scaling_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'sparsity_scaling_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine fail(msg) + !! record a failed check + character(len=*),intent(in) :: msg + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end subroutine fail + + subroutine func(me,x,f,funcs_to_compute) + !! test function: f1 depends on x1, f2 depends on x2 and x3. + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + f(1) = scale * x(1)**2 + f(2) = scale * x(2)*x(3) + end subroutine func + + end program sparsity_scaling_test +!******************************************************************************* diff --git a/test/terminate_test.f90 b/test/terminate_test.f90 new file mode 100644 index 0000000..285bb33 --- /dev/null +++ b/test/terminate_test.f90 @@ -0,0 +1,168 @@ +!******************************************************************************* +!> +! Test that [[terminate]] stops the computation immediately, when it is +! called from the user function or from the info function, at any point +! in the computation (including during the sparsity computation), for +! both the finite difference methods and the [[diff]] method. + + program terminate_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_kinds_module, only: wp + + implicit none + + integer,parameter :: n = 3 !! number of variables + integer,parameter :: m = 2 !! number of functions + real(wp),dimension(n),parameter :: xlow = -10.0_wp + real(wp),dimension(n),parameter :: xhigh = 10.0_wp + real(wp),dimension(n),parameter :: dpert = 1.0e-6_wp + real(wp),dimension(n),parameter :: x0 = [1.0_wp, 2.0_wp, 3.0_wp] + integer,parameter :: n_configs = 6 !! number of configurations to test + + integer :: n_failed = 0 !! number of failed checks + integer :: nf = 0 !! number of function evaluations + integer :: ni = 0 !! number of info function calls + integer :: stop_f = 0 !! terminate in the function on this evaluation (if >0) + integer :: stop_i = 0 !! terminate in the info function on this call (if >0) + integer :: nf_at_stop = 0 !! value of `nf` when terminate was called + integer :: iconfig + + do iconfig = 1, n_configs + call test_config(iconfig) + end do + + if (n_failed==0) then + write(output_unit,'(A)') 'terminate_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'terminate_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine setup(prob, iconfig, label) + !! initialize the problem for a configuration + type(numdiff_type),intent(out) :: prob + integer,intent(in) :: iconfig + character(len=:),allocatable,intent(out) :: label + select case (iconfig) + case(1) + label = 'diff' + call prob%diff_initialize(n,m,xlow,xhigh,func,sparsity_mode=1,info=info) + case(2) + label = 'diff with cache' + call prob%diff_initialize(n,m,xlow,xhigh,func,sparsity_mode=1,info=info,cache_size=100) + case(3) + label = 'central diffs, dense' + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,& + jacobian_method=3,info=info) + case(4) + label = 'forward diffs, sparsity_mode=2, cache' + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=2,& + jacobian_method=1,info=info,cache_size=100) + case(5) + label = 'class 3, sparsity_mode=4, partitioned' + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=4,& + class=3,info=info,partition_sparsity_pattern=.true.) + case(6) + label = 'diff, sparsity_mode=4' + call prob%diff_initialize(n,m,xlow,xhigh,func,sparsity_mode=4,info=info,& + dpert_for_sparsity=dpert,sparsity_perturb_mode=1) + end select + end subroutine setup + + subroutine test_config(iconfig) + !! terminate at every function evaluation and every info call + integer,intent(in) :: iconfig + type(numdiff_type) :: prob + character(len=:),allocatable :: label + real(wp),dimension(:),allocatable :: jac + integer :: nf_total, ni_total, k, istat + character(len=:),allocatable :: error_msg + logical :: ok_f, ok_i + + ! normal run: + stop_f = 0; stop_i = 0; nf = 0; ni = 0 + call setup(prob, iconfig, label) + call prob%compute_jacobian(x0,jac) + call check(.not. prob%failed(), label//': normal run') + nf_total = nf + ni_total = ni + + ! terminate in the function: + ok_f = .true. + do k = 1, nf_total + stop_f = k; stop_i = 0; nf = 0; ni = 0; nf_at_stop = -1 + call setup(prob, iconfig, label) + call prob%compute_jacobian(x0,jac) + call prob%get_error_status(istat=istat,error_msg=error_msg) + if (.not. prob%failed() .or. istat/=-1 .or. nf/=nf_at_stop) then + ok_f = .false. + write(error_unit,'(A,I0,A,I0,A,I0)') ' '//label//': terminated at evaluation ', k, & + ', istat=', istat, ', evaluations=', nf + exit + end if + end do + call check(ok_f, label//': terminate in the function stops all evaluations') + + ! terminate in the info function: + ok_i = ni_total>0 + do k = 1, ni_total + stop_f = 0; stop_i = k; nf = 0; ni = 0; nf_at_stop = -1 + call setup(prob, iconfig, label) + call prob%compute_jacobian(x0,jac) + call prob%get_error_status(istat=istat) + if (.not. prob%failed() .or. istat/=-1 .or. nf/=nf_at_stop) then + ok_i = .false. + write(error_unit,'(A,I0,A,I0,A,I0,A,I0,A)') ' '//label//': terminated at info call ', k, & + ', istat=', istat, ', evaluations=', nf, ' (expected ', nf_at_stop, ')' + exit + end if + end do + call check(ok_i, label//': terminate in the info function stops all evaluations') + + call check(error_msg=='Terminated by the user', label//': error message') + end subroutine test_config + + subroutine func(me,x,f,funcs_to_compute) + !! test function + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + nf = nf + 1 + f(1) = x(1)**2 + 3.0_wp*x(2) + f(2) = x(2)*x(3) + if (nf==stop_f) then + nf_at_stop = nf + call me%terminate() + end if + end subroutine func + + subroutine info(me,column,i,x) + !! info function + class(numdiff_type),intent(inout) :: me + integer,dimension(:),intent(in) :: column + integer,intent(in) :: i + real(wp),dimension(:),intent(in) :: x + ni = ni + 1 + if (ni==stop_i) then + nf_at_stop = nf + call me%terminate() + end if + end subroutine info + + end program terminate_test +!******************************************************************************* diff --git a/tests/test1.f90 b/test/test1.f90 similarity index 100% rename from tests/test1.f90 rename to test/test1.f90 diff --git a/tests/test2.f90 b/test/test2.f90 similarity index 100% rename from tests/test2.f90 rename to test/test2.f90 diff --git a/test/validation_test.f90 b/test/validation_test.f90 new file mode 100644 index 0000000..4d7cd2d --- /dev/null +++ b/test/validation_test.f90 @@ -0,0 +1,273 @@ +!******************************************************************************* +!> +! Tests for the input validation in [[numdiff_type]] (each invalid +! input raises the expected exception), and for the output routines +! (formulas, printing, and returning the sparsity pattern). + + program validation_test + + use iso_fortran_env, only: output_unit, error_unit + use numerical_differentiation_module + use numdiff_kinds_module, only: wp + + implicit none + + integer,parameter :: n = 3 !! number of variables + integer,parameter :: m = 2 !! number of functions + real(wp),dimension(n),parameter :: xlow = -10.0_wp + real(wp),dimension(n),parameter :: xhigh = 10.0_wp + real(wp),dimension(n),parameter :: dpert = 1.0e-6_wp + real(wp),dimension(n),parameter :: x0 = [1.0_wp, 2.0_wp, 3.0_wp] + + integer :: n_failed = 0 !! number of failed checks + + call test_initialize_errors() + call test_other_errors() + call test_error_status() + call test_formulas() + call test_sparsity_output() + + if (n_failed==0) then + write(output_unit,'(A)') 'validation_test: all tests passed' + else + write(error_unit,'(A,I0,A)') 'validation_test: ', n_failed, ' check(s) failed' + error stop 1 + end if + +contains + + subroutine check(ok, msg) + !! record the result of a check + logical,intent(in) :: ok + character(len=*),intent(in) :: msg + if (.not. ok) then + n_failed = n_failed + 1 + write(error_unit,'(A)') 'FAILED: '//msg + end if + end subroutine check + + subroutine check_error(prob, istat_expected, label, msg_contains) + !! check that an exception with the expected code was raised, + !! and that the Jacobian is then not computed. + type(numdiff_type),intent(inout) :: prob + integer,intent(in) :: istat_expected + character(len=*),intent(in) :: label + character(len=*),intent(in),optional :: msg_contains !! text expected in the message + integer :: istat + character(len=:),allocatable :: error_msg + real(wp),dimension(:),allocatable :: jac + character(len=60) :: codes + call prob%get_error_status(istat=istat, error_msg=error_msg) + write(codes,'(A,I0,A,I0)') ' (got ', istat, ', expected ', istat_expected + call check(prob%failed() .and. istat==istat_expected, label//trim(codes)//')') + if (present(msg_contains)) & + call check(index(error_msg,msg_contains)>0, label//': message: '//error_msg) + ! nothing is computed after an exception: + call prob%compute_jacobian(x0,jac) + call check(.not. allocated(jac), label//': no jacobian after an exception') + call prob%get_error_status(istat=istat) + call check(istat==istat_expected, label//': exception is preserved') + end subroutine check_error + + subroutine test_initialize_errors() + !! invalid inputs to initialize and diff_initialize + type(numdiff_type) :: prob + real(wp),dimension(n) :: lo + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,jacobian_method=45) + call check_error(prob, 8, 'invalid jacobian_method') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,jacobian_methods=[1,45,3]) + call check_error(prob, 9, 'invalid jacobian_methods') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1) + call check_error(prob, 12, 'no method specified') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,jacobian_method=1,class=3) + call check_error(prob, 12, 'two method options specified') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,4,dpert,func,sparsity_mode=1,jacobian_method=1) + call check_error(prob, 13, 'invalid perturb_mode') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=5,jacobian_method=1) + call check_error(prob, 5, 'invalid sparsity_mode') + + lo = xlow + lo(2) = xhigh(2) + call prob%destroy() + call prob%initialize(n,m,lo,xhigh,1,dpert,func,sparsity_mode=1,jacobian_method=1) + call check_error(prob, 4, 'xlow >= xhigh', msg_contains='Error for optimization variable 2') + + call prob%destroy() + call prob%diff_initialize(n,m,lo,xhigh,func,sparsity_mode=1) + call check_error(prob, 4, 'diff: xlow >= xhigh') + + call prob%destroy() + call prob%diff_initialize(n,m,xlow,xhigh,func,sparsity_mode=4) + call check_error(prob, 2, 'diff: sparsity_mode=4 without dpert_for_sparsity') + + call prob%destroy() + call prob%diff_initialize(n,m,xlow,xhigh,func,sparsity_mode=4,& + dpert_for_sparsity=dpert,sparsity_perturb_mode=0) + call check_error(prob, 1, 'diff: invalid sparsity_perturb_mode') + end subroutine test_initialize_errors + + subroutine test_other_errors() + !! invalid inputs to the other routines + type(numdiff_type) :: prob + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,jacobian_method=1) + call prob%set_numdiff_bounds([0.0_wp],[1.0_wp]) + call check_error(prob, 3, 'set_numdiff_bounds: wrong size') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=3,jacobian_method=1) + call prob%set_sparsity_pattern([1,2],[1]) + call check_error(prob, 15, 'set_sparsity_pattern: size mismatch') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=3,jacobian_method=1) + call prob%set_sparsity_pattern([1,2],[1,n+1]) + call check_error(prob, 15, 'set_sparsity_pattern: column out of range') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=3,jacobian_method=1) + call prob%set_sparsity_pattern([1],[1],linear_irow=[0],linear_icol=[1],linear_vals=[1.0_wp]) + call check_error(prob, 17, 'set_sparsity_pattern: invalid linear pattern') + + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=3,jacobian_method=1,& + partition_sparsity_pattern=.true.) + call prob%set_sparsity_pattern([1],[1],maxgrp=1,ngrp=[1,2,1]) + call check_error(prob, 28, 'set_sparsity_pattern: ngrp > maxgrp') + end subroutine test_other_errors + + subroutine test_error_status() + !! no error, then a user termination + type(numdiff_type) :: prob + integer :: istat + character(len=:),allocatable :: error_msg + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,jacobian_method=1) + call prob%get_error_status(istat=istat, error_msg=error_msg) + call check(.not. prob%failed() .and. istat==0 .and. error_msg=='', 'no error: istat=0, empty message') + call prob%terminate() + call check_error(prob, -1, 'terminate', msg_contains='Terminated by the user') + end subroutine test_error_status + + subroutine test_formulas() + !! finite difference formulas, and printing a method + character(len=:),allocatable :: formula, name + type(finite_diff_method) :: fd, fd_empty + type(numdiff_type) :: prob + logical :: status_ok + character(len=:),allocatable :: text + + call get_finite_diff_formula(1, formula, name) + call check(formula=='dfdx = (f(x+h)-f(x)) / h' .and. name=='2-point forward 1', 'formula: id 1') + call get_finite_diff_formula(3, formula, name) + call check(formula=='dfdx = (f(x+h)-f(x-h)) / (2h)' .and. name=='3-point central', 'formula: id 3') + call get_finite_diff_formula(10, formula) + call check(formula=='dfdx = (f(x-2h)-8f(x-h)+8f(x+h)-f(x+2h)) / (12h)', 'formula: id 10') + call get_finite_diff_formula(45, formula, name) + call check(formula=='' .and. name=='', 'formula: unknown id') + call fd_empty%get_formula(formula) + call check(formula=='', 'formula: uninitialized method') + + ! get a method, and print it: + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=1,jacobian_method=1) + call prob%select_finite_diff_method(0.0_wp,-1.0_wp,1.0_wp,1.0e-3_wp,& + get_all_methods_in_class(3),fd,status_ok) + call check(status_ok, 'select_finite_diff_method: status_ok') + text = print_to_string(fd) + call check(index(text,'name : 3-point central')>0, 'print method: name') + call check(index(text,'dx_factors : 1, -1,')>0, 'print method: dx_factors') + call check(index(text,'df_den_factor : 2')>0, 'print method: df_den_factor') + end subroutine test_formulas + + function print_to_string(fd) result(text) + !! print a method to a scratch file, and return the output + type(finite_diff_method),intent(in) :: fd + character(len=:),allocatable :: text + integer :: iunit + open(newunit=iunit, status='scratch', form='formatted') + call fd%print(iunit) + text = read_back(iunit) + end function print_to_string + + function read_back(iunit) result(text) + !! rewind a scratch file, read all the lines, and close it + integer,intent(in) :: iunit + character(len=:),allocatable :: text + integer :: istat + character(len=1000) :: line + rewind(iunit) + text = '' + do + read(iunit,'(A)',iostat=istat) line + if (istat/=0) exit + text = text//trim(line)//' | ' + end do + close(iunit) + end function read_back + + subroutine test_sparsity_output() + !! returning and printing a sparsity pattern with linear elements and a partition + type(numdiff_type) :: prob + integer,dimension(:),allocatable :: irow, icol, linear_irow, linear_icol, ngrp + real(wp),dimension(:),allocatable :: linear_vals + integer :: maxgrp, iunit + character(len=:),allocatable :: text + + ! f1 = x1**2 (nonlinear), f2 = x2*x3 (nonlinear), plus a linear element (1,3)=5 + call prob%destroy() + call prob%initialize(n,m,xlow,xhigh,1,dpert,func,sparsity_mode=3,jacobian_method=3,& + partition_sparsity_pattern=.true.) + call prob%set_sparsity_pattern([1,2,2],[1,2,3],linear_irow=[1],linear_icol=[3],linear_vals=[5.0_wp]) + call check(.not. prob%failed(), 'sparsity output: set_sparsity_pattern') + + call prob%get_sparsity_pattern(irow,icol,linear_irow,linear_icol,linear_vals,maxgrp,ngrp) + call check(all(irow==[1,2,2]) .and. all(icol==[1,2,3]), 'get_sparsity_pattern: pattern') + call check(all(linear_irow==[1]) .and. all(linear_icol==[3]) .and. all(linear_vals==[5.0_wp]), & + 'get_sparsity_pattern: linear pattern') + call check(maxgrp>=1 .and. size(ngrp)==n, 'get_sparsity_pattern: partition') + + ! vector form: + open(newunit=iunit, status='scratch', form='formatted') + call prob%print_sparsity_pattern(iunit) + text = read_back(iunit) + call check(index(text,'irow: 1, 2, 2,')>0 .and. index(text,'icol: 1, 2, 3,')>0, & + 'print_sparsity_pattern: pattern') + call check(index(text,'---Sparsity partition---')>0, 'print_sparsity_pattern: partition') + call check(index(text,'---Linear sparsity pattern---')>0 .and. index(text,'vals:')>0, & + 'print_sparsity_pattern: linear pattern') + + ! matrix form: + open(newunit=iunit, status='scratch', form='formatted') + call prob%print_sparsity_matrix(iunit) + text = read_back(iunit) + ! nonlinear rows: X00, 0XX. linear rows: 00X, 000 + call check(index(text,'---Sparsity pattern--- | X00 | 0XX |')>0, 'print_sparsity_matrix: pattern') + call check(index(text,'---Linear sparsity pattern--- | 00X | 000 |')>0, 'print_sparsity_matrix: linear pattern') + end subroutine test_sparsity_output + + subroutine func(me,x,f,funcs_to_compute) + !! test function + class(numdiff_type),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: f + integer,dimension(:),intent(in) :: funcs_to_compute + f(1) = x(1)**2 + 5.0_wp*x(3) + f(2) = x(2)*x(3) + end subroutine func + + end program validation_test +!*******************************************************************************