diff --git a/.gitignore b/.gitignore index 3314a7d..7b5617d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ coverage.info **/*~ demo/binary + +# In the `fncas` repository, Bricks are installed/updated from GitHub. +# To avoid conflicts, KnowSheet/Bricks should not be checked in. +Bricks diff --git a/KnowSheet/scripts/KnowSheetDir.sh b/KnowSheet/scripts/KnowSheetDir.sh index 0d48e37..c0fa3a1 100755 --- a/KnowSheet/scripts/KnowSheetDir.sh +++ b/KnowSheet/scripts/KnowSheetDir.sh @@ -4,11 +4,11 @@ # Prints its path. dir=$(cd $(dirname $0) && pwd) -subdir=KnowSheet +subdir="KnowSheet" -while [ ! -d ${dir}/${subdir} ] ; do +while [ ! -d "${dir}/${subdir}" ] ; do [ "$dir" == "/" ] && break - dir=$(dirname $dir) + dir=$(dirname "$dir") done -[ -d "$dir/${subdir}" ] && echo "${dir}/${subdir}" || exit 1 +[ -d "${dir}/${subdir}" ] && echo "${dir}/${subdir}" || exit 1 diff --git a/KnowSheet/scripts/KnowSheetReadlink.sh b/KnowSheet/scripts/KnowSheetReadlink.sh new file mode 100755 index 0000000..80690f6 --- /dev/null +++ b/KnowSheet/scripts/KnowSheetReadlink.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# `readlink -e` replacement compatible with Mac OS X `readlink` implementation. +# Credits to http://stackoverflow.com/questions/59895/can-a-bash-script-tell-what-directory-its-stored-in and https://github.com/tarruda/zsh-autosuggestions/issues/38#issuecomment-69258372 + +set -u -e + +SOURCE="$1" +while [ -h "$SOURCE" ]; do # Resolve $SOURCE until the file is no longer a symlink. + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE" # If $SOURCE was a relative symlink, we need to resolve it + # relative to the path where the symlink file was located. +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +echo "$DIR/$(basename "$1")" diff --git a/KnowSheet/scripts/Makefile b/KnowSheet/scripts/Makefile index 96d7597..3d9aee6 100644 --- a/KnowSheet/scripts/Makefile +++ b/KnowSheet/scripts/Makefile @@ -9,7 +9,8 @@ .PHONY: test all indent clean check coverage # Need to know where to invoke scripts from, since `Makefile` can be a relative path symlink. -KNOWSHEET_SCRIPTS_DIR=$(dir $(shell readlink -e $(lastword $(MAKEFILE_LIST)))) +KNOWSHEET_SCRIPTS_DIR := $(patsubst %\,%,$(patsubst %/,%,$(dir $(shell readlink $(lastword $(MAKEFILE_LIST)))))) +KNOWSHEET_SCRIPTS_DIR_FULL_PATH := $(shell "$(KNOWSHEET_SCRIPTS_DIR)/KnowSheetReadlink.sh" "$(KNOWSHEET_SCRIPTS_DIR)" ) CPLUSPLUS?=g++ CPPFLAGS=-std=c++11 -Wall -W @@ -26,17 +27,20 @@ BIN=$(SRC:%.cc=.noshit/%) OS=$(shell uname) ifeq ($(OS),Darwin) - CPPFLAGS+= -x objective-c++ -fobjc-arc + CPPFLAGS+= -stdlib=libc++ -x objective-c++ -fobjc-arc LDFLAGS+= -framework Foundation endif default: all if [ -f test.cc ] ; then \ - .noshit/test --bricks_runtime_arch=${OS} ; \ + make test ;\ else \ find .noshit/ -executable -type f -exec "{}" ";" ; \ fi +test: .noshit/test + .noshit/test --bricks_runtime_arch=${OS} + debug: ulimit -c unlimited && touch test.cc && rm -f core && make ./.noshit/test && (./.noshit/test && echo OK || gdb ./.noshit/test core) @@ -50,10 +54,10 @@ clean: ${CPLUSPLUS} ${CPPFLAGS} -o $@ $< ${LDFLAGS} indent: - ${KNOWSHEET_SCRIPTS_DIR}/indent.sh + ${KNOWSHEET_SCRIPTS_DIR_FULL_PATH}/indent.sh check: - ${KNOWSHEET_SCRIPTS_DIR}/check-headers.sh + ${KNOWSHEET_SCRIPTS_DIR_FULL_PATH}/check-headers.sh coverage: - ${KNOWSHEET_SCRIPTS_DIR}/coverage-report.sh + ${KNOWSHEET_SCRIPTS_DIR_FULL_PATH}/coverage-report.sh diff --git a/KnowSheet/scripts/check-all-headers.sh b/KnowSheet/scripts/check-all-headers.sh index 7bfacb0..7e15791 100755 --- a/KnowSheet/scripts/check-all-headers.sh +++ b/KnowSheet/scripts/check-all-headers.sh @@ -6,14 +6,15 @@ set -u -e -SCRIPT_DIR=$(dirname $(readlink -e "$BASH_SOURCE")) -RUN_DIR=$PWD +KNOWSHEET_SCRIPTS_DIR=$( dirname "${BASH_SOURCE[0]}" ) +KNOWSHEET_SCRIPTS_DIR_FULL_PATH=$( "$KNOWSHEET_SCRIPTS_DIR/KnowSheetReadlink.sh" "$KNOWSHEET_SCRIPTS_DIR" ) +RUN_DIR_FULL_PATH=$( "$KNOWSHEET_SCRIPTS_DIR/KnowSheetReadlink.sh" "$PWD" ) echo -e "\033[1m\033[34mTesting all headers to comply with the header-only paradigm.\033[0m" -for i in $(for i in $(find . -iname "*.h" | grep -v 3party) ; do dirname $i ; done | sort -u) ; do +for i in $(for i in $(find . -iname "*.h" | grep -v 3party) ; do dirname "$i" ; done | sort -u) ; do echo -e "\033[1mDirectory\033[0m: $i" - cd $i && $SCRIPT_DIR/check-headers.sh && cd $RUN_DIR && continue + cd "$i" && "$KNOWSHEET_SCRIPTS_DIR_FULL_PATH/check-headers.sh" && cd "$RUN_DIR_FULL_PATH" && continue echo -e "\033[1m\033[31mTerminating.\033[0m" && exit 1 done diff --git a/KnowSheet/scripts/check-headers.sh b/KnowSheet/scripts/check-headers.sh index fadb6cf..1b694f8 100755 --- a/KnowSheet/scripts/check-headers.sh +++ b/KnowSheet/scripts/check-headers.sh @@ -10,28 +10,30 @@ set -u -e CPPFLAGS="-std=c++11 -g -Wall -W -DBRICKS_CHECK_HEADERS_MODE" LDFLAGS="-pthread" -TMPDIR=.noshit +# NOTE: TMP_DIR must be resolved from the current working directory. -rm -rf $TMPDIR/headers -mkdir -p $TMPDIR/headers +TMP_DIR_NAME=".noshit" + +rm -rf "$TMP_DIR_NAME/headers" +mkdir -p "$TMP_DIR_NAME/headers" echo -e -n "\033[1mCompiling\033[0m: " for i in $(ls *.h | grep -v ".cc.h$") ; do echo -e -n "\033[36m" echo -n "$i " echo -e -n "\033[31m" - ln -sf $PWD/$i $PWD/$TMPDIR/headers/$i.g++.cc - ln -sf $PWD/$i $PWD/$TMPDIR/headers/$i.clang++.cc - g++ -I . $CPPFLAGS -c $PWD/$TMPDIR/headers/$i.g++.cc -o $PWD/$TMPDIR/headers/$i.g++.o $LDFLAGS - clang++ -I . $CPPFLAGS -c $PWD/$TMPDIR/headers/$i.clang++.cc -o $PWD/$TMPDIR/headers/$i.clang++.o $LDFLAGS + ln -sf "$PWD/$i" "$PWD/$TMP_DIR_NAME/headers/$i.g++.cc" + ln -sf "$PWD/$i" "$PWD/$TMP_DIR_NAME/headers/$i.clang++.cc" + g++ -I . $CPPFLAGS -c "$PWD/$TMP_DIR_NAME/headers/$i.g++.cc" -o "$PWD/$TMP_DIR_NAME/headers/$i.g++.o" $LDFLAGS + clang++ -I . $CPPFLAGS -c "$PWD/$TMP_DIR_NAME/headers/$i.clang++.cc" -o "$PWD/$TMP_DIR_NAME/headers/$i.clang++.o" $LDFLAGS done echo echo -e -n "\033[0m\033[1mLinking\033[0m:\033[0m\033[31m " -echo -e '#include \nint main() { printf("OK\\n"); }\n' >$TMPDIR/headers/main.cc -g++ -c $CPPFLAGS -o $TMPDIR/headers/main.o $TMPDIR/headers/main.cc $LDFLAGS -g++ -o $TMPDIR/headers/main $TMPDIR/headers/*.o $LDFLAGS +echo -e '#include \nint main() { printf("OK\\n"); }\n' >"$TMP_DIR_NAME/headers/main.cc" +g++ -c $CPPFLAGS -o "$TMP_DIR_NAME/headers/main.o" "$TMP_DIR_NAME/headers/main.cc" $LDFLAGS +g++ -o "$TMP_DIR_NAME/headers/main" $TMP_DIR_NAME/headers/*.o $LDFLAGS echo -e -n "\033[1m\033[32m" -$TMPDIR/headers/main +"$TMP_DIR_NAME/headers/main" echo -e -n "\033[0m" diff --git a/KnowSheet/scripts/coverage-report.sh b/KnowSheet/scripts/coverage-report.sh index 37ba779..bb43aef 100755 --- a/KnowSheet/scripts/coverage-report.sh +++ b/KnowSheet/scripts/coverage-report.sh @@ -7,23 +7,29 @@ set -u -e CPPFLAGS="-std=c++11 -g -Wall -W -fprofile-arcs -ftest-coverage -DBRICKS_COVERAGE_REPORT_MODE" LDFLAGS="-pthread" -TMPDIR=.noshit +# NOTE: TMP_DIR must be resolved from the current working directory. -mkdir -p $TMPDIR +KNOWSHEET_SCRIPTS_DIR=$( dirname "${BASH_SOURCE[0]}" ) +RUN_DIR_FULL_PATH=$( "$KNOWSHEET_SCRIPTS_DIR/KnowSheetReadlink.sh" "$PWD" ) + +TMP_DIR_NAME=".noshit" +TMP_DIR_FULL_PATH="$RUN_DIR_FULL_PATH/.noshit" + +mkdir -p "$TMP_DIR_NAME" for i in *.cc ; do echo -e "\033[0m\033[1m$i\033[0m: \033[33mGenerating coverage report.\033[0m" - BINARY=${i%".cc"} - rm -rf $TMPDIR/coverage/$BINARY - mkdir -p $TMPDIR/coverage/$BINARY - g++ $CPPFLAGS $i -o $TMPDIR/coverage/$BINARY/binary $LDFLAGS - ./$TMPDIR/coverage/$BINARY/binary || exit 1 - gcov $i >/dev/null + BINARY="${i%".cc"}" + rm -rf "$TMP_DIR_NAME/coverage/$BINARY" + mkdir -p "$TMP_DIR_NAME/coverage/$BINARY" + g++ $CPPFLAGS "$i" -o "$TMP_DIR_NAME/coverage/$BINARY/binary" $LDFLAGS + "./$TMP_DIR_NAME/coverage/$BINARY/binary" || exit 1 + gcov "$i" >/dev/null geninfo . --output-file coverage0.info >/dev/null lcov -r coverage0.info /usr/include/\* \*/gtest/\* \*/3party/\* -o coverage.info >/dev/null - genhtml coverage.info --output-directory $TMPDIR/coverage/$BINARY >/dev/null + genhtml coverage.info --output-directory "$TMP_DIR_NAME/coverage/$BINARY" >/dev/null rm -rf coverage.info coverage0.info *.gcov *.gcda *.gcno echo -e -n "\033[0m\033[1m$i\033[0m: \033[36m" - echo $(readlink -e $TMPDIR/coverage/$BINARY/index.html) + echo -n "$TMP_DIR_FULL_PATH/coverage/$BINARY/index.html" echo -e -n "\033[0m" done diff --git a/KnowSheet/scripts/full-test.sh b/KnowSheet/scripts/full-test.sh index a16a8e2..900e66b 100755 --- a/KnowSheet/scripts/full-test.sh +++ b/KnowSheet/scripts/full-test.sh @@ -11,52 +11,69 @@ set -u -e CPPFLAGS="-std=c++11 -g -Wall -W -fprofile-arcs -ftest-coverage -DBRICKS_COVERAGE_REPORT_MODE" LDFLAGS="-pthread" -TMPDIR=.noshit/fulltest -TMPDIR_FULL_PATH=$(readlink -e .)/.noshit/fulltest +# NOTE: FULL_TEST_DIR must be resolved from the current working directory. -GOLDEN_SUBDIR_NAME=golden -GOLDEN_FULL_PATH=$TMPDIR_FULL_PATH/$GOLDEN_SUBDIR_NAME +KNOWSHEET_SCRIPTS_DIR=$( dirname "${BASH_SOURCE[0]}" ) +RUN_DIR_FULL_PATH=$( "$KNOWSHEET_SCRIPTS_DIR/KnowSheetReadlink.sh" "$PWD" ) + +FULL_TEST_DIR_NAME="zzz_full_test" # The `zzz` prefix guarantees the full test directory is down the list. +FULL_TEST_DIR_FULL_PATH="$RUN_DIR_FULL_PATH/$FULL_TEST_DIR_NAME" + +ALL_TESTS_TOGETHER="everything" + +GOLDEN_DIR_NAME="golden" +GOLDEN_FULL_PATH="$FULL_TEST_DIR_FULL_PATH/$GOLDEN_DIR_NAME" # Concatenate all `test.cc` files in the right way, creating one test to rule them all. -mkdir -p $TMPDIR -echo "// Magic. Watch." > $TMPDIR/ALL_TESTS_TOGETHER.cc +mkdir -p "$FULL_TEST_DIR_NAME" +( +echo '// This file is auto-generated by KnowSheet/scripts/full-test.sh.' ; +echo '// It is updated by running `make run` in the top-level Bricks directory, along with the documentation.' +echo '// And it is checked in, much like the documentation, so that non-*nix systems can run all the tests.' +echo +echo '#include "port.h" // To have `std::{min/max}` work in Visual Studio, need port.h before STL headers.' +echo +echo '#include "dflags/dflags.h"' +echo '#include "3party/gtest/gtest-main-with-dflags.h"' +echo +) > "$FULL_TEST_DIR_NAME/$ALL_TESTS_TOGETHER.cc" -echo "#include \"dflags/dflags.h\"" >> $TMPDIR/ALL_TESTS_TOGETHER.cc -echo "#include \"3party/gtest/gtest-main-with-dflags.h\"" >> $TMPDIR/ALL_TESTS_TOGETHER.cc echo -n -e "\033[0m\033[1mTests:\033[0m\033[36m" -for i in $(find . -iname test.cc | grep -v 3party | grep -v "/.noshit/"); do - echo "#include \"$i\"" >> $TMPDIR/ALL_TESTS_TOGETHER.cc +for i in $(find . -iname "*test.cc" | grep -v 3party | grep -v "/.noshit/" | sort -g); do + echo "#include \"$i\"" >> "$FULL_TEST_DIR_NAME/$ALL_TESTS_TOGETHER.cc" echo -n " $i" done # Allow this one test to rule them all to access all the `golden/` files. -mkdir -p $GOLDEN_FULL_PATH -for dirname in $(find . -iname $GOLDEN_SUBDIR_NAME -type d | grep -v 3party | grep -v "/.noshit/"); do - (cd $dirname; for filename in * ; do ln -sf $PWD/$filename $GOLDEN_FULL_PATH ; done) +mkdir -p "$GOLDEN_FULL_PATH" +echo -e "\n\n\033[0m\033[1mGolden files\033[0m: \033[35m" +for dirname in $(find . -iname "$GOLDEN_DIR_NAME" -type d | grep -v 3party | grep -v "/.noshit/" | grep -v "$FULL_TEST_DIR_NAME"); do + (cd $dirname; for filename in * ; do cp -v "$PWD/$filename" "$GOLDEN_FULL_PATH" ; done) done +echo -e -n "\033[0m" ( # Compile and run The Big Test. - cd $TMPDIR + cd "$FULL_TEST_DIR_NAME" echo -e "\033[0m" echo -n -e "\033[1mCompiling all tests together: \033[0m\033[31m" - g++ $CPPFLAGS -I ../.. ALL_TESTS_TOGETHER.cc -o ALL_TESTS_TOGETHER $LDFLAGS + g++ $CPPFLAGS -I .. "$ALL_TESTS_TOGETHER.cc" -o "$ALL_TESTS_TOGETHER" $LDFLAGS echo -e "\033[32m\033[1mOK.\033[0m" echo -e "\033[1mRunning the tests and generating coverage info.\033[0m" - ./ALL_TESTS_TOGETHER || exit 1 - echo -e "\033[32m\033[1mALL TESTS PASS.\033[0m" + "./$ALL_TESTS_TOGETHER" || exit 1 + echo -e "\n\033[32m\033[1mALL TESTS PASS.\033[0m" # Generate the resulting code coverage report. - gcov ALL_TESTS_TOGETHER.cc >/dev/null + gcov "$ALL_TESTS_TOGETHER.cc" >/dev/null geninfo . --output-file coverage0.info >/dev/null lcov -r coverage0.info /usr/include/\* \*/gtest/\* \*/3party/\* -o coverage.info >/dev/null genhtml coverage.info --output-directory coverage/ >/dev/null rm -rf coverage.info coverage0.info *.gcov *.gcda *.gcno echo echo -e -n "\033[0m\033[1mCoverage report\033[0m: \033[36m" - readlink -e coverage/index.html + echo -n "$FULL_TEST_DIR_FULL_PATH/coverage/index.html" echo -e -n "\033[0m" ) diff --git a/KnowSheet/scripts/gen-docu.sh b/KnowSheet/scripts/gen-docu.sh new file mode 100755 index 0000000..973e2b5 --- /dev/null +++ b/KnowSheet/scripts/gen-docu.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# This script generates the documentation in GitHub markdown format. +# It scans all `docu_*.*` files, uses `.md` directly and takes the lines starting with double space from `.h`. +# +# Best practices: +# +# 1) Make `docu_*.cc` files that should be passing unit tests `docu_*_test.cc`. +# This ensures they will be run by the full test make target, and thus it would +# not so happen accidentally that some of the docu-mented code does not compile and/or pass tests. +# +# 2) Use header guards in those `docu_*_test.cc` files and `#include` them from the actual `test.cc` +# for that Bricks module. +# That would ensure that those `docu_*_test.cc` tests are run both by `make` from within that directory, +# and by the top-level `make` that runs all tests at once and generates the coverage report. +# +# 3) Use the two spaces wisely. +# If something should be in the docu, it should be indented. +# If it should not be, it should not be indented. +# The `docu_*.*` files are excluded from `make indent`. + +set -u -e + +for fn in $(for i in $(find . -type f -iname "docu_*.*" | grep -v ".noshit" | grep -v "zzz_full_test"); do + echo -e "$(basename "$i")\t$i"; + done | sort | cut -f2) ; do + echo $fn >/dev/stderr + case $fn in + *.cc) + echo '```cpp' + (grep '^ ' $fn || echo " // No documentation data in '$fn'.") | sed "s/^ //g" + echo '```' + ;; + *.md) + cat $fn + ;; + *) + echo "Unrecognized extension for file '$fn'." + exit 1 + ;; + esac +done diff --git a/KnowSheet/scripts/github-install.sh b/KnowSheet/scripts/github-install.sh index d03e199..6d392da 100755 --- a/KnowSheet/scripts/github-install.sh +++ b/KnowSheet/scripts/github-install.sh @@ -5,37 +5,39 @@ set -u -e # Command-line parameters. -GITHUB_REPO=${1:-Bricks} -GITHUB_USER=${2:-KnowSheet} -GITHUB_BRANCH=${3:-master} +GITHUB_REPO="${1:-Bricks}" +GITHUB_USER="${2:-KnowSheet}" +GITHUB_BRANCH="${3:-master}" + +GITHUB_REPO_DASH_BRANCH="${GITHUB_REPO}-${GITHUB_BRANCH}" # The URL to fetch a .tar.gz from. -URL=https://github.com/$GITHUB_USER/$GITHUB_REPO/archive/$GITHUB_BRANCH.tar.gz +URL="https://github.com/${GITHUB_USER}/${GITHUB_REPO}/archive/${GITHUB_BRANCH}.tar.gz" # A temporary directory, expected to be .gitignore-d. -TMPDIR=.noshit +TMP_DIR_NAME=".noshit" -( - mkdir -p $TMPDIR - cd $TMPDIR - rm -rf $GITHUB_REPO $GITHUB_REPO-$GITHUB_BRANCH - echo -n "github.com/$GITHUB_USER/$GITHUB_REPO@$GITHUB_BRANCH: Fetch... " - curl -s -L $URL | tar xfz - || (echo "Failed, please check whether $URL can be fetched."; exit 1) - mv $GITHUB_REPO-$GITHUB_BRANCH $GITHUB_REPO +( + mkdir -p "$TMP_DIR_NAME" + cd "$TMP_DIR_NAME" + rm -rf "$GITHUB_REPO" "$GITHUB_REPO_DASH_BRANCH" + echo -n "github.com/${GITHUB_USER}/${GITHUB_REPO}@${GITHUB_BRANCH}: Fetch... " + curl -s -L "$URL" | tar xfz - || (echo "Failed, please check whether $URL can be fetched."; exit 1) + mv "$GITHUB_REPO_DASH_BRANCH" "$GITHUB_REPO" echo -ne "\b\b\b\b\b\b\b\b\b" ) -if [ -d $GITHUB_REPO ] ; then - if diff -r $GITHUB_REPO $TMPDIR/$GITHUB_REPO ; then +if [ -d "$GITHUB_REPO" ] ; then + if diff -r "$GITHUB_REPO" "$TMP_DIR_NAME/$GITHUB_REPO" ; then echo "Up to date." else echo "Conflict, please fix or reinstall." echo "For a quick workaround, try renaming the current revision under another name:" - echo "mv $GITHUB_REPO $TMPDIR/$GITHUB_REPO-$(date +%Y%m%d-%H%M%S)" + echo "mv \"$GITHUB_REPO\" \"$TMP_DIR_NAME/${GITHUB_REPO}-$(date +%Y%m%d-%H%M%S)\"" echo "and then re-run the same command." exit 1 fi else - mv $TMPDIR/$GITHUB_REPO . + mv "$TMP_DIR_NAME/$GITHUB_REPO" . echo "Installed." fi diff --git a/KnowSheet/scripts/indent.sh b/KnowSheet/scripts/indent.sh index c82231c..6badd66 100755 --- a/KnowSheet/scripts/indent.sh +++ b/KnowSheet/scripts/indent.sh @@ -3,4 +3,7 @@ # Indents all *.cc and *.h files in the current directory and below # according to .clang-format in this directory or above. -(find . -name "*.cc" ; find . -name "*.h") | grep -v "/.noshit/" | grep -v "/3party/" | xargs clang-format-3.5 -i +(find . -name "*.cc" ; find . -name "*.h") \ +| grep -v "/.noshit/" | grep -v "/3party/" \ +| grep -v "/docu_" \ +| xargs clang-format-3.5 -i diff --git a/Makefile b/Makefile index 2133ea2..8a5a500 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,15 @@ include KnowSheet/scripts/Makefile +LDFLAGS+=-ldl -.PHONY: all demo unittest +.PHONY: install docu README.md deprecated_test -all: demo unittest +install: + ./KnowSheet/scripts/github-install.sh -demo: - (cd demo; make) +docu: README.md -unittest: - (cd test; make) +README.md: + ./KnowSheet/scripts/gen-docu.sh >$@ + +deprecated_test: + (cd deprecated_test; make) diff --git a/README.md b/README.md index d97dd43..5d01dc9 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,3 @@ -# Title +# `fncas` -FNCAS is not Computer Algebra System - -# Purpose - -Clear, but yet to be put here. - -# Build Instructions - -To run on a fresh Ubuntu machine run the following. - - sudo apt-get update - sudo apt-get install -y git build-essential libboost-dev nasm clang - - git clone https://github.com/dkorolev/fncas.git - - cd fncas - (cd fncas; make) - (cd test; make) - cd - - -* (cd fncas; make) confirms the build environment is set up. -* (cd test; make) runs some basic tests. - -## Issues - -### g++ sorry, unimplemented: non-static data member initializers - -You'll need gcc/g++ 4.7+. I had to do the following in Ubuntu 12.04: - - sudo add-apt-repository ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt-get install gcc-4.7 g++-4.7 - - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 60 --slave /usr/bin/g++ g++ /usr/bin/g++-4.6 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.7 40 --slave /usr/bin/g++ g++ /usr/bin/g++-4.7 - sudo update-alternatives --config gcc - -### static_assert in /usr/include/c++/4.6/chrono - -If you get an error like this when compiling with clang++: - - /usr/include/c++/4.6/chrono:666:7: error: static_assert expression is not an integral constant expression - -Just comment out that static_assert. +Here be dragons. diff --git a/demo/Makefile b/demo/Makefile deleted file mode 100644 index 861d5b8..0000000 --- a/demo/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -.PHONY: all binary - -all: binary - -binary: - g++ -std=c++11 demo.cc -o binary -ldl - ./binary diff --git a/demo/demo.cc b/demo/demo.cc deleted file mode 100644 index 88ab870..0000000 --- a/demo/demo.cc +++ /dev/null @@ -1,45 +0,0 @@ -#include - -#ifndef FNCAS_JIT -#define FNCAS_JIT CLANG -#endif - -#include "../fncas/fncas.h" - -template -typename fncas::output::type f(const T& x) { - return (x[0] + x[1] * 2) * (x[0] + x[1] * 2); -} - -int main() { - std::cout << "Hello, FNCAS!" << std::endl; - - std::cout << "f(x) is declared as f(x[2]) = (x_0 + 2 * x_1) ^ 2);" << std::endl; - - fncas::f_native fn(f>, 2); - std::cout << "Native execution: f(1, 2) == " << fn(std::vector({1, 2})) << std::endl; - - fncas::x x(2); - fncas::f_intermediate fi = f(x); - std::cout << "Intermediate format: f(1, 2) == " << fi(std::vector({1, 2})) << std::endl; - - fncas::f_compiled fc = fncas::f_compiled(fi); - std::cout << "Compiled format: f(1, 2) == " << fc(std::vector({1, 2})) << std::endl; - - std::cout << "Intermediate details: " << fi.debug_as_string() << std::endl; - std::cout << "Compiled details: " << fc.lib_filename() << std::endl; - - auto p_3_3 = std::vector({3, 3}); - - fncas::g_approximate ga = fncas::g_approximate(f>, 2); - auto d_3_3_approx = ga(p_3_3); - std::cout << "Approximate {f, df}(3, 3) = { " << d_3_3_approx.value << ", { " << d_3_3_approx.gradient[0] - << ", " << d_3_3_approx.gradient[1] << " } }." << std::endl; - - fncas::g_intermediate gi = fncas::g_intermediate(x, f(x)); - auto d_3_3_intermediate = gi(p_3_3); - std::cout << "Differentiated {f, df}(3, 3) = { " << d_3_3_intermediate.value << ", { " - << d_3_3_intermediate.gradient[0] << ", " << d_3_3_intermediate.gradient[1] << " } }." << std::endl; - - std::cout << "Done." << std::endl; -} diff --git a/test/Makefile b/deprecated_test/Makefile similarity index 100% rename from test/Makefile rename to deprecated_test/Makefile diff --git a/test/eval.cc b/deprecated_test/eval.cc similarity index 100% rename from test/eval.cc rename to deprecated_test/eval.cc diff --git a/test/f/arithmetics.h b/deprecated_test/f/arithmetics.h similarity index 100% rename from test/f/arithmetics.h rename to deprecated_test/f/arithmetics.h diff --git a/test/f/big_arithmetics.h b/deprecated_test/f/big_arithmetics.h similarity index 100% rename from test/f/big_arithmetics.h rename to deprecated_test/f/big_arithmetics.h diff --git a/test/f/big_math.h b/deprecated_test/f/big_math.h similarity index 100% rename from test/f/big_math.h rename to deprecated_test/f/big_math.h diff --git a/test/f/math.h b/deprecated_test/f/math.h similarity index 100% rename from test/f/math.h rename to deprecated_test/f/math.h diff --git a/test/f/sum.h b/deprecated_test/f/sum.h similarity index 100% rename from test/f/sum.h rename to deprecated_test/f/sum.h diff --git a/test/function.h b/deprecated_test/function.h similarity index 100% rename from test/function.h rename to deprecated_test/function.h diff --git a/test/perf/.gitignore b/deprecated_test/perf/.gitignore similarity index 100% rename from test/perf/.gitignore rename to deprecated_test/perf/.gitignore diff --git a/test/perf/Makefile b/deprecated_test/perf/Makefile similarity index 100% rename from test/perf/Makefile rename to deprecated_test/perf/Makefile diff --git a/test/perf/run_perf_test.sh b/deprecated_test/perf/run_perf_test.sh similarity index 100% rename from test/perf/run_perf_test.sh rename to deprecated_test/perf/run_perf_test.sh diff --git a/test/smoke/.gitignore b/deprecated_test/smoke/.gitignore similarity index 100% rename from test/smoke/.gitignore rename to deprecated_test/smoke/.gitignore diff --git a/test/smoke/Makefile b/deprecated_test/smoke/Makefile similarity index 100% rename from test/smoke/Makefile rename to deprecated_test/smoke/Makefile diff --git a/test/smoke/run_smoke_test.sh b/deprecated_test/smoke/run_smoke_test.sh similarity index 100% rename from test/smoke/run_smoke_test.sh rename to deprecated_test/smoke/run_smoke_test.sh diff --git a/docu/docu_00.md b/docu/docu_00.md new file mode 100644 index 0000000..5d01dc9 --- /dev/null +++ b/docu/docu_00.md @@ -0,0 +1,3 @@ +# `fncas` + +Here be dragons. diff --git a/fncas/fncas.h b/fncas/fncas.h index 1ebc7f1..79695c2 100644 --- a/fncas/fncas.h +++ b/fncas/fncas.h @@ -1,9 +1,27 @@ -// https://github.com/dkorolev/fncas - -// Requires: -// * c++11, use g++ --std=c++11 -// * Boost, sudo apt-get install libboost-dev -- TODO(dkorolev): Get rid of Boost here. -// * -fno-strict-aliasing, to avoid warnings when compiling with g++. +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2015 Dmitry "Dima" Korolev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * *******************************************************************************/ #ifndef FNCAS_H #define FNCAS_H @@ -11,6 +29,7 @@ #include "fncas_base.h" #include "fncas_node.h" #include "fncas_differentiate.h" +#include "fncas_optimize.h" #include "fncas_jit.h" #endif diff --git a/fncas/fncas_base.h b/fncas/fncas_base.h index c3292fd..dae7aa4 100644 --- a/fncas/fncas_base.h +++ b/fncas/fncas_base.h @@ -1,4 +1,27 @@ -// https://github.com/dkorolev/fncas +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2015 Dmitry "Dima" Korolev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * *******************************************************************************/ #ifndef FNCAS_BASE_H #define FNCAS_BASE_H diff --git a/fncas/fncas_differentiate.h b/fncas/fncas_differentiate.h index e1c5348..4e0f929 100644 --- a/fncas/fncas_differentiate.h +++ b/fncas/fncas_differentiate.h @@ -1,6 +1,27 @@ -// https://github.com/dkorolev/fncas - -// Defines the means to differentiate a node. +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2015 Dmitry "Dima" Korolev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * *******************************************************************************/ #ifndef FNCAS_DIFFERENTIATE_H #define FNCAS_DIFFERENTIATE_H diff --git a/fncas/fncas_jit.h b/fncas/fncas_jit.h index 6c5522b..d925fd5 100644 --- a/fncas/fncas_jit.h +++ b/fncas/fncas_jit.h @@ -1,4 +1,27 @@ -// https://github.com/dkorolev/fncas +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2015 Dmitry "Dima" Korolev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * *******************************************************************************/ // FNCAS on-the-fly compilation logic. // FNCAS_JIT must be defined to enable, supported values are 'NASM' and 'CLANG'. @@ -6,20 +29,30 @@ #ifndef FNCAS_JIT_H #define FNCAS_JIT_H +// Do include this header in the `make test` target for checking there are no leaked symbols. +#ifdef BRICKS_CHECK_HEADERS_MODE +#ifndef FNCAS_JIT +#define FNCAS_JIT CLANG +#endif +#endif + #ifdef FNCAS_JIT #include #include #include #include +#include #include -#include - +#include "../../Bricks/strings/printf.h" +#include "../../Bricks/file/file.h" #include "fncas_base.h" #include "fncas_node.h" +using namespace bricks; + namespace fncas { // Linux-friendly code to compile into .so and link against it at runtime. @@ -57,7 +90,7 @@ struct compiled_expression : noncopyable { double operator()(const double* x) const { std::vector& tmp = internals_singleton().ram_for_compiled_evaluations_; node_index_type dim = static_cast(dim_()); - if (tmp.size() < dim) { + if (tmp.size() < static_cast(dim)) { tmp.resize(dim); } return eval_(x, &tmp[0]); @@ -75,7 +108,7 @@ struct compiled_expression : noncopyable { }; // generate_c_code_for_node() writes C code to evaluate the expression to the file. -void generate_c_code_for_node(node_index_type index, FILE* f) { +inline void generate_c_code_for_node(node_index_type index, FILE* f) { fprintf(f, "#include \n"); fprintf(f, "double eval(const double* x, double* a) {\n"); node_index_type max_dim = index; @@ -132,13 +165,14 @@ void generate_c_code_for_node(node_index_type index, FILE* f) { } // generate_asm_code_for_node() writes NASM code to evaluate the expression to the file. -const char* const operation_as_nasm_instruction(operation_t operation) { +inline const char* operation_as_nasm_instruction(operation_t operation) { static const char* representation[static_cast(operation_t::end)] = { "addpd", "subpd", "mulpd", "divpd", }; return operation < operation_t::end ? representation[static_cast(operation)] : "?"; } -void generate_asm_code_for_node(node_index_type index, FILE* f) { + +inline void generate_asm_code_for_node(node_index_type index, FILE* f) { fprintf(f, "[bits 64]\n"); fprintf(f, "\n"); fprintf(f, "global eval, dim\n"); @@ -235,11 +269,11 @@ struct compile_impl { generate_asm_code_for_node(index, f); fclose(f); - const char* compile_cmdline = "nasm -f elf64 %1%.asm -o %1%.o"; - const char* link_cmdline = "ld -lm -shared -o %1%.so %1%.o"; + const char* compile_cmdline = "nasm -f elf64 %s.asm -o %s.o"; + const char* link_cmdline = "ld -lm -shared -o %s.so %s.o"; - compiled_expression::syscall((boost::format(compile_cmdline) % filebase).str()); - compiled_expression::syscall((boost::format(link_cmdline) % filebase).str()); + compiled_expression::syscall(strings::Printf(compile_cmdline, filebase.c_str(), filebase.c_str())); + compiled_expression::syscall(strings::Printf(link_cmdline, filebase.c_str(), filebase.c_str())); } }; struct CLANG { @@ -249,8 +283,8 @@ struct compile_impl { generate_c_code_for_node(index, f); fclose(f); - const char* compile_cmdline = "clang -fPIC -shared -nostartfiles %1%.c -o %1%.so"; - std::string cmdline = (boost::format(compile_cmdline) % filebase).str(); + const char* compile_cmdline = "clang -fPIC -shared -nostartfiles %s.c -o %s.so"; + std::string cmdline = strings::Printf(compile_cmdline, filebase.c_str(), filebase.c_str()); compiled_expression::syscall(cmdline); } }; @@ -261,19 +295,15 @@ struct compile_impl { typedef FNCAS_JIT selected; }; -compiled_expression compile(node_index_type index) { - std::random_device random; - std::uniform_int_distribution distribution(1000000, 9999999); - std::ostringstream os; - os << "/tmp/" << distribution(random); - const std::string filebase = os.str(); +inline compiled_expression compile(node_index_type index) { + const std::string filebase(FileSystem::GenTmpFileName()); const std::string filename_so = filebase + ".so"; - unlink(filename_so.c_str()); + FileSystem::RmFile(filename_so, FileSystem::RmFileParameters::Silent); compile_impl::selected::compile(filebase, index); return compiled_expression(filename_so); } -compiled_expression compile(const node& node) { return compile(node.index_); } +inline compiled_expression compile(const node& node) { return compile(node.index_); } struct f_compiled : f { fncas::compiled_expression c_; diff --git a/fncas/fncas_mathutil.h b/fncas/fncas_mathutil.h new file mode 100644 index 0000000..d3aa9bc --- /dev/null +++ b/fncas/fncas_mathutil.h @@ -0,0 +1,105 @@ +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2015 Maxim Zhurovich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * *******************************************************************************/ + +#ifndef FNCAS_MATHUTIL_H +#define FNCAS_MATHUTIL_H + +#include +#include + +namespace fncas { + +inline bool IsNormal(double arg) { return (std::isnormal(arg) || arg == 0.0); } + +template +inline typename std::enable_if::value, T>::type DotProduct(const std::vector& v1, + const std::vector& v2) { +#ifndef NDEBUG + assert(v1.size() == v2.size()); +#endif + return std::inner_product(std::begin(v1), std::end(v1), std::begin(v2), static_cast(0)); +} + +template +inline typename std::enable_if::value, T>::type L2Norm(const std::vector& v) { + return DotProduct(v, v); +} + +template ::value, T>::type> +inline void FlipSign(std::vector& v) { + std::transform(std::begin(v), std::end(v), std::begin(v), std::negate()); +} + +// Polak-Ribiere formula for conjugate gradient method +// http://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method +inline double PolakRibiere(const std::vector& g, const std::vector& g_prev) { + const double beta = (L2Norm(g) - DotProduct(g, g_prev)) / L2Norm(g_prev); + if (IsNormal(beta)) { + return beta; + } else { + return 0.0; + } +} + +// Simplified backtracking line search algorithm with limited number of steps. +// Starts in `current_point` and searches for minimum in `direction` +// sequentially shrinking the step size. Returns new optimal point. +// Algorithm parameters: 0 < alpha < 1, 0 < beta < 1. +template +inline std::vector BackTracking(F&& eval_function, + G&& eval_gradient, + const std::vector& current_point, + const std::vector& direction, + const double alpha = 0.5, + const double beta = 0.8, + const size_t max_steps = 100) { + const double current_f_value = eval_function(current_point); + const std::vector current_gradient = eval_gradient(current_point).gradient; + std::vector test_point(current_point.size()); + for (size_t i = 0; i < current_point.size(); ++i) { + test_point[i] = current_point[i] + direction[i]; + } + double test_f_value = eval_function(test_point); + const double gradient_l2norm = L2Norm(current_gradient); + double t = 1.0; + + size_t bt_iteration = 0; + while (test_f_value > (current_f_value - alpha * t * gradient_l2norm) || !IsNormal(test_f_value)) { + t *= beta; + for (size_t i = 0; i < test_point.size(); ++i) { + test_point[i] = current_point[i] + t * direction[i]; + } + test_f_value = eval_function(test_point); + if (bt_iteration++ == max_steps) { + break; + } + } + + return test_point; +} + +} // namespace fncas + +#endif // #ifndef FNCAS_MATHUTIL_H diff --git a/fncas/fncas_node.h b/fncas/fncas_node.h index 66d97c8..c22e338 100644 --- a/fncas/fncas_node.h +++ b/fncas/fncas_node.h @@ -1,6 +1,27 @@ -// https://github.com/dkorolev/fncas - -// Defines FNCAS node class for basic expressions and the means to use it. +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2015 Dmitry "Dima" Korolev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * *******************************************************************************/ #ifndef FNCAS_NODE_H #define FNCAS_NODE_H diff --git a/fncas/fncas_optimize.h b/fncas/fncas_optimize.h new file mode 100644 index 0000000..658a4ef --- /dev/null +++ b/fncas/fncas_optimize.h @@ -0,0 +1,227 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2015 Dmitry "Dima" Korolev + (c) 2015 Maxim Zhurovich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*******************************************************************************/ + +#ifndef FNCAS_OPTIMIZE_H +#define FNCAS_OPTIMIZE_H + +#include +#include +#include + +#include "fncas_mathutil.h" + +namespace fncas { + +struct OptimizationResult { + double value; + std::vector point; +}; + +class OptimizerParameters { + public: + template + void SetValue(std::string name, T value); + template + const T GetValue(std::string name, T default_value); + + private: + std::map params_; +}; + +template +void OptimizerParameters::SetValue(std::string name, T value) { + static_assert(std::is_arithmetic::value, "Value must be numeric"); + params_[name] = value; +} + +template +const T OptimizerParameters::GetValue(std::string name, T default_value) { + static_assert(std::is_arithmetic::value, "Value must be numeric"); + if (params_.count(name)) { + return static_cast(params_[name]); + } else { + return default_value; + } +} + +// TODO(mzhurovich+dkorolev): Make different implementation to use functors. +// Naive gradient descent that tries 3 different step sizes in each iteration. +// Searches for a local minimum of `F::Compute` function. +template +class GradientDescentOptimizer : noncopyable { + public: + GradientDescentOptimizer() {} + GradientDescentOptimizer(OptimizerParameters& params) { + max_steps_ = params.GetValue("max_steps", max_steps_); + step_factor_ = params.GetValue("step_factor", step_factor_); + } + OptimizationResult Optimize(const std::vector& starting_point); + + private: + size_t max_steps_ = 5000; // Maximum number of optimization steps. + double step_factor_ = 1.0; // Gradient is multiplied by this factor. +}; + +// Simple gradient descent optimizer with backtracking algorithm. +// Searches for a local minimum of `F::Compute` function. +template +class GradientDescentOptimizerBT : noncopyable { + public: + GradientDescentOptimizerBT() {} + GradientDescentOptimizerBT(OptimizerParameters& params) { + min_steps_ = params.GetValue("min_steps", min_steps_); + max_steps_ = params.GetValue("max_steps", max_steps_); + bt_alpha_ = params.GetValue("bt_alpha", bt_alpha_); + bt_beta_ = params.GetValue("bt_beta", bt_beta_); + bt_max_steps_ = params.GetValue("bt_max_steps", bt_max_steps_); + grad_eps_ = params.GetValue("grad_eps", grad_eps_); + } + OptimizationResult Optimize(const std::vector& starting_point); + + private: + size_t min_steps_ = 3; // Minimum number of optimization steps (ignoring early stopping). + size_t max_steps_ = 5000; // Maximum number of optimization steps. + double bt_alpha_ = 0.5; // Alpha parameter for backtracking algorithm. + double bt_beta_ = 0.8; // Beta parameter for backtracking algorithm. + size_t bt_max_steps_ = 100; // Maximum number of backtracking steps. + double grad_eps_ = 1e-8; // Magnitude of gradient for early stopping. +}; + +// Optimizer that uses a combination of conjugate gradient method and +// backtracking line search to find a local minimum of `F::Compute` function. +template +class ConjugateGradientOptimizer : noncopyable { + public: + ConjugateGradientOptimizer() {} + ConjugateGradientOptimizer(OptimizerParameters& params) { + min_steps_ = params.GetValue("min_steps", min_steps_); + max_steps_ = params.GetValue("max_steps", max_steps_); + bt_alpha_ = params.GetValue("bt_alpha", bt_alpha_); + bt_beta_ = params.GetValue("bt_beta", bt_beta_); + bt_max_steps_ = params.GetValue("bt_max_steps", bt_max_steps_); + grad_eps_ = params.GetValue("grad_eps", grad_eps_); + } + OptimizationResult Optimize(const std::vector& starting_point); + + private: + size_t min_steps_ = 3; // Minimum number of optimization steps (ignoring early stopping). + size_t max_steps_ = 5000; // Maximum number of optimization steps. + double bt_alpha_ = 0.5; // Alpha parameter for backtracking algorithm. + double bt_beta_ = 0.8; // Beta parameter for backtracking algorithm. + size_t bt_max_steps_ = 100; // Maximum number of backtracking steps. + double grad_eps_ = 1e-8; // Magnitude of gradient for early stopping. +}; + +template +OptimizationResult GradientDescentOptimizer::Optimize(const std::vector& starting_point) { + fncas::reset_internals_singleton(); + const size_t dim = starting_point.size(); + const fncas::x gradient_helper(dim); + fncas::f_intermediate fi(F::compute(gradient_helper)); + fncas::g_intermediate gi = fncas::g_intermediate(gradient_helper, fi); + std::vector current_point(starting_point); + + for (size_t iteration = 0; iteration < max_steps_; ++iteration) { + const auto g = gi(current_point); + const auto try_step = [&dim, ¤t_point, &fi, &g ](double step) + -> std::pair> { + std::vector candidate(dim); + for (size_t i = 0; i < dim; ++i) { + candidate[i] = current_point[i] - g.gradient[i] * step; + } + const double value = fi(candidate); + return std::make_pair(value, candidate); + }; + current_point = std::min(try_step(0.01 * step_factor_), + std::min(try_step(0.05 * step_factor_), try_step(0.2 * step_factor_))).second; + } + + return OptimizationResult{fi(current_point), current_point}; +} + +template +OptimizationResult GradientDescentOptimizerBT::Optimize(const std::vector& starting_point) { + fncas::reset_internals_singleton(); + const size_t dim = starting_point.size(); + const fncas::x gradient_helper(dim); + fncas::f_intermediate fi(F::compute(gradient_helper)); + fncas::g_intermediate gi = fncas::g_intermediate(gradient_helper, fi); + std::vector current_point(starting_point); + + for (size_t iteration = 0; iteration < max_steps_; ++iteration) { + auto direction = gi(current_point).gradient; + fncas::FlipSign(direction); // Going against the gradient to minimize the function. + current_point = BackTracking(fi, gi, current_point, direction, bt_alpha_, bt_beta_, bt_max_steps_); + + // Simple early stopping by the norm of the gradient. + if (std::sqrt(fncas::L2Norm(direction)) < grad_eps_ && iteration >= min_steps_) { + break; + } + } + + return OptimizationResult{fi(current_point), current_point}; +} + +// TODO(mzhurovich): Implement more sophisticated version. +template +OptimizationResult ConjugateGradientOptimizer::Optimize(const std::vector& starting_point) { + fncas::reset_internals_singleton(); + const size_t dim = starting_point.size(); + const fncas::x gradient_helper(dim); + fncas::f_intermediate fi(F::compute(gradient_helper)); + fncas::g_intermediate gi = fncas::g_intermediate(gradient_helper, fi); + std::vector current_point(starting_point); + + const auto initial_f_gradf = gi(current_point); + std::vector current_grad = initial_f_gradf.gradient; + std::vector s(current_grad); // Direction to search for a minimum. + fncas::FlipSign(s); // Trying first step against the gradient to minimize the function. + + for (size_t iteration = 0; iteration < max_steps_; ++iteration) { + // Backtracking line search. + const auto new_point = fncas::BackTracking(fi, gi, current_point, s, bt_alpha_, bt_beta_, bt_max_steps_); + const auto new_f_gradf = gi(new_point); + + // Calculating direction for the next step. + const double omega = std::max(fncas::PolakRibiere(new_f_gradf.gradient, current_grad), 0.0); + for (size_t i = 0; i < dim; ++i) { + s[i] = omega * s[i] - new_f_gradf.gradient[i]; + } + + current_grad = new_f_gradf.gradient; + current_point = new_point; + + // Simple early stopping by the norm of the gradient. + if (std::sqrt(L2Norm(s)) < grad_eps_ && iteration >= min_steps_) { + break; + } + } + + return OptimizationResult{fi(current_point), current_point}; +} + +} // namespace fncas + +#endif // #ifndef FNCAS_OPTIMIZE_H diff --git a/test.cc b/test.cc new file mode 100644 index 0000000..5f1f455 --- /dev/null +++ b/test.cc @@ -0,0 +1,275 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2015 Dmitry "Dima" Korolev + (c) 2015 Maxim Zhurovich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*******************************************************************************/ + +#include "../Bricks/3party/gtest/gtest-main.h" + +#ifndef FNCAS_JIT +#define FNCAS_JIT CLANG +#endif + +#include "fncas/fncas.h" +#include + +// TODO(dkorolev)+TODO(mzhurovich): Chat about this `typename fncas::output::type` syntax. We can do better. +template +typename fncas::output::type f(const T& x) { + return (x[0] + x[1] * 2) * (x[0] + x[1] * 2); +} + +TEST(FNCAS, ReallyNativeComputationJustToBeSure) { EXPECT_EQ(25, f(std::vector({1, 2}))); } + +TEST(FNCAS, NativeWrapper) { + fncas::reset_internals_singleton(); + fncas::f_native fn(f>, 2); + EXPECT_EQ(25.0, fn(std::vector({1.0, 2.0}))); +} + +TEST(FNCAS, IntermediateWrapper) { + fncas::reset_internals_singleton(); + fncas::x x(2); + fncas::f_intermediate fi = f(x); + EXPECT_EQ(25.0, fi(std::vector({1.0, 2.0}))); + EXPECT_EQ("((x[0]+(x[1]*2.000000))*(x[0]+(x[1]*2.000000)))", fi.debug_as_string()); +} + +TEST(FNCAS, CompilingWrapper) { + fncas::reset_internals_singleton(); + fncas::x x(2); + fncas::f_intermediate fi = f(x); + fncas::f_compiled fc = fncas::f_compiled(fi); + EXPECT_EQ(25.0, fc(std::vector({1.0, 2.0}))) << fc.lib_filename(); +} + +TEST(FNCAS, GradientsWrapper) { + fncas::reset_internals_singleton(); + auto p_3_3 = std::vector({3.0, 3.0}); + + fncas::g_approximate ga = fncas::g_approximate(f>, 2); + auto d_3_3_approx = ga(p_3_3); + EXPECT_EQ(81.0, d_3_3_approx.value); + EXPECT_NEAR(18.0, d_3_3_approx.gradient[0], 1e-5); + EXPECT_NEAR(36.0, d_3_3_approx.gradient[1], 1e-5); + + const fncas::x x(2); + fncas::g_intermediate gi = fncas::g_intermediate(x, f(x)); + auto d_3_3_intermediate = gi(p_3_3); + EXPECT_EQ(81.0, d_3_3_intermediate.value); + EXPECT_EQ(18.0, d_3_3_intermediate.gradient[0]); + EXPECT_EQ(36.0, d_3_3_intermediate.gradient[1]); +} + +struct StaticFunctionToOptimize { + template + static typename fncas::output::type compute(const T& x) { + // An obviously convex function with a single minimum `f(3, 4) == 1`. + const auto dx = x[0] - 3; + const auto dy = x[1] - 4; + return exp(0.01 * (dx * dx + dy * dy)); + } +}; + +struct MemberFunctionToOptimize { + double a = 0.0; + double b = 0.0; + template + typename fncas::output::type operator()(const T& x) { + // An obviously convex function with a single minimum `f(a, b) == 1`. + const auto dx = x[0] - a; + const auto dy = x[1] - b; + return exp(0.01 * (dx * dx + dy * dy)); + } +}; + +struct PolynomialFunctionToOptimize { + template + static typename fncas::output::type compute(const T& x) { + // An obviously convex function with a single minimum `f(0, 0) == 0`. + const double a = 10.0; + const double b = 0.5; + return (a * x[0] * x[0] + b * x[1] * x[1]); + } +}; + +struct RosenbrockFunctionToOptimize { + template + static typename fncas::output::type compute(const T& x) { + // http://en.wikipedia.org/wiki/Rosenbrock_function + // Non-convex function with global minimum `f(a, a^2) == 0`. + const double a = 1.0; + const double b = 100.0; + const auto d1 = (a - x[0]); + const auto d2 = (x[1] - x[0] * x[0]); + return (d1 * d1 + b * d2 * d2); + } +}; + +struct HimmelblauFunctionToOptimize { + template + static typename fncas::output::type compute(const T& x) { + // http://en.wikipedia.org/wiki/Himmelblau%27s_function + // Non-convex function with four local minima: + // f(3.0, 2.0) = 0.0 + // f(-2.805118, 3.131312) = 0.0 + // f(-3.779310, -3.283186) = 0.0 + // f(3.584428, -1.848126) = 0.0 + const auto d1 = (x[0] * x[0] + x[1] - 11); + const auto d2 = (x[0] + x[1] * x[1] - 7); + return (d1 * d1 + d2 * d2); + } +}; + +TEST(FNCAS, OptimizationOfAStaticFunction) { + const auto result = + fncas::GradientDescentOptimizer().Optimize(std::vector({0, 0})); + EXPECT_NEAR(1.0, result.value, 1e-3); + ASSERT_EQ(2u, result.point.size()); + EXPECT_NEAR(3.0, result.point[0], 1e-3); + EXPECT_NEAR(4.0, result.point[1], 1e-3); +} + +// TODO(mzhurovich): Add member function testing when its support is +// implemented. +/* +TEST(FNCAS, OptimizationOfAMemberFunction) { + MemberFunctionToOptimize f; + f.a = 2.0; + f.b = 1.0; + const auto result = fncas::OptimizeUsingGradientDescent(f, std::vector({0, 0})); + EXPECT_NEAR(1.0, result.value, 1e-3); + ASSERT_EQ(2u, result.point.size()); + EXPECT_NEAR(2.0, result.point[0], 1e-3); + EXPECT_NEAR(1.0, result.point[1], 1e-3); +} +*/ + +TEST(FNCAS, OptimizationOfAPolynomialMemberFunction) { + const auto result = fncas::GradientDescentOptimizer().Optimize( + std::vector({5.0, 20.0})); + EXPECT_NEAR(0.0, result.value, 1e-3); + ASSERT_EQ(2u, result.point.size()); + EXPECT_NEAR(0.0, result.point[0], 1e-3); + EXPECT_NEAR(0.0, result.point[1], 1e-3); +} + +TEST(FNCAS, OptimizationOfAPolynomialUsingBacktrackingGD) { + const auto result = fncas::GradientDescentOptimizerBT().Optimize( + std::vector({5.0, 20.0})); + EXPECT_NEAR(0.0, result.value, 1e-3); + ASSERT_EQ(2u, result.point.size()); + EXPECT_NEAR(0.0, result.point[0], 1e-3); + EXPECT_NEAR(0.0, result.point[1], 1e-3); +} + +TEST(FNCAS, OptimizationOfAPolynomialUsingConjugateGradient) { + const auto result = fncas::ConjugateGradientOptimizer().Optimize( + std::vector({5.0, 20.0})); + EXPECT_NEAR(0.0, result.value, 1e-6); + ASSERT_EQ(2u, result.point.size()); + EXPECT_NEAR(0.0, result.point[0], 1e-6); + EXPECT_NEAR(0.0, result.point[1], 1e-6); +} + +TEST(FNCAS, OptimizationOfRosenbrockUsingConjugateGradient) { + const auto result = fncas::ConjugateGradientOptimizer().Optimize( + std::vector({-3.0, -4.0})); + EXPECT_NEAR(0.0, result.value, 1e-6); + ASSERT_EQ(2u, result.point.size()); + EXPECT_NEAR(1.0, result.point[0], 1e-6); + EXPECT_NEAR(1.0, result.point[1], 1e-6); +} + +TEST(FNCAS, OptimizationOfHimmelbaluUsingCojugateGradient) { + fncas::ConjugateGradientOptimizer optimizer; + const auto min1 = optimizer.Optimize(std::vector({5.0, 5.0})); + EXPECT_NEAR(0.0, min1.value, 1e-6); + ASSERT_EQ(2u, min1.point.size()); + EXPECT_NEAR(3.0, min1.point[0], 1e-6); + EXPECT_NEAR(2.0, min1.point[1], 1e-6); + + const auto min2 = optimizer.Optimize(std::vector({-3.0, 5.0})); + EXPECT_NEAR(0.0, min2.value, 1e-6); + ASSERT_EQ(2u, min2.point.size()); + EXPECT_NEAR(-2.805118, min2.point[0], 1e-6); + EXPECT_NEAR(3.131312, min2.point[1], 1e-6); + + const auto min3 = optimizer.Optimize(std::vector({-5.0, -5.0})); + EXPECT_NEAR(0.0, min3.value, 1e-6); + ASSERT_EQ(2u, min3.point.size()); + EXPECT_NEAR(-3.779310, min3.point[0], 1e-6); + EXPECT_NEAR(-3.283186, min3.point[1], 1e-6); + + const auto min4 = optimizer.Optimize(std::vector({5.0, -5.0})); + EXPECT_NEAR(0.0, min4.value, 1e-6); + ASSERT_EQ(2u, min4.point.size()); + EXPECT_NEAR(3.584428, min4.point[0], 1e-6); + EXPECT_NEAR(-1.848126, min4.point[1], 1e-6); +} + +// Check that gradient descent optimizer with backtracking performs better than +// naive optimizer on Rosenbrock function, when maximum step count = 1000. +TEST(FNCAS, NaiveGDvsBacktrackingGDOnRosenbrockFunction1000Steps) { + fncas::OptimizerParameters params; + params.SetValue("max_steps", 1000); + params.SetValue("step_factor", 0.001); // Used only by naive optimizer. Prevents it from moving to infinity. + const auto result_naive = fncas::GradientDescentOptimizer(params) + .Optimize(std::vector({-3.0, -4.0})); + const auto result_bt = fncas::GradientDescentOptimizerBT(params) + .Optimize(std::vector({-3.0, -4.0})); + const double x0_err_n = std::abs(result_naive.point[0] - 1.0); + const double x0_err_bt = std::abs(result_bt.point[0] - 1.0); + const double x1_err_n = std::abs(result_naive.point[1] - 1.0); + const double x1_err_bt = std::abs(result_bt.point[1] - 1.0); + ASSERT_TRUE(fncas::IsNormal(x0_err_n)); + ASSERT_TRUE(fncas::IsNormal(x1_err_n)); + ASSERT_TRUE(fncas::IsNormal(x0_err_bt)); + ASSERT_TRUE(fncas::IsNormal(x1_err_bt)); + EXPECT_TRUE(x0_err_bt < x0_err_n); + EXPECT_TRUE(x1_err_bt < x1_err_n); + EXPECT_NEAR(1.0, result_bt.point[0], 1e-6); + EXPECT_NEAR(1.0, result_bt.point[1], 1e-6); +} + +// Check that conjugate gradient optimizer performs better than gradient descent +// optimizer with backtracking on Rosenbrock function, when maximum step count = 100. +TEST(FNCAS, ConjugateGDvsBacktrackingGDOnRosenbrockFunction100Steps) { + fncas::OptimizerParameters params; + params.SetValue("max_steps", 100); + const auto result_cg = fncas::ConjugateGradientOptimizer(params) + .Optimize(std::vector({-3.0, -4.0})); + const auto result_bt = fncas::GradientDescentOptimizerBT(params) + .Optimize(std::vector({-3.0, -4.0})); + const double x0_err_cg = std::abs(result_cg.point[0] - 1.0); + const double x0_err_bt = std::abs(result_bt.point[0] - 1.0); + const double x1_err_cg = std::abs(result_cg.point[1] - 1.0); + const double x1_err_bt = std::abs(result_bt.point[1] - 1.0); + ASSERT_TRUE(fncas::IsNormal(x0_err_cg)); + ASSERT_TRUE(fncas::IsNormal(x1_err_cg)); + ASSERT_TRUE(fncas::IsNormal(x0_err_bt)); + ASSERT_TRUE(fncas::IsNormal(x1_err_bt)); + EXPECT_TRUE(x0_err_cg < x0_err_bt); + EXPECT_TRUE(x1_err_cg < x1_err_bt); + EXPECT_NEAR(1.0, result_cg.point[0], 1e-6); + EXPECT_NEAR(1.0, result_cg.point[1], 1e-6); +} diff --git a/test.h b/test.h new file mode 100644 index 0000000..e69de29